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

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

【Istio⛵️】サービスディスカバリーの仕組み

目次


01. はじめに

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

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

さて今回は、サービスメッシュを実装するIstio⛵️のサービスディスカバリーに関する記事を投稿しました。

Istioの機能の一つである『サービスディスカバリー』の仕組みを、Envoyを交えながら、もりもり布教しようと思います(沼のまわりに餌をまく)。

今回の記事では、先日の 3-shake SRE Tech Talk で発表した内容に加えて、スライドの余白と発表時間の制約で記載できなかったことも記載しました😗

ℹ️ 参考:Istio⛵️によるサービスディスカバリーの仕組み - Speaker Deck


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

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

(例)NeflixのEureka、など

ℹ️ 参考:


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

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

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

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

各コンテナについて詳しく見ていく前に、全体像を解説します。

service-discovery_istio.png

  1. kube-apiserverは、Pod等の宛先情報をetcd等に保管する。これは、Kubernetesの通常の仕組みである。
  2. discoveryコンテナは、kube-apiserverからPod等の宛先情報を取得し、自身に保管する。
  3. istio-proxyコンテナは、discoveryコンテナからPod等の宛先情報を双方向ストリーミングRPCで取得する。
  4. 送信元マイクロサービスがリクエストを送信する。Pod内のiptablesがこれをリダイレクトし、istio-proxyコンテナが受信する。
  5. istio-proxyコンテナは、リクエストをロードバランシングし、宛先Podにこれを送信する。

上記では、サーバーサイドパターンでの責務通り、送信元マイクロサービスはロードバランサー(ここではistio-proxyコンテナ)にリクエストを送信しています。

この時、送信元マイクロサービスがistio-proxyコンテナに直接的にリクエストを送信しているというよりは、iptablesistio-proxyコンテナにリクエストをリダイレクトしてくれています。

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

ℹ️ 参考:


discoveryコンテナの仕組み

discoveryコンテナを詳しく見てみましょう。

discoveryコンテナは、別名Istiodと呼ばれています。

XDS-APIというエンドポイントを公開しており、XDS-APIのうち、サービスディスカバリーに関係するAPIは以下の通りです。

APIの種類 説明
LDS-API Envoyのリスナー値を取得できる。
RDS-API Envoyのルート値を取得できる。
CDS-API Envoyのクラスター値を取得できる。
EDS-API Envoyのエンドポイント値できる。
ADS-API 各XDS-APIから取得できる宛先情報を整理して取得できる。

service-discovery_xds-api.png

discoveryコンテナは、kube-apiserverからPod等の宛先情報を取得して自身のメモリ上に保管し、各XDS-APIから提供します。

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

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

各種XDS-APIから個別に宛先情報を取得できますが、Envoy上で宛先情報のバージョンの不整合が起こる可能性があるため、Istioでは実際にはADS-APIを使用しています。

ℹ️ 参考:Amazon | Istio in Action | Posta, Christian E., Maloku, Rinor | Software Development


istio-proxyコンテナの仕組み

istio-proxyコンテナを詳しく見てみましょう。

istio-proxyコンテナでは、pilot-agentとEnvoyが稼働しています。

先ほどistio-proxyコンテナは、双方向ストリーミングRPCでADS-APIから宛先情報を取得すると説明しました。

厳密にはEnvoyが、pilot-agentを介して、ADS-APIから双方向ストリーミングRPCで宛先情報を取得します。

istio-proxyコンテナが送信元マイクロサービスからリクエストを受信すると、EnvoyはADS-APIから取得した宛先情報に基づいて、宛先マイクロサービスのインスタンスにロードバランシングします。

service-discovery_xds-api.png

ℹ️ 参考:


04. istio-proxyコンテナ内のEnvoyの仕組み

Envoyの処理の流れ

EnvoyがADS-APIから取得した宛先情報を見ていく前に、Envoyの処理の流れを解説します。

istio-proxyコンテナ内のEnvoyでは、以下の仕組みでリクエストを処理します。

service-discovery_envoy.png

  1. istio-proxyコンテナは、送信元マイクロサービスからリクエストを受信する。
  2. Envoyは、リクエストの宛先情報(例:宛先IPアドレス、ポート番号、パス、ホスト、など)に応じてリスナー値を選ぶ。
  3. Envoyは、リスナーに紐づくルート値を選ぶ。
  4. Envoyは、クラスターに紐づくクラスター値を選ぶ。
  5. Envoyは、クラスターに紐づくエンドポイント値を選ぶ。
  6. Envoyは、エンドポイント値に対応するインスタンスにリクエストを送信する。

Envoyで確認した宛先情報を👆に当てはめて見ていくことにしましょう。

ℹ️ 参考:


EnvoyがADS-APIから取得した宛先情報を見てみよう

config_dumpエンドポイント

実際にEnvoyに登録されている宛先情報は、istio-proxyコンテナ自体のlocalhost:15000/config_dumpからJSONで取得できます。

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

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

  • クラスター値を取得した時に確認できたエンドポイントの親名
  • bar-podのインスタンス3個あるため、3個のエンドポイントがあります

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

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: 80
                  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: 80
                  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: 80
                  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の処理の流れのまとめ

service-discovery_envoy_detail.png

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

  1. 送信元マイクロサービスは、宛先マイクロサービス(<任意のIP>/:50002)にリクエストを送信し、サイドカーコンテナのistio-proxyコンテナはこれを受信します。
  2. Envoyは、リクエストの宛先(IPアドレス、ポート番号、パス)からPodのリスナー値(0.0.0.0_50002)を選ぶ。
  3. Envoyは、リスナーに紐づくPodのルート値(50002)を選ぶ。
  4. Envoyは、クラスターに紐づくPodのクラスター値(outbound|50002|v1|bar-service.bar-namespace.svc.cluster.local)を選ぶ。
  5. Envoyは、クラスターに紐づくPodのインスタンスのエンドポイント値(11.0.0.X/:80)を選ぶ。
  6. Envoyは、エンドポイント値の宛先にPodのリクエストを送信する。

サービスディスカバリーの冒険は以上です。


05. おわりに

今回、Istioの機能の一つである『サービスディスカバリー』の仕組みを、Envoyを交えながら布教しました。

もりもりでしたが、最後までお付き合いいただきありがとうございました。

ここまで見ていただいたそこのあなた、片足が沼に浸かってます😏


謝辞

3-shake SRE Tech Talk での発表前後に、以下の方々に、発表内容について助言をいただきました。

(アルファベット順)

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

(アルファベット順)

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