gRPCのロードバランシング

先日の記事から引き続きgRPCについて勉強してる。

gRPCのサーバをプロダクトで利用する場合に気になるのが、ロードバランシングをどういう風にやったら良いのかということで、その部分について調べてみた。

TL;DR: gRPC Load Balancing を読めばだいたいわかる

gRPCのロードバランシングのポイントとしては、gRPCが基本的にはHTTP2上に構築された仕組みである*1ことに注意して考えると良さそうだった。

プロキシ によるロードバランシング

まず考えられるのは、gRPCのサーバとクライアントの間にプロキシを設置してロードバランシングを行う方法だ。

よくあるHTTP/1.1の世界で考えると、複数のWebアプリケーションサーバの前段にnginxのようなリバースプロキシを設置してロードバランシングする方法になる。

gRPCはHTTP/2を利用するので、この方法の場合リバースプロキシもHTTP/2を理解できる必要がある。これを実際に行うには、例えば、先月リリースされたNginxのgRPCサポート機能が利用できる。一方、AWSの環境でよく利用されるALBは、プロキシの後ろ側の通信がHTTP/1.1になるため利用できない(参考: gRPC アプリケーションを AWS で動かすときの注意点 ) 。

上記の方法ではL7でのプロキシを用いていたが、L4のプロキシでロードバランシングする方法も考えられる。TCPのコネクションレベルでロードバランシングを行うのでL7のプロトコルがHTTP/2でも影響をうけない。これを実際に行うには haproxyを利用したり、AWSのNLBが利用できる。

気をつけないと行けないのは、HTTP/2の利点を活かすためgRPCのクライアントがなるべく一つのコネクションを使い続けるような仕組みになっていることだ。L4のプロキシによるロードバランシングが働くのはTCPのコネクションを張るときなので、そのコネクションを利用している限りは同じサーバを利用し続けることになる。

例えば、Webアプリケーションが1リクエスト処理するごとにgRPCのコネクションを毎回張るような実装であれば、L4プロキシによるロードバランシングでも十分よく機能するだろう。一方、サーバ起動時に一度だけつくったgRPCクライアントを使い続ける場合、意図せず同じ一つのgRPCサーバとのみ通信をすることになるかもしれない。

クライアントサイド ロードバランシング

ロードバランシングというと前述のようなプロキシサーバを用いた形式を考えてしまうのだけれども(僕は)、gRPC界隈ではクライアントサイドでのプロキシの仕組みも充実している。

クライアントサイドロードバランシングは、gRPCのクライアントがいくつかの接続先のサーバを知っており、gRPCメソッドの呼び出し時などに適宜、利用するサーバを切り替えることでロードバランシングをする方法だ。プロキシサーバを中継しないため、高速に動作するが、gRPCクライアントの実装が複雑になる。実際クライアントサイドバランシングは、どのgRPCクライアントにも実装されているわけではなく、少なくともGoとJavaには実装があるような状態だった。

GoのgRPCのクライアントライブラリにはround robin 方式のロードバランサー(grpc.RoundRobin)だけが実装されている。この grpc.RoundRobin は接続先のサーバの名前解決をする naming.Resolver を受け取るようになっている。

naming.Resolverの実装としては、DNSを用いたものが付属していて、DNSにホスト名を問い合わせることで幾つかのサーバのアドレスを取得して、gRPCの接続先のサーバの候補に加えるという仕組みになっている。naming.Resolverはワンショットで動作するのではなく、定期的に名前解決を実行して、サーバの候補を更新する。naming.Resolverの実装はDNSを用いる必要はなく、例えばEtcdにサーバの一覧を問い合わせるといった実装も可能になっている。

このような名前解決の仕組みだけでなく、サーバ側の負荷をフィードバックする仕組みなど、様々なロードバランサーの機能をクライアントライブラリごとに実装するのは大変なので、その部分だけを別のシステムに移譲する、extenal load balancingという仕組みを利用することもできる。gRPCのロードバランシングのコンセプトまとめたドキュメントには、external load balancerについても説明されている。おもしろい仕組みだけどもあまり実装はないようである(僕は grpclb っていうのだけ見つけることができた)。

結局どうすればいいのか

gRPC Load Balancing という記事の最下部にユースケース別のロードバランシングのおすすめ表があるので、これを参考にしまくると良い。Kubernetesを利用する場合は deeeetさんの
Kubernetes上でgRPCサービスを動かす | SOTA がめちゃくちゃ参考になりそうだった。

自分としては運用のイメージのしやすさとしては、nginxのgRPCサポート機能を使うかL4のロードバランサーを気をつけて利用するのが良さそうには思った。

gRPCの利用シーンとしてはマイクロサービスのインターフェースがやはりいちばん考えられるので、クライアントサイドロードバランシングをうまく使いこなせれば、より効率的な構成にできそうには思うが、サーバ構成をアプリケーションからみて動的に解決できる状態にまずする必要がありそうで、まぁ準備が大変そうだなという感想。

おまけ

クライアントサイドロードバランシングの雰囲気をつかむために例になるようなgRPCのサーバとクライントを実装してみた。

上述した、Goに付属のgrpc.RoundRobinを用いている。これもGoに付属しているDNSを用いたnaming.Resolverを動作させようとすると、DNSの設定を行う必要があって大変なので、初期値として与えた固定のgRPCサーバの列を返すnaming.Resolver を雑に実装して試せるようにしている。

github.com

次のステップ

たぶん grpc-gatewayの雰囲気をみておくと良い。grpcのクライアントの感じとかも調べると良さそう。

*1:正確にはトランスポート層は別のプロトコルを利用することもできる