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

Istio深化 — なぜそう流れるのか

VirtualService をルート表に変換し、順序と再試行を確かめる

TT Labで続きを見る

目標

VirtualServiceがEnvoyのルート表に変換される規則に従って、手で表を立て、Hostヘッダー・最初のマッチ優先・重み・リトライを、リクエストで確認します。

なぜ重要なのか

VirtualServiceのミスは、たいてい、ルール1つではなく、ルールの間の順序と重なりから出ます。静的解析はそれを捕まえられないので、変換規則を知って、Envoyでどう動くかを描けないと、変更のレビューで弾けません。

ステップ

  1. /root/ist2-route/dr.yaml(DestinationRule reviews、host reviews.default.svc.cluster.local、サブセットv1・v2・broken。ラベルversionがそれぞれ同じ名前)と、/root/ist2-route/vs.yaml(VirtualService reviews、hosts [reviews])を書いてください。vs.yamlのhttpは、この順序で4つです。jason(ヘッダーend-userがちょうどjasonならv2)、api(パスのプレフィックスが/apiならv1)、flaky(パスのプレフィックスが/flakyならbroken、retries: {attempts: 3, retryOn: 5xx}、timeout: 2s)、default(v1にweight75、v2に25)です。istioctl analyze --use-kube=false vs.yaml dr.yamlの出力と終了コードを、/root/ist2-route/01-analyze.txtに入れてください(最後の行はrc=0)。
  2. サービスreviewsは、ネームスペースdefault、ポート9080です。このVirtualServiceがサイドカーで作るルート表の名前を、/root/ist2-route/02-names.txtに3行で書いてください。route_config=(ルート表の名前)、virtual_host=(バーチャルホストの名前)、domains=(そのバーチャルホストのdomainsのうち、短い名前の4種類をカンマ区切りで、順序は問いません)です。
  3. /root/ist2-route/route.yamlにEnvoyの設定を書いてください。管理ポートは9984、リスナーは127.0.0.1:10084、ステップ2のルート表の名前・バーチャルホストの名前・domains4つを使います。ルートは、名前を付けて、この順序で3つです。jason(パス/、かつヘッダーend-userがちょうどjason → outbound|9080|v2|reviews.default.svc.cluster.local)、api(プレフィックス/api → outbound|9080|v1|reviews.default.svc.cluster.local)、default(プレフィックス/ → outbound|9080|v1|reviews.default.svc.cluster.local)です。クラスターは、v1(127.0.0.1:8105)とv2(127.0.0.1:8106)の2つです。アップストリーム2つをokとして起動し、Envoyを起動してから、3種類のHostヘッダーで/を呼び、/root/ist2-route/03-hosts.txtに、reviews=、reviews.default.svc.cluster.local=、ratings=の3行でHTTPコードを書いてください。
  4. route.yamlで起動しているEnvoyに、Hostreviewsで3つのリクエストを送り、応答本文のポートでサブセットを見分けて、/root/ist2-route/04-match.txtに書いてください。anonymous=(ヘッダーなしで/)、jason=(end-user: jasonで/)、api_as_jason=(end-user: jasonで/api/list)です。値はv1かv2です。
  5. vs.yamlからdefaultルールを一番前に移した/root/ist2-route/vs-bad.yamlを作り、istioctl analyze --use-kube=false vs-bad.yaml dr.yamlの終了コードを確認してください。同じミスをEnvoyで再現します。route.yamlからdefaultルートを一番前に移した/root/ist2-route/route-bad.yamlでEnvoyを起動し直し、end-user: jasonで/を呼んでみてください。/root/ist2-route/05-order.txtに、analyze_rc=(vs-badの解析の終了コード)、jason_after=(route-badでjasonが行ったサブセット)の2行を書いてください。確認した後は、route.yamlに戻して起動しておいてください。
  6. route.yamlを/root/ist2-route/route-split.yamlにコピーして、defaultルートだけをweighted_clustersに変えてください(v1が75、v2が25。VirtualServiceのdefaultルールそのまま)。--concurrency 1で起動し直してから、Hostreviewsで/をちょうど40回送り、/root/ist2-route/06-split.txtにv1=、v2=、total=の3行を書いてください。
  7. route-split.yamlを/root/ist2-route/route-retry.yamlにコピーして、2つを追加してください。クラスターoutbound|9080|broken|reviews.default.svc.cluster.local(127.0.0.1:8114、常に503を返すアップストリーム)と、defaultの前にflakyルート(プレフィックス/flaky → そのクラスター、timeout: 2s、retry_policy: {retry_on: 5xx, num_retries: 3})です。アップストリームを8114でfailとして起動し、Envoyを起動し直してから、/flakyを1回だけ呼んでください。/root/ist2-route/07-retry.txtに、status=(HTTPコード)、upstream_rq_retry=、upstream_rq_total=(brokenクラスターの2つの統計の値)を書いてください。
  8. /root/ist2-route/08-report.mdに、route_config=、virtual_host=、first_match_wins=(ステップ5で見たとおり、最初に一致するルート1つだけが使われるならyes)、retry_attempts=(VirtualServiceのattemptsの値)の4行を書き、その下に、- で始まる説明を4行以上書いてください。

参考

ルートが4つのVirtualServiceを書いて、解析を通す

/root/ist2-route/dr.yaml(DestinationRule reviews、host reviews.default.svc.cluster.local、サブセットv1・v2・broken。ラベルversionがそれぞれ同じ名前)と、/root/ist2-route/vs.yaml(VirtualService reviews、hosts [reviews])を書いてください。vs.yamlのhttpは、この順序で4つです。jason(ヘッダーend-userがちょうどjasonならv2)、api(パスのプレフィックスが/apiならv1)、flaky(パスのプレフィックスが/flakyならbroken、retries: {attempts: 3, retryOn: 5xx}、timeout: 2s)、default(v1にweight75、v2に25)です。istioctl analyze --use-kube=false vs.yaml dr.yamlの出力と終了コードを、/root/ist2-route/01-analyze.txtに入れてください(最後の行はrc=0)。

VirtualServiceは「どこへ送るか」、DestinationRuleは「送った後でどう扱うかと、サブセットの定義」です。analyzeは、2つのファイルを一緒に読んで、VSが指すサブセットがDRにあるかのような参照関係を見ます。httpの項目ごとにnameを付けておくと、Envoy側のルートにも同じ名前が付き、後で探しやすくなります。

ルート表・バーチャルホスト・ドメインの名前を導出する

サービスreviewsは、ネームスペースdefault、ポート9080です。このVirtualServiceがサイドカーで作るルート表の名前を、/root/ist2-route/02-names.txtに3行で書いてください。route_config=(ルート表の名前)、virtual_host=(バーチャルホストの名前)、domains=(そのバーチャルホストのdomainsのうち、短い名前の4種類をカンマ区切りで、順序は問いません)です。

出ていく側で、サイドカーはポートごとにルート表を1つ置きます。同じポート9080を使うサービスが複数あれば、1つの表の中に、バーチャルホストがサービスごとに1つずつ入り、リクエストのHostヘッダーで選びます。そのため、表の名前はポート、バーチャルホストの名前はFQDN:포트(プレースホルダーはポートです)です。同じネームスペースのアプリは、reviews、reviews.defaultのように短くも呼ぶので、domainsにはFQDNを後ろから切り落とした名前が一緒に入ります(公式ドキュメントのproxy-configの例を参照)。

その名前でルート表を立て、Hostヘッダーで選ばれるのを見る

/root/ist2-route/route.yamlにEnvoyの設定を書いてください。管理ポートは9984、リスナーは127.0.0.1:10084、ステップ2のルート表の名前・バーチャルホストの名前・domains4つを使います。ルートは、名前を付けて、この順序で3つです。jason(パス/、かつヘッダーend-userがちょうどjason → outbound|9080|v2|reviews.default.svc.cluster.local)、api(プレフィックス/api → outbound|9080|v1|reviews.default.svc.cluster.local)、default(プレフィックス/ → outbound|9080|v1|reviews.default.svc.cluster.local)です。クラスターは、v1(127.0.0.1:8105)とv2(127.0.0.1:8106)の2つです。アップストリーム2つをokとして起動し、Envoyを起動してから、3種類のHostヘッダーで/を呼び、/root/ist2-route/03-hosts.txtに、reviews=、reviews.default.svc.cluster.local=、ratings=の3行でHTTPコードを書いてください。

ルート表を選ぶのはリスナー(ポート)で、表の中でバーチャルホストを選ぶのはHostヘッダーです。どのバーチャルホストのdomainsにもないHostで来ると、ルートが見つからず404になります。サイドカーが知らないサービス名を呼んだときの姿です。curl -H 'Host: reviews' localhost:10084/のように、ヘッダーを変えながら呼んでください。ヘッダーマッチは、match.headersにstring_match: {exact: …}で書きます。

ヘッダーマッチとパスマッチが重なるとき、どちらが勝つか

route.yamlで起動しているEnvoyに、Hostreviewsで3つのリクエストを送り、応答本文のポートでサブセットを見分けて、/root/ist2-route/04-match.txtに書いてください。anonymous=(ヘッダーなしで/)、jason=(end-user: jasonで/)、api_as_jason=(end-user: jasonで/api/list)です。値はv1かv2です。

Envoyは、ルートを上から見て、最初に一致する1つだけを使います。api_as_jasonは、jasonのルールにもapiのルールにも一致するので、どちらが先に書かれているかが、答えを決めます。本文がok:8105ならv1、ok:8106ならv2です。

catch-allを前に置くと、解析は静かで、トラフィックは間違う

vs.yamlからdefaultルールを一番前に移した/root/ist2-route/vs-bad.yamlを作り、istioctl analyze --use-kube=false vs-bad.yaml dr.yamlの終了コードを確認してください。同じミスをEnvoyで再現します。route.yamlからdefaultルートを一番前に移した/root/ist2-route/route-bad.yamlでEnvoyを起動し直し、end-user: jasonで/を呼んでみてください。/root/ist2-route/05-order.txtに、analyze_rc=(vs-badの解析の終了コード)、jason_after=(route-badでjasonが行ったサブセット)の2行を書いてください。確認した後は、route.yamlに戻して起動しておいてください。

defaultには条件がないので、すべてのリクエストに一致します。それが一番前にあると、後ろのルールは決して使われません。ところが、analyzeは、各ルールが参照するものが存在するかを見るだけで、ルール同士の覆い隠しは見ないので、きれいだと言います。そのため、VirtualServiceを直すときは、「広いものは後ろへ」を、人が守る必要があります。yq '.spec.http |= [.[3], .[0], .[1], .[2]]'のように、リストの順序を変えられます。

weightはweighted_clustersになる — 40回数える

route.yamlを/root/ist2-route/route-split.yamlにコピーして、defaultルートだけをweighted_clustersに変えてください(v1が75、v2が25。VirtualServiceのdefaultルールそのまま)。--concurrency 1で起動し直してから、Hostreviewsで/をちょうど40回送り、/root/ist2-route/06-split.txtにv1=、v2=、total=の3行を書いてください。

重みは、リクエストごとにサイコロを振るものなので、40回でちょうど30対10にはなりません。合計が40で、v1のほうが多ければ、正しい結果です。Istioでカナリアの比率を1%に設定して、リクエスト数十個で確認すると、v2が一度も出ないことがあるのも、同じ理由です。

retriesとtimeoutがretry_policyになる — 何回叩かれるか数える

route-split.yamlを/root/ist2-route/route-retry.yamlにコピーして、2つを追加してください。クラスターoutbound|9080|broken|reviews.default.svc.cluster.local(127.0.0.1:8114、常に503を返すアップストリーム)と、defaultの前にflakyルート(プレフィックス/flaky → そのクラスター、timeout: 2s、retry_policy: {retry_on: 5xx, num_retries: 3})です。アップストリームを8114でfailとして起動し、Envoyを起動し直してから、/flakyを1回だけ呼んでください。/root/ist2-route/07-retry.txtに、status=(HTTPコード)、upstream_rq_retry=、upstream_rq_total=(brokenクラスターの2つの統計の値)を書いてください。

VirtualServiceのretries.attemptsは、Envoyのnum_retriesに、retryOnはretry_onに、timeoutはルートのtimeoutになります。元のリクエスト1つに、リトライが最大attempts回付くので、アップストリームは1+attempts回叩かれます。障害の起きたサービスに、リトライが負荷を何倍にも載せる理由です。統計は累積なので、起動し直した直後に1回だけ呼ぶと、数字がきれいになります。/stats?filter=で、brokenクラスターの2つの値を選んでください。

VirtualServiceの変換表にまとめる

/root/ist2-route/08-report.mdに、route_config=、virtual_host=、first_match_wins=(ステップ5で見たとおり、最初に一致するルート1つだけが使われるならyes)、retry_attempts=(VirtualServiceのattemptsの値)の4行を書き、その下に、- で始まる説明を4行以上書いてください。

値は前のステップのファイルから移してください。説明の行に、「VirtualServiceを直すとき、何を確認するか」を書いておくと、この表が、次の変更レビューのときに役立ちます。