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

Envoyの内部構造

スナップショット一つ、ストリーム一本

TT Labで続きを見る

目標

スナップショットファイルを読んでCDS・LDSを押し込む最小のADSサーバーを自分で書き、そのサーバーからすべての設定を受け取るEnvoyで、ACK・NACK・再接続・ホットリスタートを、ストリーム上で観察します。

なぜ重要なのか

istiodとサイドカーの間で起きていることが、これです。ところがメッシュでは、そのやり取りが見えないので、「設定を変えたのに、一部だけが古い動作」のような症状に出会ったとき、どこから見ればよいかわかりません。自分で書いたサーバーのログで、nonce・ACK・NACK・version_infoを一度読んでみれば、proxy-statusのSYNCEDと、istiodの拒否ログが何を指すのかがわかります。

ステップ

  1. アップストリームを3つ起動してください。python3 /opt/lab/envoy/upstream.py 8096 ok、… 8097 ok、… 8098 slowです。そして/root/envd-ads/snapshot.jsonに、バージョン"1"のスナップショットを書いてください。clustersにpool(8096・8097の2つのエンドポイント、connect_timeoutは1s)とslow(8098)、listenersに127.0.0.1:10088のedgeを置きます。パス/markerは本文snapshot=1を直接返し、/slowはslowへ、それ以外はpoolへ送ります。リソースは、EnvoyのJSONの形そのままで書きます。
  2. /root/envd-ads/ads_server.pyに、envoy.service.discovery.v3.AggregatedDiscoveryServiceのStreamAggregatedResourcesを実装して、127.0.0.1:18000で起動してください(/opt/xds/bin/python3で実行)。要件は4つあります。1つ目は、スナップショットファイルを読んで、リクエストされたタイプ(CDS・LDS)のリソースをすべて、version_info・nonceと一緒に送ることです。2つ目は、ファイルのバージョンが変わったら、開いているストリームに、リクエストを待たずに新しい応答を押し込むことです。3つ目は、受け取ったリクエストごとに、/root/envd-ads/ads.logにJSONを1行残すことです(event(nonceが空ならrequest、error_detailがあればnack、そうでなければack)・stream・node・type・version_info・response_nonce・error)。4つ目は、応答を送るときも、event: sentとバージョン・nonceを残すことです。
  3. /root/envd-ads/bootstrap.yamlに、ノードidはenvd-ads-1(clusterはenvd-ads)、管理ポートは9988、dynamic_resourcesのads_config(gRPC・V3、クラスターxds_cluster)と、cds_config・lds_configをads: {}で書き、静的リソースは、コントロールプレーンに届くためのxds_cluster(127.0.0.1:18000、HTTP/2)を1つだけ置きます。envoy -c /root/envd-ads/bootstrap.yaml --base-id 50 --restart-epoch 0 --concurrency 1 --drain-time-s 5 --parent-shutdown-time-s 10で起動してください。
  4. スナップショットをバージョン"2"に変えてください。poolから8097を外し、/markerの本文をsnapshot=2にします。ファイルは、新しく書いてからmvで差し替えます。Envoyが受け入れた後で、/root/envd-ads/04-push.txtにmarker=(/markerの応答本文)とendpoints=(/clustersに残ったpoolのエンドポイント数)の2行を書いてください。
  5. スナップショットをバージョン"3"に変えて、poolのconnect_timeoutを"0s"に、/markerの本文をsnapshot=3にしてください。次に、/root/envd-ads/05-nack.txtに4行を書いてください。ads.logで見つけたCDSのNACKのversion_infoをnack_version_info=に、拒否の理由のうちConnectTimeoutで始まる部分をreason=に、そしていまのEnvoyのcluster_manager.cds.version_text・listener_manager.lds.version_textを、cds_version=・lds_version=に書きます。
  6. ADSサーバーを止めて、/root/envd-ads/06-down.txtにtraffic=(/のリクエストのステータスコード)とconnected=(control_plane.connected_state)の2行を書いてください。次に、スナップショットをバージョン"4"(3からconnect_timeoutを1sに戻し、/markerはsnapshot=4)に直してから、サーバーを起動し直し、Envoyが再接続してバージョン4を受け入れるまで待ってください。再接続したストリームの最初のCDSリクエストが持ってきたversion_infoを、resume_cds_version=として、同じファイルに追記します。
  7. /slow(3秒かかる)にリクエストを1つバックグラウンドで送り、結果を/root/envd-ads/inflight.txtに受け取るようにして、そのリクエストが処理されている間に、同じブートストラップでepoch 1のEnvoyを起動してください(--base-id 50 --restart-epoch 1 --concurrency 1 --drain-time-s 5 --parent-shutdown-time-s 10)。新しいプロセスがLIVEになり、古いプロセスが退いた後で、/root/envd-ads/07-restart.txtにepoch=(管理ポートの/server_infoのrestart_epoch)、inflight=(バックグラウンドのリクエストのステータスコード)、new_stream_version=(新しいプロセスが開いたストリームの最初のCDSリクエストのversion_info。空ならempty)の3行を書いてください。
  8. /root/envd-ads/08-report.mdに、次の5行を書き、その下に学んだことを4行以上書いてください。first_request_version=(Envoyが一番最初に送ったCDSリクエストのversion_info、空ならempty)、nack_version_info=、resume_cds_version=(ステップ5・6の記録)、epoch_after_restart=、inflight_code=(ステップ7の記録)です。

参考

押し込む設定を、スナップショットとして書く

アップストリームを3つ起動してください。python3 /opt/lab/envoy/upstream.py 8096 ok、… 8097 ok、… 8098 slowです。そして/root/envd-ads/snapshot.jsonに、バージョン"1"のスナップショットを書いてください。clustersにpool(8096・8097の2つのエンドポイント、connect_timeoutは1s)とslow(8098)、listenersに127.0.0.1:10088のedgeを置きます。パス/markerは本文snapshot=1を直接返し、/slowはslowへ、それ以外はpoolへ送ります。リソースは、EnvoyのJSONの形そのままで書きます。

コントロールプレーンの仕事は、結局のところ「いまこのプロキシが持つべきリソースのすべて」を、バージョン1つにまとめておくことです。そのまとまりを、スナップショットと呼びます。リソースは、Cluster・Listenerのprotobufの、JSON表現のとおりに書けばよく、リスナーの中のtyped_configには@typeが必要です。採点ツールは、このJSONをxds-protosで、実際のprotobufとして読んでみます。フィールド名が1つ間違っていても、そこで引っかかります。

最小のADSサーバーを書く

/root/envd-ads/ads_server.pyに、envoy.service.discovery.v3.AggregatedDiscoveryServiceのStreamAggregatedResourcesを実装して、127.0.0.1:18000で起動してください(/opt/xds/bin/python3で実行)。要件は4つあります。1つ目は、スナップショットファイルを読んで、リクエストされたタイプ(CDS・LDS)のリソースをすべて、version_info・nonceと一緒に送ることです。2つ目は、ファイルのバージョンが変わったら、開いているストリームに、リクエストを待たずに新しい応答を押し込むことです。3つ目は、受け取ったリクエストごとに、/root/envd-ads/ads.logにJSONを1行残すことです(event(nonceが空ならrequest、error_detailがあればnack、そうでなければack)・stream・node・type・version_info・response_nonce・error)。4つ目は、応答を送るときも、event: sentとバージョン・nonceを残すことです。

gRPCの双方向ストリームは、Pythonでは「リクエストのイテレーターを受け取って、応答をyieldする関数」です。ところが、リクエストを待っている間にも、スナップショットの変化を押し込む必要があるので、リクエストは別のスレッドが読んでキューに入れ、メインのループはキューを短く待ちながらスナップショットを確認する形が楽です。リソースは、google.protobuf.any_pb2.AnyにPackして入れ、JSONをprotobufに変えるときは、json_format.ParseDictを使います(リスナーの中のHCM・routerのタイプを先にimportしておかないと、解決できません)。採点ツールは、このサーバーに直接ストリームを開いて、CDSをリクエストしてみます。

静的設定なしで、コントロールプレーンからすべてを受け取る

/root/envd-ads/bootstrap.yamlに、ノードidはenvd-ads-1(clusterはenvd-ads)、管理ポートは9988、dynamic_resourcesのads_config(gRPC・V3、クラスターxds_cluster)と、cds_config・lds_configをads: {}で書き、静的リソースは、コントロールプレーンに届くためのxds_cluster(127.0.0.1:18000、HTTP/2)を1つだけ置きます。envoy -c /root/envd-ads/bootstrap.yaml --base-id 50 --restart-epoch 0 --concurrency 1 --drain-time-s 5 --parent-shutdown-time-s 10で起動してください。

ブートストラップに残るのは、「コントロールプレーンを探しに行く道」だけです。リスナーも業務用のクラスターも、すべてADSで受け取ります。ads: {}は、「CDS・LDSを別々に購読せず、ADSストリーム1本で受け取れ」という意味なので、2つのタイプが1つのストリームで、順番にやり取りされます。--base-id・--restart-epochは、ステップ7のホットリスタートのために、いまから付けておきます。うまくつながったかは、統計control_plane.connected_stateと、curl localhost:9988/config_dumpの動的な場所で確認します。

再起動なしでエンドポイントを外す

スナップショットをバージョン"2"に変えてください。poolから8097を外し、/markerの本文をsnapshot=2にします。ファイルは、新しく書いてからmvで差し替えます。Envoyが受け入れた後で、/root/envd-ads/04-push.txtにmarker=(/markerの応答本文)とendpoints=(/clustersに残ったpoolのエンドポイント数)の2行を書いてください。

サーバーは、ファイルが変わったことを自分で察知して、開いているストリームに新しい応答を送ります。Envoyが受け入れたかは、ads.logのack(version_infoが2)と、統計cluster_manager.cds.version_textで確認します。書きかけのファイルをサーバーが読まないように、新しいファイルに書いてから移してください。

Envoyが拒否した設定は、どう返ってくるか

スナップショットをバージョン"3"に変えて、poolのconnect_timeoutを"0s"に、/markerの本文をsnapshot=3にしてください。次に、/root/envd-ads/05-nack.txtに4行を書いてください。ads.logで見つけたCDSのNACKのversion_infoをnack_version_info=に、拒否の理由のうちConnectTimeoutで始まる部分をreason=に、そしていまのEnvoyのcluster_manager.cds.version_text・listener_manager.lds.version_textを、cds_version=・lds_version=に書きます。

0秒のタイムアウトは、protobufとしては問題ないので、サーバーのJSON変換を通ります。引っかかる場所は、Envoyの検証ルールで、Envoyはその応答を適用しないまま、同じnonceでもう一度リクエストし、error_detailに理由を入れて返します。そのときのversion_infoが何かを見てください。そして、CDSとLDSは、同じスナップショットから来ていても、別々に適用されます。リスナーは問題がないので、受け入れられます。/markerで何が変わったかも、確認してみてください。

コントロールプレーンが死んで、戻ってくる

ADSサーバーを止めて、/root/envd-ads/06-down.txtにtraffic=(/のリクエストのステータスコード)とconnected=(control_plane.connected_state)の2行を書いてください。次に、スナップショットをバージョン"4"(3からconnect_timeoutを1sに戻し、/markerはsnapshot=4)に直してから、サーバーを起動し直し、Envoyが再接続してバージョン4を受け入れるまで待ってください。再接続したストリームの最初のCDSリクエストが持ってきたversion_infoを、resume_cds_version=として、同じファイルに追記します。

すでに設定を受け取ったEnvoyは、コントロールプレーンがなくても、その設定で動きます。止まるのは、新しい設定を受け取ることだけです。再接続するとき、Envoyは何も持たずには始めず、タイプごとに最後に受け入れたバージョンを、最初のリクエストに入れて送ります。サーバーを起動し直すと、ログにserver_startedが新しく出るので、その後の最初のClusterのrequestを見てください。pkill -fのパターンは、'[a]ds_server.py'のように、最初の文字を角括弧で囲んでください。

プロキシも止めずに、新しいプロセスに入れ替える

/slow(3秒かかる)にリクエストを1つバックグラウンドで送り、結果を/root/envd-ads/inflight.txtに受け取るようにして、そのリクエストが処理されている間に、同じブートストラップでepoch 1のEnvoyを起動してください(--base-id 50 --restart-epoch 1 --concurrency 1 --drain-time-s 5 --parent-shutdown-time-s 10)。新しいプロセスがLIVEになり、古いプロセスが退いた後で、/root/envd-ads/07-restart.txtにepoch=(管理ポートの/server_infoのrestart_epoch)、inflight=(バックグラウンドのリクエストのステータスコード)、new_stream_version=(新しいプロセスが開いたストリームの最初のCDSリクエストのversion_info。空ならempty)の3行を書いてください。

ホットリスタートは、新しいプロセスが古いプロセスからリスナーソケットを引き継ぐ方式です。そのため、ポートが一瞬も閉じず、古いプロセスは、ドレインの時間の間、処理中のリクエストを終わらせてから退きます。2つのプロセスは、--base-idでお互いを探すので、同じでなければならず、epochは1つずつ上げます。新しいプロセスは、古いプロセスのxDSの状態を引き継ぎません。コントロールプレーンに新しく接続して、最初から受け取ります。古いプロセスが退いたかは、pgrep -af 'restart-epoch 0'で見ます。

ストリーム上で見たことをまとめる

/root/envd-ads/08-report.mdに、次の5行を書き、その下に学んだことを4行以上書いてください。first_request_version=(Envoyが一番最初に送ったCDSリクエストのversion_info、空ならempty)、nack_version_info=、resume_cds_version=(ステップ5・6の記録)、epoch_after_restart=、inflight_code=(ステップ7の記録)です。

値は、ads.logと証拠のファイルから移してください。説明の行には、「NACKのversion_infoが、なぜ拒否したバージョンではないのか」と、「コントロールプレーンの障害とプロキシの再起動が、それぞれ何を失わせるのか」を、自分の言葉で書いておいてください。