gRPC Client streamingを使う時のタイムアウトについて

はじめに

gRPC Client streamingを使っていてタイムアウトが起きました。その時に修正が必要だった箇所についてのメモです。デフォルトの設定で大丈夫だったところは特に見ていないので他にも見るべき値があるかもしれません。

gRPC Client streaming

gRPCには4つのRPC方式があります。

  • Unary RPCs - 1リクエストに対して1レスポンス
  • Server streaming RPCs - サーバから複数のメッセージ
  • Client streaming RPCs - クライアントから複数のメッセージ
  • Bidirectional streaming RPCs 双方向に複数のメッセージ

今回はこのうちのClient streaming RPCsを使っている時にクライアントからサーバへしばらく何も送らない状態(アイドルタイム)が続いた時に接続が切れてしまいました。長いアイドルタイムが発生しうる使い方をしていたのですが、特に考えていなかったので*1、これを期に気をつけるところを書いておこうと思います。

次のような構成でgRPC Client streamingを使っていました。

クライアントからのメッセージはGCPのL7ロードバランサーサイドカーEnvoy、そしてサーバというように届きます。ちなみにL7LBはGKEのIngressです。

タイムアウト

gRPCを使う時には2つのコネクションがあります。TCPコネクションとHTTP/2のストリームです。HTTP/1.1だとコネクションというものがないので、TCPコネクションだけ気にしていればよかったのですが、HTTP/2にはストリームがあるので、そちらも気にする必要があります。

GCPのL7LB

ドキュメントに書いてあるとおり、HTTPレスポンスタイムアウトTCPセッションタイムアウトがあります。

レスポンスタイムアウトはデフォルト30秒で、TCPセッションタイムアウトは600秒固定です。

TCPコネクションが切れないようにするには今回の場合LBが600秒で固定されているのでKeepaliveの設定が必要です。クライアントのKeepaliveとEnvoyのKeepaliveを設定すれば大丈夫かと思います。Envoyの設定については後ほど掲載します。

HTTPレスポンスタイムアウトが起きるのは、Client streamingがクライアントからはたくさんメッセージは送るもののサーバはレスポンスを返さないからです。これに関してはLBの設定で十分長く設定する必要があります。もし負荷が気になる場合はサーキットブレーカーやスケーリングなど考慮する必要はあるかと思います。レスポンスタイムアウトバックエンドサービスごとに設定できるので、アイドルタイムが長くなるサービスを分けて設定するのが良さそうです。バックエンドサービスの設定はバックエンドコンフィグのYAMLで設定できます。以下に例を載せます。

apiVersion: cloud.google.com/v1beta1
kind: BackendConfig
metadata:
  name: long-lived-connection-service
spec:
  timeoutSec: 1800
  connectionDraining:
    drainingTimeoutSec: 60

Envoy

EnvoyもHTTPレスポンスタイムアウト(に対応するストリームアイドルタイムアウト)とTCPセッションタイムアウトがあります。ココに書いてあるように、ストリームアイドルタイムアウトが5分なので、アイドル状態が5分以上続く可能性があるコネクションがあれば設定が必要です。TCPコネクションはデフォルトでアイドルタイムアウトは設定されないので、Keepaliveにするかどうかはサービスごとに考慮して設定します。

ストリームアイドルタイムアウトの設定はEnvoyのルート設定に書きます。以下に例を掲載します。idle_timeoutとmax_grpc_timeoutの設定が必要です*2

    routes:
      - match:
          prefix: /longlived.Service/
          grpc:
        route:
          cluster: longlived-service
          idle_timeout: 1800s
          max_grpc_timeout: 1800s

idle_timeoutがストリームタイムアウトにあたるのですが、max_grpc_timeoutを読むと以下の書いてあります。

This can be used to prevent unexpected upstream request timeouts due to potentially long time gaps between gRPC request and response in gRPC streaming mode.

gRPCの場合はこの設定もいるようです。逆にmax_grpc_timeoutの設定だけでいいのかと思ったのですが、idle_timeoutも設定しないとうまくいきませんでした。要はアイドルタイムアウトとレスポンスタイムアウトは別物という感じでしょうか。

GCPのL7LBとEnvoy間の接続

GCPのL7LBとEnvoy間のTCPコネクションが切れないようにEnvoy側のKeepaliveを設定する必要があります。

EnvoyのダウンストリームでTCP Keepaliveを設定するにはEnvoyの設定を以下のようにします。設定の詳細はコチラにあります。

static_resources:
  listeners:
  - name: ingress
    address:
      socket_address:
        address: 0.0.0.0
        port_value: 9090
    socket_options:
    - level: 1
      name: 9
      int_value: 1
      state: STATE_LISTENING
    - level: 6
      name: 4
      int_value: 30
      state: STATE_LISTENING
    - level: 6
      name: 5
      int_value: 30
      state: STATE_LISTENING

おわりに

いろいろ見落としが多い気がしています。クライアントも接続は切れる可能性があるのでエラー吐いて繋ぎ直すのが良いのか、プログラムを落とすのが良いのか、時と場合によるのか。難しい...*3

*1:だめですね

*2:ちなみに0sで無限になるように書いてあったのですがなぜかバリデーションエラーになってしまいました

*3:風呂に入って寝よう