TT Lab
はじめる
学ぶ 学習パス コース

Envoyの内部構造

NACK の version_info はなぜ拒否したバージョンではないのか

TT Labで続きを見る

一言でいうと

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をホットリスタートして、処理中のリクエストが途切れないのを見ます。