好きな技術を布教したい 😗

〜 沼のまわりに餌をまく 〜

【Istio⛵️】Istioのサービス間通信を実現するサービスディスカバリーの仕組み


この記事から得られる知識

この記事を読むと、以下を "完全に理解" できます✌️

記事のざっくりした内容は、以下のスライドからキャッチアップできちゃいます!



01. はじめに


推し (Istio) が尊い🙏🙏🙏

istio-icon

3-shake Advent Calender 2022 最終日の記事です🎅

普段、私は 俺の技術ノート に知見を記録しており、はてなブログはデビュー戦となります。

最近の業務で、オンプレとAWS上のIstio⛵️をひたすら子守りしています。

今回は、子守りの前提知識の復習もかねて、Istioのサービス間通信を実現するサービスディスカバリーの仕組みを記事で解説しました。

Istioの機能の1つであるサービスディスカバリーは、その仕組みの多くをEnvoyに頼っているため、合わせてEnvoyの仕組みも説明します。

それでは、もりもり布教していきます😗

記事中のこのボックスは、補足情報を記載しています。

飛ばしていただいても大丈夫ですが、読んでもらえるとより理解が深まるはずです👍


02. サービスディスカバリーについて

マイクロサービスアーキテクチャにおけるサービスディスカバリー

サービスディスカバリーとは

平易な言葉で言い換えると サービス間通信 です。

マイクロサービスアーキテクチャでは、マイクロサービスからマイクロサービスにリクエストを送信する場面があります。

サービスディスカバリーとは、宛先マイクロサービスの宛先情報 (例:IPアドレス、完全修飾ドメイン名、など) を検出し、送信元マイクロサービスが宛先マイクロサービスにリクエストを継続的に送信可能にする仕組みのことです。

service-discovery.png

なぜサービスディスカバリーが必要なのか

そもそも、なぜサービスディスカバリーが必要なのでしょうか。

マイクロサービスアーキテクチャでは、システムの信頼性 (定められた条件下で定められた期間にわたり、障害を発生させることなく実行する程度) を担保するために、マイクロサービスのインスタンスの自動スケーリングを採用します。

この時、自動スケーリングのスケールアウトでマイクロサービスが増加するたびに、各インスタンスには新しい宛先情報が割り当てられてしまいます。

また、マイクロサービスが作り直された場合にも、宛先情報は更新されてしまいます。

このように、たとえインスタンスの宛先情報が更新されたとしても、インスタンスへのリクエストに失敗しない仕組みが必要です。

サービスディスカバリーの要素

サービスディスカバリーの仕組みは、次の要素からなります。

名前解決は、DNSベースのサービスディスカバリー (例:CoreDNS + Service + kube-proxyによるサービスディスカバリー) で必要となり、Istioでは使いません。

そのため、本記事では言及しないこととします🙇🏻‍

service-discovery-pattern.png

要素 責務
送信元マイクロサービス リクエストを送信する。
宛先マイクロサービス リクエストを受信する。
サービスレジストリ 宛先マイクロサービスの宛先情報を保管する。
ロードバランサー 宛先マイクロサービスのインスタンスにロードバランシングする。
名前解決 宛先マイクロサービスへのリクエスト送信時に、名前解決可能にする。


サービスディスカバリーのパターン

サービスディスカバリーのパターンとは

サービスディスカバリーの仕組みにはいくつか種類があります。

Istioのサービスディスカバリーは、このうちのサーバーサイドパターンを実装したものになります。

サーバーサイドパターン

service-discovery-pattern_client-side.png

送信元マイクロサービスから、問い合わせとロードバランシングの責務が切り離されています。

送信元マイクロサービスは、ロードバランサーにリクエストを送信します。

ロードバランサーは、宛先マイクロサービスの宛先をサービスレジストリに問い合わせ、またリクエストをロードバランシングする責務を担っています💪🏻

(例) Istio、Linkerd、など

クライアントサイドパターン

service-discovery-pattern_server-side.png

通信の送信元マイクロサービスは、宛先マイクロサービスの宛先をサービスレジストリに問い合わせ、さらにロードバランシングする責務を担います。

(例) NetflixのEureka、など


03. Istioのサービスディスカバリーの仕組み

Istioが実装するサービスメッシュには、サイドカープロキシメッシュとアンビエントメッシュがあり、今回はサイドカープロキシメッシュのサービスディスカバリーを取り上げます。

Istioのサービスディスカバリーは、discoveryコンテナとistio-proxyコンテナが軸となり、サーバーサイドパターンのサービスディスカバリーを実装します。

全体像

(1) 〜 (6) の全体像は、以下の通りです👇

istio-proxyコンテナは、サービスレジストリへの問い合わせと、ロードバランシングする責務を担っていることに注目してください。

service-discovery_istio.png

(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コンテナに直接的にリクエストを送信しているというよりは、iptablesistio-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から取得できる宛先情報を整理して取得できる。

service-discovery_xds-api.png

(1) kube-apiserverによる宛先情報保管

kube-apiserverによる宛先情報保管 と同じです。

(2) discoveryコンテナによる宛先情報保管

discoveryコンテナによる宛先情報保管 と同じです。

(3) istio-proxyコンテナによる宛先情報取得

XDS-APIistio-proxyコンテナの間では、gRPCの双方向ストリーミングRPCの接続が確立されています。

そのため、istio-proxyコンテナからのリクエストに応じて宛先情報を返却するだけでなく、リクエストがなくとも、XDS-APIからもistio-proxyコンテナに対して宛先情報を送信します。

XDS-APIのエンドポイントがいくつかあり、各エンドポイントから宛先情報を取得できます。

一方で、各エンドポイントからバラバラに宛先情報を取得すると、Envoy上でこれを整理する時に、宛先情報のバージョンの不整合が起こる可能性があります。

そのため、Istioは実際にはADS-APIを使用して宛先情報を取得します。


istio-proxyコンテナの仕組み

全体像の中から、istio-proxyコンテナを詳しく見てみましょう。

service-discovery_xds-api.png

(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) の全体像は、以下の通りです👇

service-discovery_envoy.png

(1) 送信元マイクロサービスからリクエスト受信

istio-proxyコンテナは、送信元マイクロサービスからリクエストを受信します。

(2) Envoyによるリスナー選択

Envoyは、リクエストの宛先情報 (例:宛先IPアドレス、ポート番号、パス、ホスト、など) に応じてリスナーを選びます。

(3) Envoyによるルート選択

Envoyは、リスナーに紐づくルートを選びます。


TCPリクエストを処理する場合について

HTTPリクエストを処理する場合、リスナーに紐づくのはルートですが、TCPリクエストの場合はそうではありません。

TCPリクエストを処理する場合、リスナーにクラスターが紐づきます👍🏻

(4) Envoyによるクラスター選択

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


▶ 宛先情報を見やすくするyqコマンドについて

Envoyは、JSON形式で設定を出力します。

JSONだと見にくいため、yqコマンドでYAMLに変換すると見やすくなります👍

リスナー

▼ 確認方法

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を取得でき、以下を確認できました。

  • リスナーを取得した時に確認できたルートの名前
  • リクエストのパスやHostヘッダーに応じてルートを選べるようになっている
  • ルートに紐づくクラスターの名前
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を取得でき、以下を確認できました。

  • クラスターを取得した時に確認できたエンドポイントの親名
  • bar-podのインスタンス3個あるため、3個のエンドポイントがあります
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の負荷分散方式について

全てのエンドポイントのload_balancing_weightキー値が等しい場合、EnvoyはP2Cアルゴリズムに基づいてロードバランシングします👍


Envoyの処理の流れのまとめ

確認できた宛先情報を、Envoyの処理の流れに当てはめてみました。

service-discovery_envoy_detail.png

(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 での発表前後に、以下の方々に発表内容について助言をいただきました。

(アルファベット順)

また、今回の 3-shake Advent Calender 2022 は、以下の方々に企画いただきました。

(アルファベット順)

皆様に感謝申し上げます🙇🏻‍


記事関連のおすすめ書籍