この記事から得られる知識
この記事を読むと、以下を "完全に理解" できます✌️
記事のざっくりした内容は、以下のスライドからキャッチアップできちゃいます!
- この記事から得られる知識
- 01. はじめに
- 02. サービスディスカバリーについて
- 03. Istioのサービスディスカバリーの仕組み
- 04. istio-proxyコンテナ内のEnvoyの仕組み
- 05. おわりに
- 謝辞
- 記事関連のおすすめ書籍
01. はじめに
推し (Istio) が尊い🙏🙏🙏
3-shake Advent Calender 2022 最終日の記事です🎅
普段、私は 俺の技術ノート に知見を記録しており、はてなブログはデビュー戦となります。
最近の業務で、オンプレとAWS上のIstio⛵️をひたすら子守りしています。
今回は、子守りの前提知識の復習もかねて、Istioのサービス間通信を実現するサービスディスカバリーの仕組みを記事で解説しました。
Istioの機能の1つであるサービスディスカバリーは、その仕組みの多くをEnvoyに頼っているため、合わせてEnvoyの仕組みも説明します。
それでは、もりもり布教していきます😗
飛ばしていただいても大丈夫ですが、読んでもらえるとより理解が深まるはずです👍
02. サービスディスカバリーについて
マイクロサービスアーキテクチャにおけるサービスディスカバリー
サービスディスカバリーとは
平易な言葉で言い換えると サービス間通信 です。
マイクロサービスアーキテクチャでは、マイクロサービスからマイクロサービスにリクエストを送信する場面があります。
サービスディスカバリーとは、宛先マイクロサービスの宛先情報 (例:IPアドレス、完全修飾ドメイン名、など) を検出し、送信元マイクロサービスが宛先マイクロサービスにリクエストを継続的に送信可能にする仕組みのことです。
なぜサービスディスカバリーが必要なのか
そもそも、なぜサービスディスカバリーが必要なのでしょうか。
マイクロサービスアーキテクチャでは、システムの信頼性 (定められた条件下で定められた期間にわたり、障害を発生させることなく実行する程度) を担保するために、マイクロサービスのインスタンスの自動スケーリングを採用します。
この時、自動スケーリングのスケールアウトでマイクロサービスが増加するたびに、各インスタンスには新しい宛先情報が割り当てられてしまいます。
また、マイクロサービスが作り直された場合にも、宛先情報は更新されてしまいます。
このように、たとえインスタンスの宛先情報が更新されたとしても、インスタンスへのリクエストに失敗しない仕組みが必要です。
サービスディスカバリーの要素
サービスディスカバリーの仕組みは、次の要素からなります。
名前解決は、DNSベースのサービスディスカバリー (例:CoreDNS + Service + kube-proxyによるサービスディスカバリー) で必要となり、Istioでは使いません。
そのため、本記事では言及しないこととします🙇🏻
要素 | 責務 |
---|---|
送信元マイクロサービス | リクエストを送信する。 |
宛先マイクロサービス | リクエストを受信する。 |
サービスレジストリ | 宛先マイクロサービスの宛先情報を保管する。 |
ロードバランサー | 宛先マイクロサービスのインスタンスにロードバランシングする。 |
名前解決 | 宛先マイクロサービスへのリクエスト送信時に、名前解決可能にする。 |
サービスディスカバリーのパターン
サービスディスカバリーのパターンとは
サービスディスカバリーの仕組みにはいくつか種類があります。
Istioのサービスディスカバリーは、このうちのサーバーサイドパターンを実装したものになります。
サーバーサイドパターン
送信元マイクロサービスから、問い合わせとロードバランシングの責務が切り離されています。
送信元マイクロサービスは、ロードバランサーにリクエストを送信します。
ロードバランサーは、宛先マイクロサービスの宛先をサービスレジストリに問い合わせ、またリクエストをロードバランシングする責務を担っています💪🏻
(例) Istio、Linkerd、など
クライアントサイドパターン
通信の送信元マイクロサービスは、宛先マイクロサービスの宛先をサービスレジストリに問い合わせ、さらにロードバランシングする責務を担います。
(例) NetflixのEureka、など
03. Istioのサービスディスカバリーの仕組み
Istioが実装するサービスメッシュには、サイドカープロキシメッシュとアンビエントメッシュがあり、今回はサイドカープロキシメッシュのサービスディスカバリーを取り上げます。
Istioのサービスディスカバリーは、discovery
コンテナとistio-proxy
コンテナが軸となり、サーバーサイドパターンのサービスディスカバリーを実装します。
全体像
(1) 〜 (6) の全体像は、以下の通りです👇
istio-proxy
コンテナは、サービスレジストリへの問い合わせと、ロードバランシングする責務を担っていることに注目してください。
(1) kube-apiserverによる宛先情報保管
kube-apiserverは、Pod等の宛先情報をetcd等に保管します。
これは、Kubernetesの通常の仕組みです。
(2) discoveryコンテナによる宛先情報保管
discovery
コンテナは、kube-apiserverからPod等の宛先情報を取得し、自身に保管します。
(3) istio-proxyコンテナによる宛先情報取得
istio-proxy
コンテナは、discovery
コンテナからPod等の宛先情報を双方向ストリーミングRPCで取得します。
(4) istio-proxyコンテナによるリクエスト受信
送信元マイクロサービスがリクエストを送信します。
サーバーサイドパターンでの責務通り、送信元マイクロサービスはロードバランサー (ここではistio-proxy
コンテナ) にリクエストを送信します。
この時、送信元マイクロサービスがistio-proxy
コンテナに直接的にリクエストを送信しているというよりは、iptablesがistio-proxy
コンテナにリクエストをリダイレクトします。
istio-proxy
コンテナこれを受信します。
(5) istio-proxyコンテナによるロードバランシング
istio-proxy
コンテナは、リクエストをロードバランシングし、また宛先Podに送信します。
discoveryコンテナの仕組み
全体像の中から、discovery
コンテナを詳しく見てみましょう。
discovery
コンテナは、別名Istiodと呼ばれています。
XDS-APIというエンドポイントを公開しており、XDS-APIのうち、サービスディスカバリーに関係するAPIは以下の通りです。
今回は詳しく言及しませんが、istio-proxy
コンテナがHTTPSリクエストを処理するために、証明書を配布するためのSDS-APIもあります。
APIの種類 | 説明 |
---|---|
LDS-API | Envoyのリスナーを取得できる。 |
RDS-API | Envoyのルートを取得できる。 |
CDS-API | Envoyのクラスターを取得できる。 |
EDS-API | Envoyのエンドポイントできる。 |
ADS-API | 各XDS-APIから取得できる宛先情報を整理して取得できる。 |
(1) kube-apiserverによる宛先情報保管
kube-apiserverによる宛先情報保管 と同じです。
(2) discoveryコンテナによる宛先情報保管
discoveryコンテナによる宛先情報保管 と同じです。
(3) istio-proxyコンテナによる宛先情報取得
XDS-APIとistio-proxy
コンテナの間では、gRPCの双方向ストリーミングRPCの接続が確立されています。
そのため、istio-proxy
コンテナからのリクエストに応じて宛先情報を返却するだけでなく、リクエストがなくとも、XDS-APIからもistio-proxy
コンテナに対して宛先情報を送信します。
XDS-APIのエンドポイントがいくつかあり、各エンドポイントから宛先情報を取得できます。
一方で、各エンドポイントからバラバラに宛先情報を取得すると、Envoy上でこれを整理する時に、宛先情報のバージョンの不整合が起こる可能性があります。
そのため、Istioは実際にはADS-APIを使用して宛先情報を取得します。
istio-proxyコンテナの仕組み
全体像の中から、istio-proxy
コンテナを詳しく見てみましょう。
(1) kube-apiserverによる宛先情報保管
kube-apiserverによる宛先情報保管 と同じです。
(2) discoveryコンテナによる宛先情報保管
discoveryコンテナによる宛先情報保管 と同じです。
(3) istio-proxyコンテナによる宛先情報取得
istio-proxy
コンテナでは、pilot-agentとEnvoyが稼働しています。
先ほどistio-proxy
コンテナは、双方向ストリーミングRPCでADS-APIから宛先情報を取得すると説明しました。
厳密にはEnvoyが、pilot-agentを介して、ADS-APIから双方向ストリーミングRPCで宛先情報を取得します。
(4) istio-proxyコンテナによるリクエスト受信
istio-proxyコンテナによるリクエスト受信 と同じです。
(5) istio-proxyコンテナによるリクエスト受信
EnvoyはADS-APIから取得した宛先情報に基づいて、宛先マイクロサービスのインスタンスにロードバランシングします。
04. istio-proxyコンテナ内のEnvoyの仕組み
全体像
EnvoyがADS-APIから取得した宛先情報を見ていく前に、Envoyの処理の流れを解説します。
istio-proxy
コンテナ内のEnvoyでは、以下の仕組みでHTTPリクエストを処理します。
(1) 〜 (6) の全体像は、以下の通りです👇
(1) 送信元マイクロサービスからリクエスト受信
istio-proxy
コンテナは、送信元マイクロサービスからリクエストを受信します。
(2) Envoyによるリスナー選択
Envoyは、リクエストの宛先情報 (例:宛先IPアドレス、ポート番号、パス、ホスト、など) に応じてリスナーを選びます。
(3) Envoyによるルート選択
Envoyは、リスナーに紐づくルートを選びます。
(4) Envoyによるクラスター選択
(5) Envoyによるエンドポイント選択
Envoyは、クラスターに紐づくエンドポイントを選びます。
(6) 宛先マイクロサービスへのリクエスト送信
Envoyは、エンドポイントに対応するインスタンスにリクエストを送信します。
Envoyで確認した宛先情報を👆に当てはめて見ていくことにしましょう。
EnvoyがADS-APIから取得した宛先情報を見てみよう
config_dumpエンドポイント
実際にEnvoyに登録されている宛先情報は、istio-proxy
コンテナ自体のlocalhost:15000/config_dump
からJSON形式で取得できます。
もしお手元にIstioがある場合は、Envoyにどんな宛先情報が登録されているか、Envoyを冒険してみてください。
$ kubectl exec \ -it foo-pod \ -n foo-namespace \ -c istio-proxy \ -- bash -c "curl http://localhost:15000/config_dump" | yq -P
リスナー
▼ 確認方法
istio-proxy
コンテナがADS-APIから取得したリスナーは、/config_dump?resource={dynamic_listeners}
から確認できます。
ここでは、foo-pod内でbar-podのリスナーを確認したと仮定します。
$ kubectl exec \ -it foo-pod \ -n foo-namespace \ -c istio-proxy \ -- bash -c "curl http://localhost:15000/config_dump?resource={dynamic_listeners}" | yq -P
▼ 結果
以下を確認できました。
- 宛先IPアドレスや宛先ポート番号に応じてリスナーを選べるようになっており、ここでは
<任意のIPアドレス>:50002
。 - リスナーに紐づくルートの名前
configs: - "@type": type.googleapis.com/envoy.admin.v3.ListenersConfigDump.DynamicListener # リスナー名 name: 0.0.0.0_50002 active_state: version_info: 2022-11-24T12:13:05Z/468 listener: "@type": type.googleapis.com/envoy.config.listener.v3.Listener name: 0.0.0.0_50002 address: socket_address: # 受信したパケットのうちで、宛先IPアドレスでフィルタリング address: 0.0.0.0 # 受信したパケットのうちで、宛先ポート番号でフィルタリング port_value: 50002 filter_chains: - filter_chain_match: transport_protocol: raw_buffer application_protocols: - http/1.1 - h2c filters: - name: envoy.filters.network.http_connection_manager typed_config: "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager stat_prefix: outbound_0.0.0.0_50001 rds: config_source: ads: {} initial_fetch_timeout: 0s resource_api_version: V3 # 本リスナーに紐づくルートの名前 route_config_name: 50002 ... - "@type": type.googleapis.com/envoy.admin.v3.ListenersConfigDump.DynamicListener ...
ルート
▼ 確認方法
istio-proxy
コンテナがADS-APIから取得したリスナーは、/config_dump?resource={dynamic_route_configs}
から確認できます。
ここでは、foo-pod内でbar-podのルートを確認したと仮定します。
$ kubectl exec \ -it foo-pod \ -n foo-namespace \ -c istio-proxy \ -- bash -c "curl http://localhost:15000/config_dump?resource={dynamic_route_configs}" | yq -P
▼ 結果
コマンドを実行するとYAMLを取得でき、以下を確認できました。
configs: - "@type": type.googleapis.com/envoy.admin.v3.RoutesConfigDump.DynamicRouteConfig version_info: 2022-11-24T12:13:05Z/468 route_config: "@type": type.googleapis.com/envoy.config.route.v3.RouteConfiguration # ルートの名前 name: 50002 virtual_hosts: - name: bar-service.bar-namespace.svc.cluster.local:50002 # ホストベースルーティング domains: - bar-service.bar-namespace.svc.cluster.local - bar-service.bar-namespace.svc.cluster.local:50002 - bar-service - bar-service:50002 - bar-service.bar-namespace.svc - bar-service.bar-namespace.svc:50002 - bar-service.bar-namespace - bar-service.bar-namespace:50002 - 172.16.0.2 - 172.16.0.2:50002 routes: - match: # パスベースルーティング prefix: / route: # 本ルートに紐づくクラスターの名前 cluster: outbound|50002|v1|bar-service.bar-namespace.svc.cluster.local timeout: 0s retry_policy: retry_on: connect-failure,refused-stream,unavailable,cancelled,retriable-status-codes num_retries: 2 retry_host_predicate: - name: envoy.retry_host_predicates.previous_hosts host_selection_retry_max_attempts: "5" retriable_status_codes: - 503 max_stream_duration: max_stream_duration: 0s grpc_timeout_header_max: 0s decorator: operation: bar-service.bar-namespace.svc.cluster.local:50002/* ... - '@type': type.googleapis.com/envoy.admin.v3.RoutesConfigDump.DynamicRouteConfig ...
クラスター
▼ 確認方法
istio-proxy
コンテナがADS-APIから取得したクラスターは、/config_dump?resource={dynamic_active_clusters}
から確認できます。
ここでは、foo-pod内でbar-podのクラスターを確認したと仮定します。
$ kubectl exec \ -it foo-pod \ -n foo-namespace \ -c istio-proxy \ -- bash -c "curl http://localhost:15000/config_dump?resource={dynamic_active_clusters}" | yq -P
▼ 結果
コマンドを実行するとYAMLを取得でき、以下を確認できました。
configs: - "@type": type.googleapis.com/envoy.admin.v3.ClustersConfigDump.DynamicCluster version_info: 2022-11-24T12:13:05Z/468 cluster: "@type": type.googleapis.com/envoy.config.cluster.v3.Cluster # クラスターの名前 name: outbound|50002|v1|bar-service.bar-namespace.svc.cluster.local type: EDS eds_cluster_config: eds_config: ads: {} initial_fetch_timeout: 0s resource_api_version: V3 # 本クラスターに紐づくエンドポイントの親名 service_name: outbound|50002|v1|bar-service.bar-namespace.svc.cluster.local ... - "@type": type.googleapis.com/envoy.admin.v3.ClustersConfigDump.DynamicCluster ...
エンドポイント
▼ 確認方法
istio-proxy
コンテナがADS-APIから取得したクラスターは、/config_dump?include_eds
から確認できます。
ここでは、foo-pod内でbar-podのクラスターを確認したと仮定します。
$ kubectl exec \ -it foo-pod \ -n foo-namespace \ -c istio-proxy \ -- bash -c "curl http://localhost:15000/config_dump?include_eds" | yq -P
▼ 結果
コマンドを実行するとYAMLを取得でき、以下を確認できました。
configs: dynamic_endpoint_configs: - endpoint_config: "@type": type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment # エンドポイントの親名 cluster_name: outbound|50002|v1|bar-service.bar-namespace.svc.cluster.local endpoints: - locality: region: ap-northeast-1 zone: ap-northeast-1a lb_endpoints: - endpoint: address: socket_address: # 冗長化されたbar-podのIPアドレス address: 11.0.0.1 # bar-pod内のコンテナが待ち受けているポート番号 port_value: 50002 health_check_config: {} health_status: HEALTHY metadata: filter_metadata: istio: workload: bar envoy.transport_socket_match: tlsMode: istio # ロードバランシングアルゴリズムを決める数値 load_balancing_weight: 1 - locality: region: ap-northeast-1 zone: ap-northeast-1d lb_endpoints: - endpoint: address: socket_address: # 冗長化されたbar-podのIPアドレス address: 11.0.0.2 # bar-pod内のコンテナが待ち受けているポート番号 port_value: 50002 health_check_config: {} health_status: HEALTHY metadata: filter_metadata: istio: workload: bar envoy.transport_socket_match: tlsMode: istio # ロードバランシングアルゴリズムを決める数値 load_balancing_weight: 1 - locality: region: ap-northeast-1 zone: ap-northeast-1d lb_endpoints: - endpoint: address: socket_address: # 冗長化されたbar-podのIPアドレス address: 11.0.0.3 # bar-pod内のコンテナが待ち受けているポート番号 port_value: 50002 health_check_config: {} health_status: HEALTHY metadata: filter_metadata: istio: workload: bar envoy.transport_socket_match: tlsMode: istio # ロードバランシングアルゴリズムを決める数値 load_balancing_weight: 1 policy: overprovisioning_factor: 140 ... - endpoint_config: ...
Envoyの処理の流れのまとめ
確認できた宛先情報を、Envoyの処理の流れに当てはめてみました。
(1) 送信元マイクロサービスからリクエスト受信
送信元マイクロサービスは、宛先マイクロサービス (<任意のIP>/:50002
) にリクエストを送信します。
サイドカーコンテナのistio-proxy
コンテナはこれを受信します。
(2) Envoyによるリスナー選択
Envoyは、リクエストの宛先 (IPアドレス、ポート番号、パス) からPodのリスナー (0.0.0.0_50002
) を選びます。
(3) Envoyによるルート選択
Envoyは、リスナーに紐づくPodのルート (50002
) を選びます。
(4) Envoyによるクラスター選択
Envoyは、クラスターに紐づくPodのクラスター (outbound|50002|v1|bar-service.bar-namespace.svc.cluster.local
) を選びます。
(5) Envoyによるクラスター選択
Envoyは、クラスターに紐づくPodのインスタンスのエンドポイント (11.0.0.X/:50002
) を選びます。
(6) 宛先マイクロサービスへのリクエスト送信
Envoyは、エンドポイントの宛先にPodのリクエストを送信します。
サービスディスカバリーの冒険は以上です⛵
05. おわりに
Istioの機能の1つである『サービスディスカバリー』の仕組みを、Envoyを交えながらもりもり布教しました。
愛が溢れてしまいました。
Istioの機能を1つとっても、複雑な仕組みで実現していることがお分かりいただけたかと思います。
Istioありがとう🙏🙏🙏
謝辞
3-shake SRE Tech Talk での発表前後に、以下の方々に発表内容について助言をいただきました。
@ido_kara_deru
さん@yosshi_
さん@yteraoka
さん
(アルファベット順)
また、今回の 3-shake Advent Calender 2022 は、以下の方々に企画いただきました。
@jigyakkuma_
さん@nwiizo
さん
(アルファベット順)
皆様に感謝申し上げます🙇🏻