※ この記事に書いてあるのは「ベストな方法」ではないです。時間に制約がある中でとりあえずこうやって問題解決した、今のところは良さげ、という記事です。
gRPCサーバのロードバランスを行う方法については色々な方法があって、それをくだくだ書くことはしない。リンク先を参照されたし。
まー要は
- クライアント側でやる
- Proxy挟んでそいつにやらせる
- L4でロードバランス
- L7でロードバランス
のどれかを選ぶことになる。サービスがコンテナベースで動いているなら、EnvoyとかのSideCarにやらせるのが一番センスいい気がしている。
で、ちょっとそのためだけに実環境をコンテナ化するわけにもいかんし、Proxyサーバを別に運用したくなんてないし、そしてAWSのL7ロードバランサであるALBはバックエンドのHTTP/2通信に対応していないクソである。どうするか。色々考えた末にNLBを使ってL4でロードバランスすることにした。
この方法には問題があって、要はL4だとメッセージ単位でなくコネクション単位でしかロードバランスしないので「あんまりロードバランスできていない」感じになってしまう。まあそこは妥協しよう。
で、やっていったところ、開発環境で問題が起こった。
上記リンク先にあるように、NLBは勝手にtimeoutしてコネクションを切る。最大3600秒か。これが起こるとクライアント側が通信しようとした時点でサーバ側からRSTが送りつけられ、gRPC的にはUNAVAILABLEエラーが発生する。困ったことにこれが発生するとコネクションが腐ったような状態になってしまい、gRPCサーバを一度再起動しないと直らない。困ったな、困ったな。
だったらそもそもkeepaliveせずに都度接続すりゃいいんじゃないか?と思って色々試したが、gRPCクライアントのコネクション管理は内部的にChannelと呼ばれる何かによって行われており、Rubyレベルでオブジェクトを破棄してもTCPコネクションを閉じてはくれない。そして明示的に閉じるインタフェースもない。困ったな、困ったな。どうやらgRPCではユーザ側でTCPコネクションの状態を制御するもんじゃないらしい。全ては俺たち (Channel) に任せろ、お前らは手を出すな。そういう思想らしい。
それならkeepaliveの上でheartbeatをちゃんと打つように設定するか、と思ってクライアント側やサーバ側の設定値をいろいろいじっても、heartbeat送ってる感全然なし。俺はtsharkとにらめっこ。なんだこりゃ!設定が反映されないぞ。困ったな、困ったな。こんなことやってるとリリースに間に合わんぞ。
どーも調べると、クライアント側にも設定自体はできるが意味ないオプションがあったりするようだ。それで色々いじっているうち、「サーバ側から一定時間でコネクションを切る」というオプションはちゃんと動くことがわかった。
grpc.max_connection_age_ms
これだ。
これを設定すると、一定時間以上接続の続いているコネクション(ストリーム?)にはHTTP/2のGOAWAYフレームが送出されるようになる。なお、クライアント側では切断時にGOAWAYエラーが発生するようになった。
ちなみにgRPCの公式な仕様として、「サーバ側から明示的にコネクション切る際にはGOAWAYを送る」ということになっている。らしい。
grpc/PROTOCOL-HTTP2.md at master · grpc/grpc · GitHub
なので、この設定を入れることによって
「3600秒で勝手にNLBがコネクションを切り、クライアント側はUNAVAILABLEエラーを受け取る。以降、コネクションが腐ったような状態になる」
から
「一定時間経過で明示的にGOAWAYフレームが送られ、クライアント側はGOAWAYエラーを受け取る」
に変わったことになる。微妙な違いのように見えるが、事態をハンドリングできているだけ後者の方がだいぶマシである。
で、このままだと新規リクエストを送る際にGOAWAYエラーが発生してしまうことがある(明示的にGOAWAY送る設定にしたんだから当たり前だけど)。カバーするためにクライアント側でGOAWAYの時だけリトライを行うようにして解決しました。ちゃんちゃん。
あ、ちなみにクライアント側の言語はRubyでした。