スナップショット一つ、ストリーム一本
目標
スナップショットファイルを読んでCDS・LDSを押し込む最小のADSサーバーを自分で書き、そのサーバーからすべての設定を受け取るEnvoyで、ACK・NACK・再接続・ホットリスタートを、ストリーム上で観察します。
なぜ重要なのか
istiodとサイドカーの間で起きていることが、これです。ところがメッシュでは、そのやり取りが見えないので、「設定を変えたのに、一部だけが古い動作」のような症状に出会ったとき、どこから見ればよいかわかりません。自分で書いたサーバーのログで、nonce・ACK・NACK・version_infoを一度読んでみれば、proxy-statusのSYNCEDと、istiodの拒否ログが何を指すのかがわかります。
ステップ
- アップストリームを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の形そのままで書きます。 /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を残すことです。/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で起動してください。- スナップショットをバージョン
"2"に変えてください。poolから8097を外し、/markerの本文をsnapshot=2にします。ファイルは、新しく書いてからmvで差し替えます。Envoyが受け入れた後で、/root/envd-ads/04-push.txtにmarker=(/markerの応答本文)とendpoints=(/clustersに残ったpoolのエンドポイント数)の2行を書いてください。 - スナップショットをバージョン
"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=に書きます。 - 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=として、同じファイルに追記します。 /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行を書いてください。/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サーバーは、
/opt/xds/bin/python3で実行します(grpcioとEnvoy APIのprotobufが入った仮想環境)。 - スナップショットは、新しいファイルに書いてから
mvで差し替えてください。サーバーが書きかけのファイルを読むと、JSONエラーで飛ばします(サーバーのログにsnapshot_errorが残ります)。 - Envoyは、ステップ3で
--restart-epoch 0で起動し、ステップ7の前までは起動し直さないでください。起動し直すと、ステップ7のepochがずれます。止めるにはcurl -X POST localhost:9988/quitquitquitです。 - ads.logは、JSONが1行ずつです。
jq -c 'select(.event=="nack")' ads.logのように、選んで見てください。 - よくある間違い:
pkill -f ads_server.pyと書いてしまうことです。その文字列を含むシェルも一緒に終了します。'[a]ds_server.py'と書いてください。
押し込む設定を、スナップショットとして書く
アップストリームを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が、なぜ拒否したバージョンではないのか」と、「コントロールプレーンの障害とプロキシの再起動が、それぞれ何を失わせるのか」を、自分の言葉で書いておいてください。