gRPC streamとServer-Sent Eventsを利用したサーバプッシュ型ミドルウェアを世に放った

以前のエントリにて、gRPCとServer-Sent Eventsを利用したサーバプッシュ型ミドルウェアを作ったという話を書いた。今回のエントリは、Production環境に配置し、Web/iOS/Androidの3デバイスにおいて世に放ったというエントリ。

gRPCとServer-Sent Eventsでサーバプッシュできるplasmaを公開しました

このミドルウェアはPlasmaと名付けられていて、3月に春休みを使ってインターンをしに来た恐ろしく徳の高い大学生によって書かれた。

実装に関する馴れ初めについては彼のエントリを参照して欲しい。

サイバーエージェントのインターンに参加してポーリング撲滅しようとしてきた

このPlasmaを構想するに至った動機だが、Webやスマートフォンアプリが参照している画面やコンポーネントの状態変化をサーバプッシュによって検知し、何かしらの処理をするということをやりたかったに他ならない。これまでは、クライアントから定期的にAPIをコールして(ポーリング)状態に変化があった場合に処理を発火していた。

FRESH!においては、番組の放送前から放送中への状態の変化を知るためにポーリングをしていた。また、それ以外にもサーバプッシュの需要を強く感じたため、汎用的なサーバプッシュミドルウェアとして開発することになった。

経緯については天皇賞のついでに京都に行った際の、そうだ Go、京都。でもその話をしている。

この時点はPlasmaの初期形態ができあがり、本番運用するための課題を潰していくフェーズに入ったばかりだった。プロジェクトのSlackで#kill_pollingというチャンネルが作られ、クライアント側も「さあこれから触っていこうか」という感じでポーリング撲滅という意味では進捗10%くらいだった。

あれからどうなった

ついに先日無事3デバイスともリリースが完了し、ポーリングからの脱却を果たした(強制アップデートはしてないので、正確に言うと旧バージョンのアプリからはまだポーリングされてる)。

番組の状態変化

以前と見た目上変わった面はほとんど無いが、以前は定期的にAPIにリクエストを投げていたため番組開始後即座にViewが変更されるというわけではなかった。それがPlasmaによる番組開始イベント後すぐさま開始されるようになっている。

image

また、視聴数やコメント数といったデータもPlasmaから配信するようになった。これにより大幅なHTTPリクエストの削減と、DBやキャッシュへのアクセスが削減された。

テロップ

番組の上部にアルファで表示されるテロップもPlasmaによって配信されている。これは配信主が自由なタイミングでテキストを更新したり消したりすることができるものでPlasmaの活用にうってつけの要件。

image

その他でもPlasmaを活用した新機能のリリースを控えている。

Service間でのキャッシュの伝搬

このユースケースは以前のエントリで書いたので、リンクだけ置いておこう。クライアントだけではなくバックエンド用途においても力を発揮できる。

microservices間でデータ変更をReactiveに伝搬させる

クライアントからの接続

WebはPlasmaをgRPCではなくSSE(Server-Sent Events)を利用しているが、こちらはそこそこスムースに導入ができた。難産だったのがやはりgRPCの方で、クライアント側は初gRPCがいきなりbidirectional streamだったのでgRPCの概念はもちろんだが、クライアントメンバーにとってはstreamの扱いがとっつきにくかったと思われる。

再接続のケア

gRPCサーバのダウンや、デプロイによる切り替えによって接続が切断されることが当然ある。gRPCのディスカバリや負荷分散には色々な手法があって、サーバサイドでLBを用意して最初のディスカバリに使う方式や、クライアントサイドでディスカバリしてロードバランスする手法がある。

今回はサーバでLBを用意する方を選んだ。AWSなのでELB。ALBは基本L7 LayerなのでgRPCを使うのであればClassic ELBを選択することになると思う。

gRPCは接続が切れた際のbackoffの仕組みが備わっている。そのため自動的に接続を回復しようとする動きになる。ELBに他にhealthyなPlasmaが存在すればそのどれかにbackoffする。

PlasmaはgRPCの接続が確立された後、subscribeやメッセージの受取をbidirectional streamで双方向にやりとりを行う。gRPCのコネクションとstreamはそもそも別の概念なので、このstreamを再確立するためのRPCは再度実行することが必要となる。Plasmaで言うと、Eventsメソッドがこれにあたる。

service StreamService {
    rpc Events(stream Request) returns (stream Payload) {}
}

TLS接続

gRPCへの接続は基本的にHTTP2であり、かつHTTPSによるセキュアな接続が求められる。インターナルなネットワークにおけるサーバ間通信であれば、InSecureにするオプションがあるのでそれを設定すればいいわけだが、Plasmaはエンドユーザーから直接接続が来るので必然的にPublicであり、TLSでのセキュアな接続が求められることになる 。

クライアントからgRPCでTLS接続する場合、証明書を指定する必要がある。例えばSwiftでルート証明書を設定する場合こんな感じのコードを書く。pemRootCertは証明書のパスじゃなくて内容の文字列。

public static func setTLSPEMRootCerts(pemRootCert: String, forHost host: String) throws {
    try GRPCCall.setTLSPEMRootCerts(pemRootCert, forHost: host)
}

一般的にHTTPS通信する場合は、端末やPCにインストール・自動更新されるルート証明書が自動で選択されているのであまり存在を意識してプログラムを書くことは少ない。gRPCにおいては基本的に言語を問わず、TLS接続をするための証明書の指定が必須となっている。この証明書を実際にどのように運用していくべきか?を悩むケースが結構多いのではないかと思う。

最初は端末にインストールされているはずのルート証明書を活用できないかを検討したが、パーミッションレベルでの壁がありそうで回避した(Androidは証明書見れるアプリがあったりするので、がんばればなんとかなるかもしれないが)。そのため以下の手法を取ることにした。

  • 必要なルート証明書をWebからダウンロードできるようにする。かつ高可用性を担保して安定的に配信できるようにする
  • 端末のストレージにダウンロードしてきたルート証明書を保存しておく。保存した証明書をgRPC接続の際に利用
  • ルート証明書は有効期限があるのでいずれ更新される。アプリケーションからは低頻度でストレージに保存されている証明書が最新であるか?をチェックするためのAPIを実行。このAPIのレスポンスには最新の証明書のchecksumが含まれており、ストレージに保存した証明書のchecksumと照合して更新すべきかを判断する

という手法を取った。これがベストプラクティスかどうかはともかく、現時点では現実的な落とし所だったのではないかと考えてる。

まとめ

おそらく、gRPCはサーバよりクライアント側の最初の導入や、継続的な運用のためのケアを考えるとそこそこ苦労が多い。ただ、今回Plasmaを通して得られた利はかなり大きく、gRPCの知見も貯めることができたので次のロードマップにあるAPIのフルgRPC化への足がかりになった。

gRPC導入で苦労の話、今後も各所で増えそうな気がするのでクライアントメンバーと共に一度どっかで喋っておきたいところ。