準備チェックは通るのに 500 を返す Pod
目標
常に500を返すPodが混じったサービスで、外れ値検出によってそのPodが実際に排除されることを、レスポンスの数とEnvoyのエンドポイントの状態で確認し、ミラーリングと障害注入を加えて、それらの設定がEnvoyの何になるかを見ます。
なぜ重要なのか
Kubernetesのreadinessプローブは、Podが自分で報告する状態を見ます。リクエストごとに500を返しながらreadinessプローブは通るPodは、エンドポイントに残り続けます。外れ値検出は、実際に受け取ったレスポンスで判断するので、そのPodを外せますが、その分、故障を隠してしまうこともあります。排除を目で確認し、数字で数える方法を知って初めて、この機能を信頼して使えます。
ステップ
kubectl apply -f /opt/fixtures/istlab/resilience-app.yamlで材料を載せ、Podがすべて準備できるまで待ってください。その後、clientからhttp://api/を30回呼び、/root/istlab-resilience/01-baseline.txtにok=(200の数)とerr=(500の数)を書いてください。/root/istlab-resilience/client.yamlにclientのPodを書き直し、アノテーションproxy.istio.io/configにproxyStatsMatcher.inclusionRegexps: [".*outlier_detection.*"]を入れて、既存のclientを削除した後、このファイルで作り直してください。/root/istlab-resilience/dr.yamlにDestinationRuleのapi(ホストapi.pay.svc.cluster.local)を書いて適用してください。outlierDetectionで、連続する5xxが2回(consecutive5xxErrors: 2)、検査周期2秒、基本の排除時間60秒、最大排除割合50%にします。その後、30回ずつ2回呼び、/root/istlab-resilience/03-outlier.txtにerr_first=(最初の30回の500の数)とerr_second=(2回目の30回の500の数)を書いてください。- 排除がかかっている間(ステップ3の後60秒以内)に、
istioctl proxy-config endpoints client.pay --cluster 'outbound|80||api.pay.svc.cluster.local' -o jsonの出力を/root/istlab-resilience/04-endpoints.jsonに保存してください。60秒が過ぎていたら、ステップ3のように30回呼んで再び排除させてから保存します。 /root/istlab-resilience/vs.yamlにVirtualServiceのapi(ホストapi.pay.svc.cluster.local)を書いて適用してください。宛先は今までどおりapi.pay.svc.cluster.local、mirrorはapi-v2.pay.svc.cluster.local、mirrorPercentageは100です。適用後、x-request-idをmirror-1・mirror-2・mirror-3として3回呼び、/root/istlab-resilience/05-mirror.txtにmirrored=(api-v2のサイドカーのログに残ったその3つのIDの数)とclient_saw_v2=(clientが受け取った本文の中にv2があればyes)の2行を書いてください。/root/istlab-resilience/vs.yamlのVirtualServiceの前のほうに、ルールを2つ追加してください。ヘッダーx-chaos: abortならfault.abortで503を100%、x-chaos: delayならfault.delayで2秒を100%入れ、宛先は今までどおりです(ミラーを付けた基本ルールは最後に置きます)。適用後、/root/istlab-resilience/06-fault.txtに、abort_code=(abortのリクエストのステータスコード)、abort_flag=(そのリクエストのclientのサイドカーのアクセスログのレスポンスフラグ)、delay_seconds=(delayのリクエストにかかった時間の、小数点以下を切り捨てた秒数)の3行を書いてください。- clientのサイドカーが受け取った設定から2つを取り出し、
/root/istlab-resilience/07-envoy.jsonにJSONオブジェクトとして保存してください。outlierは、istioctl proxy-config cluster client.pay --fqdn api.pay.svc.cluster.local -o jsonの最初のクラスターのoutlierDetectionオブジェクトをそのまま、mirror_clusterは、istioctl proxy-config routes client.pay --name 80 -o jsonでapi.pay.svc.cluster.localの仮想ホストの最後のルートが持つrequestMirrorPolicies[0].clusterの値です。 /root/istlab-resilience/08-report.mdに5行を書き、その下に学んだことを4行以上書いてください。5行は、errors_before=(ステップ1のerr)、errors_after_ejection=(ステップ3のerr_second)、ejected_ip=(ステップ4のファイルでfailedOutlierCheckがtrueのアドレス)、mirror_reached_client=(ステップ5のclient_saw_v2)、abort_flag=(ステップ6)です。
参考
- このVMは準備に2–4分かかります。clientでは、
kubectl -n pay exec client -c curl -- …でコマンドを打ちます。 - サイドカーの統計は、
kubectl -n pay exec client -c istio-proxy -- pilot-agent request GET statsで見ます。 - 排除は60秒後に解けます。ステップ4で排除が見えなければ、30回呼び直して排除させてから保存してください。
- ステップ2でclientを作り直すと、そのPodのサイドカーのログは新しく始まります。
- よくある失敗: 障害注入のルールを、ミラーを付けた基本ルールの後ろに置くケースです。上から最初に一致したルールが使われるので、ヘッダー条件のルールが永遠にかかりません。
故障したPodが1つ混じったサービス
kubectl apply -f /opt/fixtures/istlab/resilience-app.yamlで材料を載せ、Podがすべて準備できるまで待ってください。その後、clientからhttp://api/を30回呼び、/root/istlab-resilience/01-baseline.txtにok=(200の数)とerr=(500の数)を書いてください。
apiサービスの後ろにはPodが3つあり、そのうちapi-badは常に500を返します。readinessプローブがないので、Kubernetesは3つともReadyとみなしてエンドポイントに入れます。Kubernetesの目には正常なPodです。そのため、リクエストのおよそ3分の1が失敗します。Podの中のシェルで繰り返すには、sh -c 'for i in $(seq 1 30); do …; done'を使ってください。
見えなかった統計を有効にする
/root/istlab-resilience/client.yamlにclientのPodを書き直し、アノテーションproxy.istio.io/configにproxyStatsMatcher.inclusionRegexps: [".*outlier_detection.*"]を入れて、既存のclientを削除した後、このファイルで作り直してください。
Istioは、サイドカーが出すEnvoyの統計の大半を、デフォルトでフィルタして捨てます。Pod数千個がすべて出力したら、Prometheusが処理しきれないからです。そのため、外れ値検出を有効にしても、排除の回数のような数字が見えません。proxyStatsMatcherは、Podのアノテーションで必要な統計だけを復活させる仕組みで、プロキシが起動するときに読み込むので、Podを作り直す必要があります。うまく入ったかは、プロキシのブートストラップのstats_configで見られます。
外れ値検出で故障したPodを外す
/root/istlab-resilience/dr.yamlにDestinationRuleのapi(ホストapi.pay.svc.cluster.local)を書いて適用してください。outlierDetectionで、連続する5xxが2回(consecutive5xxErrors: 2)、検査周期2秒、基本の排除時間60秒、最大排除割合50%にします。その後、30回ずつ2回呼び、/root/istlab-resilience/03-outlier.txtにerr_first=(最初の30回の500の数)とerr_second=(2回目の30回の500の数)を書いてください。
外れ値検出は、サイドカーが実際に受け取ったレスポンスで、アップストリームを1つずつ判断します。同じPodが続けて5xxを返すと、そのPodをロードバランシングの対象からしばらく外します(排除)。最初の回では排除される前の数回の500が見え、2回目は0になるはずです。排除された回数は、ステップ2で復活させた統計…outlier_detection.ejections_enforced_totalで数えます。kubectl -n pay exec client -c istio-proxy -- pilot-agent request GET stats | grep outlierです。
Envoyはそのpodをどう記録したか
排除がかかっている間(ステップ3の後60秒以内)に、istioctl proxy-config endpoints client.pay --cluster 'outbound|80||api.pay.svc.cluster.local' -o jsonの出力を/root/istlab-resilience/04-endpoints.jsonに保存してください。60秒が過ぎていたら、ステップ3のように30回呼んで再び排除させてから保存します。
排除されたPodは、Kubernetesのエンドポイントからは外れません。このサイドカーのロードバランシングテーブルでだけ外れます。そのためkubectl get endpointsには3つがそのまま残り、Envoyのエンドポイント一覧で、そのPodのhealthStatusにfailedOutlierCheck: trueが付きます。表の形で見ると、OUTLIER CHECKの欄がFAILEDです。排除は永久ではなく、基本の排除時間の後に再び入り、また失敗すればより長く外れます。
本番トラフィックを新バージョンに映してみる: ミラーリング
/root/istlab-resilience/vs.yamlにVirtualServiceのapi(ホストapi.pay.svc.cluster.local)を書いて適用してください。宛先は今までどおりapi.pay.svc.cluster.local、mirrorはapi-v2.pay.svc.cluster.local、mirrorPercentageは100です。適用後、x-request-idをmirror-1・mirror-2・mirror-3として3回呼び、/root/istlab-resilience/05-mirror.txtにmirrored=(api-v2のサイドカーのログに残ったその3つのIDの数)とclient_saw_v2=(clientが受け取った本文の中にv2があればyes)の2行を書いてください。
ミラーリングは、リクエストを複製して片方へ送り、そのレスポンスは捨てます。そのため、新しいバージョンを本番のトラフィックで試しながら、ユーザーには影響を与えません。複製は元のリクエストと同じx-request-idを持つので、ミラー先のサイドカーのログでIDによって探せます。kubectl -n pay logs deploy/api-v2 -c istio-proxyです。複製は非同期で行くので、ログに少し遅れて出力されることがあります。
障害をわざと入れる: ヘッダーがあるときだけ
/root/istlab-resilience/vs.yamlのVirtualServiceの前のほうに、ルールを2つ追加してください。ヘッダーx-chaos: abortならfault.abortで503を100%、x-chaos: delayならfault.delayで2秒を100%入れ、宛先は今までどおりです(ミラーを付けた基本ルールは最後に置きます)。適用後、/root/istlab-resilience/06-fault.txtに、abort_code=(abortのリクエストのステータスコード)、abort_flag=(そのリクエストのclientのサイドカーのアクセスログのレスポンスフラグ)、delay_seconds=(delayのリクエストにかかった時間の、小数点以下を切り捨てた秒数)の3行を書いてください。
障害注入は、クライアント側のサイドカーがリクエストをアップストリームに送る前に行います。そのためabortはアップストリームに届かず、レスポンスフラグがFI(fault injected)になります。ヘッダー条件を付けておけば、普段のトラフィックはそのままにして、試すリクエストだけを壊せます。VirtualServiceのhttpルールは、上から最初に一致したものが使われるので、包括的なルールは最後に置きます。かかった時間はcurl -w '%{time_total}'で測ります。
DestinationRuleとVirtualServiceはEnvoyの何になったか
clientのサイドカーが受け取った設定から2つを取り出し、/root/istlab-resilience/07-envoy.jsonにJSONオブジェクトとして保存してください。outlierは、istioctl proxy-config cluster client.pay --fqdn api.pay.svc.cluster.local -o jsonの最初のクラスターのoutlierDetectionオブジェクトをそのまま、mirror_clusterは、istioctl proxy-config routes client.pay --name 80 -o jsonでapi.pay.svc.cluster.localの仮想ホストの最後のルートが持つrequestMirrorPolicies[0].clusterの値です。
istiodは、DestinationRuleのconsecutive5xxErrors・interval・baseEjectionTime・maxEjectionPercentをEnvoyクラスターのoutlierDetectionに、VirtualServiceのmirrorをルートのrequestMirrorPoliciesに移します。名前が少しずつ違い(consecutive5xx)、時間は"2s"のような文字列に変わります。設定を変えたのに動作がおかしければ、結局はこの変換結果を見ることになります。
メッシュが何を代わりにしてくれたかを書く
/root/istlab-resilience/08-report.mdに5行を書き、その下に学んだことを4行以上書いてください。5行は、errors_before=(ステップ1のerr)、errors_after_ejection=(ステップ3のerr_second)、ejected_ip=(ステップ4のファイルでfailedOutlierCheckがtrueのアドレス)、mirror_reached_client=(ステップ5のclient_saw_v2)、abort_flag=(ステップ6)です。
値は前のステップのファイルから写してください。説明の行には、「Kubernetesのreadinessプローブとメッシュの外れ値検出が、それぞれ何を見て判断するか」を書いておいてください。片方はPodが報告する状態を、もう片方は実際に受け取ったレスポンスを見ます。