NACK の version_info はなぜ拒否したバージョンではないのか
一言でいうと
ADSは、Envoyとコントロールプレーンの間にgRPCストリーム1本を開いておき、すべてのタイプ(CDS・LDS・…)の設定をその上でやり取りする方式です。応答にはバージョンとnonceが付き、Envoyは適用してみた結果を、同じnonceの次のリクエストで知らせます。理由が空ならACK、入っていればNACKです。
なぜ必要なのか
ファイル購読も設定を無停止で変えてくれますが、数千のプロキシにファイルを配る方法がなく、プロキシがその設定を受け入れたかを知る手段もありません。コントロールプレーンが最初に知るべきことが、まさにそれです。自分が送った設定は適用されたか、拒否されたか、拒否されたならなぜか。
そこで、xDSにはストリーム方式があります。プロキシが先に接続を開き(プロキシ側から出ていく接続なので、ファイアウォールの内側のプロキシも接続できます)、コントロールプレーンは、そのストリーム上で、必要なときに送り込みます。タイプごとにストリームを別々に開くこともできますが、そうすると「クラスターを先に受け取り、それを使うリスナーを後で」のような順序を保証できません。ADS(Aggregated Discovery Service)は、すべてのタイプを1つのストリームにまとめて、コントロールプレーンが順序を決められるようにします。Istioのistiodとサイドカーが使っているのが、この方式です。
どう動くのか
1回のやり取り。State of the World(SotW)方式を基準にします。
Envoy → 요청 type=Cluster version_info="" response_nonce="" (처음)
서버 → 응답 type=Cluster version_info="1" nonce="1" resources=[pool, slow]
Envoy → 요청 type=Cluster version_info="1" response_nonce="1" ← ACK
… 파일이 바뀌어 서버가 밀어 넣는다 …
서버 → 응답 type=Cluster version_info="3" nonce="5" resources=[…]
Envoy → 요청 type=Cluster version_info="2" response_nonce="5"
error_detail="…ConnectTimeout: value must be greater than 0s" ← NACK
読み方は3つあります。
| 項目 | 意味 |
|---|---|
response_nonce |
どの応答に対する返事か。空なら新しい購読リクエストです |
error_detail |
入っていればNACK。理由が文字列で届きます |
version_info |
最後に受け入れたバージョン。NACKでも、拒否したバージョンではありません |
最後の行が、最も紛らわしいところです。NACKのリクエストのversion_infoは、拒否した3ではなく、いまも使っている2です。拒否した応答は、nonceで指します。
State of the World方式では、応答がそのタイプのリソースをすべて含みます。1つだけ変えても、すべてを送り直します。リソースが多いメッシュでは、これが負担になるので、変わったものだけを送る増分(Delta)方式が別にあります。
タイプごとに別々に適用されます。同じスナップショットのバージョン3で、リスナーは問題なく、クラスターだけが間違っていたなら、LDSは3を受け入れ、CDSは2にとどまります。統計cluster_manager.cds.version_textとlistener_manager.lds.version_textが、互いに異なる値を示す瞬間です。
コントロールプレーンがいなくなると、プロキシは現在の設定で動き続けます(control_plane.connected_stateが0)。再接続するとき、Envoyは何も持たずには始めず、タイプごとに最後に受け入れたバージョンを最初のリクエストに入れます。サーバーはそれを見て、何を再送すべきか判断できます。
ホットリスタートは、プロキシ自身を入れ替えるとき(バイナリの交換、ブートストラップの変更)に使う仕組みです。新しいプロセス(epoch N+1)が同じ--base-idで古いプロセスを見つけ、リスナーソケットを引き継ぎ、古いプロセスは--drain-time-sの間、処理中のリクエストを終わらせてから、--parent-shutdown-time-sに退きます。ポートが一瞬も閉じません。その代わり、新しいプロセスはxDSの状態を引き継がず、コントロールプレーンに新しく接続して、最初から受け取ります。
現場での姿
「設定を変えたのに、一部のプロキシだけが古い動作です」。NACKが最もよくある原因です。Istioでは、istioctl proxy-statusが、プロキシごとにCDS・LDSがSYNCEDかを見せてくれ、拒否があればistiodのログに理由が残ります。トラフィックは流れ続けるので、誰も見ていなければ数週間続きます。
「istiodを再起動したら、すべてのサイドカーが一斉に再接続しました」。再接続するとき、各自が最後のバージョンを示すので、コントロールプレーンは、その数千の最初のリクエストに答える必要があります。コントロールプレーンを複数台にする理由です。
「Envoyを再起動すると、接続が切れます」。ゲートウェイのように、長く保持される接続が多い場所では、ホットリスタートや、ドレインを経た順次入れ替えが必要です。Kubernetesのサイドカーは、Pod丸ごと入れ替えるので、ホットリスタートは使いません。Istioが--disable-hot-restartで起動するのも、そのためです。
公式ドキュメント: xDS REST and gRPC protocol・Aggregated Discovery Service・Hot restart・Command line options
次のラボですること
スナップショットファイルを1つ読んで、CDS・LDSを押し込むADSサーバーをPythonで自分で書き、静的設定なしで、そのサーバーからすべてを受け取るEnvoyを起動します。エンドポイントを外して再起動なしで反映されること、わざと間違ったクラスターを送ってNACKとその理由が返ってくること、サーバーが死んで復活するときにEnvoyが示すバージョンを、ログで読みます。最後に、そのEnvoyをホットリスタートして、処理中のリクエストが途切れないのを見ます。