Enjoy Architecting

Twitter: @taisho6339

これだけは知っておこう負荷試験 ~その1 基本とお試し試験~

記事の概要

負荷試験はシステム運用において避けて通れないタスクであるが難易度が高いタスクでもある。

本記事ではまず、実際にWordpressに対して簡単な負荷試験を行いながら、 負荷試験における基本的な観点を整理していく。

負荷試験の主な目的

負荷試験は何のために行うのだろうか? 主な観点としては下記のような項目が挙げられる。

  • 負荷に耐えうる構成か?
    • システムが想定した負荷に耐えられることを確認する
    • 長時間負荷をかけ続けた場合にメモリリークなどの潜在的なバグがないかを確認する
  • 要件に対して最適な構成になっているか?
    • 突然スパイクしたときにも自動でスケールし耐えられる構成になっているか?
    • 通常時に必要以上にインフラリソースを確保していないか?
  • スケール特性を把握する
    • まずどこにボトルネックが来て、どこをチューニングすればシステムがスケールするかを確認する

負荷試験の大事なルール

  • 必ずどこかがボトルネックになっている状態を作ること

  • スコープを絞ってステップごとに区切って実施すること

    • 問題の原因特定を効率的に行うことができるため、少しずつ負荷のかけ方を変えながら実施する
  • ネットワーク的に近いところから攻撃する

    • クラウドであれば同じVPC内から攻撃するのが望ましい
    • ネットワーク環境は千差万別であり、コントロールできないため、純粋にシステムの負荷を可視化したいのであればネットワーク起因の問題は排除した上で試験するべきである
  • 最初は最小の構成 & 自動スケールしないようにして実施する

    • ボトルネックがどこにあるかがわからない状態でオートスケールする構成で実施してしまうと、ボトルネックの特定が困難になる。よって、スケール特性を把握するフェーズで初めてオートスケールするようにする。

負荷試験をする上で参考にする性能指標

負荷試験実施の際は、下記指標を確認していき、問題があった場合はCPU使用率やメモリ使用率、コネクション数などの細かい指標を追っていくことになる。

  • スループット

    • 単位時間あたりどのくらいの処理をさばくことができるのか
  • レイテンシ

    • 処理をリクエストして、結果がクライアントに返ってくるまでどのくらいの時間がかかったか

実際の負荷試験のステップ例

  • 攻撃サーバのセットアップ

    • 最大攻撃性能を把握することで攻撃のパフォーマンスが十分に発揮できているかを確認する。
  • フレームワークの素の性能の把握

  • DBへの参照を含めた性能の把握

    • DBとの接続方法、クエリの実行方法などが適切に設定、実装されているかを確認する。
    • 前工程と比較してDB参照に大幅な劣化がある場合は、下記観点で確認する。
      • Web, DB, 攻撃サーバで負荷が正しくかかっているかを確認する
      • バッファプールにデータが乗り切っているか、キャッシュヒット率はどのくらいか
      • コネクションを永続化できているか確認する(コネクションプーリングの設定)
      • アプリケーション側でN+1やスロークエリなどが出ているか
  • DBへの更新を含めた性能の把握

    • DBとの接続方法、クエリの実行方法などが適切に設定、実装されているかを確認する。
    • 前工程と比較してDB参照に大幅な劣化がある場合は、下記観点で確認する。
      • Web, DB, 攻撃サーバで負荷が正しくかかっているかを確認する
      • 必要以上に偏ったユーザデータのシナリオになっていないか? ※例えばテストユーザを一人だけにして更新テストを行う場合、ロックの傾向が偏ってしまい、本番のシナリオと剥離してしまう。
      • 不必要なロック、トランザクション分離レベルなどを見直しつつ調整
  • 外部サービスとの通信を含めた性能の把握

    • 外部サービスとネットワークを経由して通信しているケースの負荷シナリオ
    • 自サービスの管轄外の場合、ここがボトルネックになりやすい
    • 負荷をかける場合は、意図せず外部サービスに過負荷をかけてしまわないよう気をつける
  • 実際の使用を想定したシナリオでの性能の把握

    • 実際のユーザのシナリオを想定し、負荷をかける
    • このシナリオでの負荷試験でしっかり想定どおりのパフォーマンスがでるかどうかを検証する
  • スケール特性試験

    • リソースが想定より余裕がある場合はスケールイン、スケールダウンして試してみることで必要以上にコストがかかっていないかを検証する

    • 負荷をあげていったときにどこがボトルネックになり、実際にそのボトルネックをスケールアウト、スケールアップ、パラメータチューニングにより解消したときに、システムが想定どおりスケールすることを確認する

実践してみよう!

環境

この環境で出せる攻撃時スループットの上限値を知る

現時点で、MAXでどのくらいのスループットがでるのかを検証するため、 一番処理の軽い静的ファイルを返すエンドポイントに対し、 Apache Benchを使って負荷をかけてみる。 ネットワーク的な要因を排除した純粋なスループットを計測したいのでホストから実行する。 ネットワークはスループットをかなり落としてしまう要因になるので、実際の負荷試験においてもなるべくネットワーク的に近いところから試験することはかなり重要になってくる。

ab -n 20000 -c 200 -k http://10.108.252.89/readme.html

大体この数値がこの環境における攻撃時に出せる最大のスループットということになる。

Requests per second:    7711.03 [#/sec] (mean)

検証の妥当性

この結果が妥当であることをどのように判別すればよいだろうか? まず最初に見るべき観点は以下になる。

  • 対象システムのCPU使用率が100%近くまで上がっているか?

    • ※DBサーバへの負荷の場合はもう少し下がることが多い
  • 攻撃サーバ側のCPU使用率は100%近くまで上がっているか?

    • 余裕がある場合は、同時接続クライアント数が足りていない

極端に同時接続クライアントが多すぎる場合スループットには出ないが、レイテンシが極端に落ちる。(リクエストが実行されるまでの待ち時間も含まれてしまうため)

こうなると問題の切り分けが大変になってしまうので、同時接続クライアント数は多すぎず、少なすぎない状態で実施するのがベスト。

(十分に負荷がかかっているのに必要以上にクライアント数をあげない)

今回は静的ファイルのため、Webサーバ単体への負荷となり、WebサーバはCPU使用率が90%以上で張り付いていたため、 十分に負荷をかけられていると判断した。

Wordpressの実際のスループット

今度は静的ページではなく、MySQLへのアクセスを伴う実際のページにアクセスしてみる。

ab -n 20000 -c 200 -k http://10.108.252.89

スループットは下記まで落ち込んだ。

Requests per second:    30.93 [#/sec] (mean)

また、DBもWebもCPU使用率90%以上で張り付いているので負荷はしっかりかかっていると見て良さそうである。

DB側がボトルネックになっていそうなので、DBのPodをスケールアップしてみる。

DBをスケールアップしてみた後

もともとが1コアを割り当てていたので、2コア割り当てるようにしてみた。

kubectl describe node

  Namespace                  Name                                CPU Requests  CPU Limits  Memory Requests  Memory Limits  AGE
  ---------                  ----                                ------------  ----------  ---------------  -------------  ---
  default                    wordpress-5bbd7fd785-gswtm          0 (0%)        0 (0%)      0 (0%)           0 (0%)         22h
  default                    wordpress-mysql-88898b8ff-jhj9g     2 (50%)       2 (50%)     0 (0%)           0 (0%)         22h
ab -n 20000 -c 200 -k 10.108.109.63

両者ともにCPU使用率は90%以上で、スループットが純粋に向上した。

Requests per second:    70.14 [#/sec] (mean)

ただ、またもやDBもWeb側もCPUが90%以上になっているため、 まずDB側をスケールアップしないとスループットは上がらなさそうである。 今回はアプローチを変えてDBを一定時間キャッシュさせ、DBアクセスをしないようにしてみる。

DBキャッシュ適用後

DBキャッシュを有効にした結果、スループットが劇的に改善している。

Requests per second:    3120.29 [#/sec] (mean)

DB側のCPU使用率も一瞬はねたが、一気に下がった。 一方Web側は変わらずCPU使用率が90%超えであり、 ボトルネックがDBからWebに移ったのが分かる。

まとめ

ボトルネックがWeb側に移動したのでWeb側をスケールしたいところだが、ローカルの貧弱な環境だと限界なのでローカルでの試験は一旦ここまでとする。

基本的な流れをこのさきのステップも同じで、

を繰り返していくことになる。

次回はKubernetesにアプリケーションをデプロイし、複数ノード & 攻撃サーバも負荷対象システムもスケールできる環境で実際にやってみる。

~ TO BE CONTINUED ~

Istioで割合でTraffic Managementするときにユーザごとにセッションを固定する

解決したい課題

Istioでweightによってサービスのバージョンを切り替えているとき、 リクエストの割合ベースで切り替えているだけなので、 同一ユーザでも異なるバージョンが表示されてしまう。 ユーザには少なくとも一定期間は同じバージョンを見せたいケースが多いと思うので今回はその方法を検証してみた。

Istioに用意されているSession Affinity機能

IstioではDestinationRuleで、ユーザごとに一定期間バージョンを固定化してルーティングするよう宣言できる。 https://istio.io/docs/reference/config/networking/destination-rule/#LoadBalancerSettings-ConsistentHashLB だがこれはConsistent Hashアルゴリズムによって実装されているため、PodがHPAでスケールアウト、スケールインしたときにルーティングされる向き先が変わってしまう。 また、この機能はweightの指定によってルーティングを設定しているときには併用してつかうことができない。 この機能はIstioというよりは実際にはEnvoyが担っていて、Envoy側のIssueを見る限りまだ解決されている様子はない。 https://github.com/envoyproxy/envoy/issues/8167

ではどうすればよいのか?

今回はWorkAround的な手法として、Cookieを用い、下記の方法を考えた。

  • Cookieがないリクエストに関しては、設定した割合でそれぞれのバージョンに割り振る
  • Cookieを持っているリクエストに関しては、そのCookieが保持しているバージョン情報に基づいて割り振る

あるバージョンに割り振られたトラフィックに対して、レスポンスにSetCookieヘッダーを付与し、 そこにサービスのバージョン情報を付与する、という方針を考えた。

具体的な実装

apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: sample-service-gateway
spec:
  selector:
    istio: ingressgateway # use istio default controller
  servers:
    - port:
        number: 80
        name: http
        protocol: HTTP
      hosts:
        - "*"
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: sample-service-gateway-vs
spec:
  hosts:
    - "*"
  gateways:
    - sample-service-gateway
  http:
    - match:
        - headers:
            Cookie:
              exact: sample-service-version=v1
      route:
        - destination:
            host: sample-service
            subset: v1
            port:
              number: 8080
    - match:
        - headers:
            Cookie:
              exact: sample-service-version=v2
      route:
        - destination:
            host: sample-service
            subset: v2
            port:
              number: 8080
    - route:
        - destination:
            host: sample-service
            subset: v1
            port:
              number: 8080
          weight: 50
          headers:
            response:
              add:
                "Set-Cookie": sample-service-version=v1; Max-Age=2592000
        - destination:
            host: sample-service
            subset: v2
            port:
              number: 8080
          weight: 50
          headers:
            response:
              add:
                "Set-Cookie": sample-service-version=v2; Max-Age=2592000
---
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: sample-service
spec:
  host: sample-service
  subsets:
    - name: v1
      labels:
        version: v1
    - name: v2
      labels:
        version: v2

これで実際にアクセスしてみると、最初は50%の確率でどちらかに割り振られ、それ以降のリクエストは同じバージョンへルーティングされる。

まとめ

今回はIstioでCookieを用いることで、バージョンを固定したルーティングを行った。 A/Bテストなど、ユーザには一貫した結果を出したい場合に活用したい。

Istio + Argo Rolloutとカナリアリリース

本記事について

Argo Rolloutを導入すると、様々なデプロイ戦略を実践することができる。 ただ少し実際の運用イメージが見えづらかったので整理してみる。

Argo Rolloutについての概要

Argo Rolloutとは通常のk8sのDeploymentに加え、カナリアリリースなどの様々なアップデート戦略をサポートするためのコンポーネント。 Rolloutという、Deploymentに対し、様々なPodのアップデート戦略をつけ加えたCRDを用いる。 Deployment同様に内部ではReplicaSetを管理していて、 アップデートのたびに新しいReplicaSetを作り、設定の通りにPodを移行させる。

カナリアリリース

一部のリクエストを新しいバージョンに流し、なにかあればロールバック、何も無ければ徐々にトラフィックを増やし、最終的に100%に切り替える。 ArgoRolloutでは、Rolloutリソースにどのように移行していくかをステップごとに宣言する。 設定できるのは下記のような項目。

  • setWeightでカナリアバージョンの比重を設定する
  • pauseでその状態でどのくらい待機するかを定義する
  • analysisで同期的な新バージョンのヘルスチェックを挟む

ヘルスチェックについて

B/Gやカナリアの場合、定期的にヘルスチェックを行い、失敗した場合にロールバックすることができる。 実際にはAnalysisTemplateに記述し、Rolloutで指定することで、実行時にAnalysisRunリソースが作られ、 指定したヘルスチェックを実行する。 AnalysisTemplateでは、下記の実行形式をサポートしている。

  • Prometheusサーバからのメトリクス取得

    • 一定時間内の200エラーが95%以上など
    • 一定時間内の5xx系エラーが1%以下など
  • Job Metrics

  • WebHook

    • クラスタ内、クラスタ外に対して指定タイミングでリクエストを送り、その結果でもって判別する
    • 結果はJSONである必要があるが、形式は問わない(パースの設定をすることができる)

ちなみにAnalysisを定義しない場合はロールバックは自動で行わない。

結局Argo Rolloutを使って担保されるものはなに?

カナリアリリースの想定実行フロー

  • 新バージョンデプロイ => 数%のトラフィックを新バージョンへ流す => 一定時間経って問題なければ自動的に新バージョンをStableバージョンを切り替え
  • 新バージョンデプロイ => 数%のトラフィックを新バージョンへ流す => manual approveで新バージョンをStableバージョンへ切り替え

やらないといけないこと

  • DeploymentはRolloutに置き換える
  • カナリアリリース用のServiceを作成する
  • VirtualServiceでカナリアリリースサービスとStableリリース用サービスで比重を0%, 100%に設定しておく
  • ヘルスチェック用のリソース定義(AnalysisTemplate)
  • Rolloutでstable用Service, canary用Service, virtualService, AnalysisTemplateを指定しておく

挙動FAQ

  • Podが存在しないときはどうなる?

    • Deploymentと同様で、UpdatePolicyによらず、新規のReplicaSetおよびPodが作られる
  • どのようにWeightをコントロールしている?

    • Traffic Routingの設定を組み合わせることでリクエストベースのWeigh制御ができるが、何も指定しないときはReplicaSetのPodの割合で調整する
    • IstioのようなTraffic Routing機能を持つコンポーネントと連携すると分散ポリシーを移譲することができる
  • Istioと連携するとどうなる?

    • ルーティングがリクエストの割合ベースで分散できる
    • WeightはArgoRolloutのPodがVirtualServiceの設定を随時書き換えている
  • カナリア用のServiceの向き先はStableと同じでいいの?

    • 良い。Argo Rolloutが随時Serviceのセレクタに条件を付け足している

How to Use

設定例

  • stable = v1, canary = v2のケース
  • AnalysisTemplateを使って、500エラーが一定期間内に3回でたらNGという定義を行っている
    • istioが持つprometheusに対してクエリを投げている
  • この設定では、比重を10%にし10分待ち、以降徐々に10分毎に20%ずつ増やす。50%になったら、そのまま待ち手動によるResumeを待つ。
apiVersion: v1
kind: Service
metadata:
  name: reviews
  labels:
    app: reviews
    service: reviews
spec:
  ports:
    - port: 9080
      name: http
  selector:
    app: reviews
---
apiVersion: v1
kind: Service
metadata:
  name: reviews-canary
  labels:
    app: reviews
    service: reviews
spec:
  ports:
    - port: 9080
      name: http
  selector:
    app: reviews
---
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: reviews
  labels:
    app: reviews
spec:
  replicas: 1
  selector:
    matchLabels:
      app: reviews
  template:
    metadata:
      labels:
        app: reviews
        version: v2
    spec:
      serviceAccountName: bookinfo-reviews
      containers:
        - name: reviews
          image: docker.io/istio/examples-bookinfo-reviews-v2:1.15.0
      volumes:
        - name: wlp-output
          emptyDir: {}
        - name: tmp
          emptyDir: {}
  strategy:
    canary:
      analysis:
        templates:
          - templateName: failure-count
        args:
          - name: service-name
            value: reviews-canary.default.svc.cluster.local
      stableService: reviews
      canaryService: reviews-canary
      trafficRouting:
        istio:
          virtualService:
            name: reviews
            routes:
              - primary
      steps:
        - setWeight: 10
        - pause: { 10m }
        - setWeight: 30
        - pause: { 10m }
        - setWeight: 50
        - pause: { }
---
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
  name: failure-count
spec:
  args:
    - name: service-name
  metrics:
    - name: failure-count
      interval: 30s
      failureCondition: result >= 3
      failureLimit: 3
      provider:
        prometheus:
          address: "http://prometheus.istio-system.svc.cluster.local:9090"
          query: |
            sum(irate(
              istio_requests_total{reporter="source",destination_service=~"{{args.service-name}}",response_code!~"5.*"}[30s]
            ))

GitOpsとの兼ね合い

  • カナリアリリースが完了し、100%新バージョンに移行したあとに同期されたらどうなる?

    • 結論: 何も変わらない
      • ArgoRolloutがいじった結果とリソースが一緒になる
      • StableとCanaryのServiceの向き先が両方共新Podになっていて、VirtualServiceもStableに100%向ける設定になっている
  • カナリアリリース実行中に同期されたらどうなる?

    • 結論: 一度ルーティングの割合がStable 100%, Canary 0%にリセットされた後、Rolloutによって同期される直前の状態に戻される

カナリアリリース時と通常リリース時でのフローの違い

デバッグ方法

  • RolloutとAnalysisRunリソースを見るとデプロイの挙動が分かる
 kd rollouts.argoproj.io
 kd analysisruns.argoproj.io

Analysisの設定は実運用上どうすれば良い?

  • IstioでPrometheusを有効にしてインストールし、Istioのメトリクスによって判別する

    • --set values.prometheus.enabled=trueをつけてistioctl manifest applyすればprometheusが有効になるので、これをつかってメトリクスを取得することはできる
    • ただ、素直に入れても可用性は担保されていないので、別途冗長化するなどの可用性対応が必要になってくる
  • Stackdriverなどにメトリクスを集約している場合、Goなどでデータを引っ張ってきた上で判定する

  • カナリアリリースした結果失敗してロールバックしたよね、というのはどのように検知すればよいか?

    • Argo Rolloutの機能では通知できないので、自前の検証プログラムを使ってAnalysisRunを動かす場合は、プログラムの中にエラーケースでアラートするように記載しておく
    • 例えばStackdriverならCloud Monitoringを活用して、ロールバック条件と同じ条件でアラートが飛ぶようにポリシー設定しておく

【GCP Anthos】 Fail Overの挙動から最適なマルチクラスタ構成を考える

記事の目的

前回の記事でIngress For Anthosを用いてRegionにまたがったKubernetsのマルチクラスタを構築した。 今回は1段深く掘り下げて、クラスタ内に複数のフロントサービスを持つケースを考える。 このケースでAnthosで構築したマルチクラスタのFail Overの挙動を詳しく検証し、 どんなときに、どんな条件でFail Overするのかを整理する。 また、それによって最適なマルチクラスタ構成を考える。 今回はIstioはマルチコントロールプレーン構成にしないので、クラスタ内のサービス間通信のFail Overについてはとりあげない。 【GCP Anthos】 Regionに跨って冗長化したKubernetsのマルチクラスタをロードバランシングする

題材となるサービス

今回は少し実運用を考慮し、クラスタ内に2つのフロントサービス(外部から直接ルーティングされてくるサービス)を作り、それぞれへはIngressGatewayでルーティングすることとする。

f:id:taisho6339:20200514120059p:plain

GCLB + NEGsのFailOverの検証

Anthosはクラスタ間のロードバランシングにGCPのCloud Loadbalancer(以下GCLB)とクラスタ内の任意のPodに紐付いたNEGを使う。 よってまずはGCLB + NEGの構成のFailOverの挙動をまずは整理する。

事前準備

  • 東京クラスタ
    • GKE 1.15
    • Anthosへのメンバー登録済み
    • istioインストール済み(v1.5.1)
  • 台湾クラスタ
    • GKE 1.15
    • Anthosへのメンバー登録済み
    • istioインストール済み(v1.5.1)

アプリケーションのデプロイ

ホストヘッダーによってbookinfoとzoneprinterへのルーティングの向き先を変更するようにしている。

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: zoneprinter
spec:
  hosts:
#    - "*"
    - "www.zoneprinter.com"
  gateways:
    - single-gateway
  http:
    - match:
        - uri:
            exact: /
        - uri:
            exact: /ping
        - uri:
            exact: /location
      route:
        - destination:
            host: zoneprinter
            port:
              number: 80
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: bookinfo
spec:
  hosts:
    - "*"
#    - "www.bookinfo.com"
  gateways:
    - single-gateway
  http:
    - match:
        - uri:
            exact: /
        - uri:
            exact: /productpage
        - uri:
            prefix: /static
        - uri:
            exact: /login
        - uri:
            exact: /logout
        - uri:
            prefix: /api/v1/products
      fault:
          abort:
            httpStatus: 503
            percentage:
              value: 100.0
      route:
        - destination:
            host: productpage
            port:
              number: 9080

MCI, MCSリソースのデプロイ

一つのMCSを一つのIngress GatewayのPodに割り当てている。

FailOverの検証方法

MultiClusterServiceが一つなのでIngress GatewayのPodに紐付いた単一のNEGが作られる。 よってこのままだと、ヘルスチェックに使用できるパスが一つのフロントサービスへのルーティングのみになってしまうが、一旦このまま検証する。

  • ヘルスチェック対象のサービス = zoneprinter, ヘルスチェック非対象のサービス = bookinfo
    • ホストヘッダーがwww.bookinfo.comのものはbookinfoへ、それ以外はzoneprinterへルーティング
    • ヘルスチェックは、/ のパスへ
    • ダウンさせるサービスは、IstioのFault Injectionを利用し、エラーが常に返却されるように
    • サンプルコードはこちら

GCLB + NEGのFail Overの挙動結果まとめ

  • ヘルスチェック対象でないサービスがダウン

    • リクエスト時に5xxエラー => Fail Overしない
    • リクエスト時に503(Service Unavailable)エラー => Fail Overしない
    • 一定時間内のほとんどのリクエストが5xxエラー => Fail Overしない
    • 一定時間内のほとんどのリクエストが503エラー => Fail Overしない
  • ヘルスチェック対象のサービスがダウン

    • リクエスト時に5xxエラー => Fail Overしない
    • リクエスト時に503(Service Unavailable)エラー => Fail Overしない
    • 一定時間内のほとんどのリクエストが5xxエラー => Fail Overしない
    • 一定時間内のほとんどのリクエストが503エラー => Fail Overしない
    • ヘルスチェックがエラー => Fail Overする
  • Fail Over条件のカスタマイズは不可能

つまり、ヘルスチェックによってのみFail Overするかどうかが決まる。 よって、今の構成だと、ヘルスチェック対象のサービスがUnhealthyの場合、 引きずられてHealthyなサービスまでも、冗長化された別クラスタにFail Overされてしまう。

理想的な挙動はなにか?

フロントサービスに対して個別にヘルスチェックを実施し、 一つのフロントサービスのヘルスチェックステータスが他のサービスのルーティングに影響しないようにしたい。 つまり、今回ならzoneprinterがUnhealthyだからといって、bookinfoまで別クラスタにルーティングしないでほしい。

どういう構成にするべきか

ようは各フロントサービスごとにヘルスチェックしてほしい => 各サービスごとにNEGが作られて、それらがGCLBのバックエンドとして設定されてほしいということになる、 よってフロントサービスの数だけMultiClusterServiceを作成し、単一のIngressGatewayに紐付けることにした。

f:id:taisho6339:20200514115840p:plain

補足

ちなみに、各フロントサービスに対して直接NEGを紐付ければいいのでは?と思うかもしれないが、 そうなるとIngressGatewayのEnvoyを通過せずに直接リクエスト先サービスのPodにルーティングされてしまうので、 VirtualServiceやDestinationRuleの設定が無視されてしまう。 この辺の挙動のイメージは、こちらにまとめた。 また、IngressGatewayのPodへのNEGを作るので、IngressGatewayにアタッチされているL4のGCLBは不要になる。 よってIstio Operatorなどを使ってNodePortに変更する。

実際のコード

apiVersion: cloud.google.com/v1beta1
kind: BackendConfig
metadata:
  name: multi-gateway-bookinfo-cfg
  namespace: istio-system
spec:
  healthCheck:
    checkIntervalSec: 15
    timeoutSec: 10
    healthyThreshold: 1
    unhealthyThreshold: 2
    type: HTTP
    port: 80
    requestPath: /
    hostHeader: "www.bookinfo.com"
---
apiVersion: networking.gke.io/v1
kind: MultiClusterService
metadata:
  name: multi-gateway-bookinfo
  namespace: istio-system
  annotations:
    beta.cloud.google.com/backend-config:
      '{"ports": {"80":"multi-gateway-bookinfo-cfg"}}'
spec:
  template:
    spec:
      selector:
        app: istio-ingressgateway
      ports:
        - name: multi-gateway-bookinfo
          protocol: TCP
          port: 80
          targetPort: 80
---
apiVersion: cloud.google.com/v1beta1
kind: BackendConfig
metadata:
  name: multi-gateway-zoneprinter-cfg
  namespace: istio-system
spec:
  healthCheck:
    checkIntervalSec: 15
    timeoutSec: 10
    healthyThreshold: 1
    unhealthyThreshold: 2
    type: HTTP
    port: 80
    requestPath: /
    hostHeader: "www.zoneprinter.com"
---
apiVersion: networking.gke.io/v1
kind: MultiClusterService
metadata:
  name: multi-gateway-zoneprinter
  namespace: istio-system
  annotations:
    beta.cloud.google.com/backend-config:
      '{"ports": {"80":"multi-gateway-zoneprinter-cfg"}}'
spec:
  template:
    spec:
      selector:
        app: istio-ingressgateway
      ports:
        - name: multi-gateway-zoneprinter
          protocol: TCP
          port: 80
          targetPort: 80
apiVersion: networking.gke.io/v1
kind: MultiClusterIngress
metadata:
  name: multi-gateway
  namespace: istio-system
spec:
  template:
    spec:
      rules:
        - host: "www.bookinfo.com"
          http:
            paths:
              - path: /
                backend:
                  serviceName: multi-gateway-bookinfo
                  servicePort: 80
        - host: "www.zoneprinter.com"
          http:
            paths:
              - path: /
                backend:
                  serviceName: multi-gateway-zoneprinter
                  servicePort: 80
      backend:
        serviceName: multi-gateway-zoneprinter
        servicePort: 80

確認

この状態でbookinfoのヘルスチェックエンドポイントに対してFaultInjectionし、 zoneprinterにcurlすると、

❯❯❯ curl -H "Host:www.zoneprinter.com" http://35.241.18.9/location
<!DOCTYPE html>
<h4>Welcome from Google Cloud datacenters at:</h4>
<h1>Tokyo, Japan</h1>
<h3>You are now connected to &quot;asia-northeast1-a&quot;</h3>
<img src="https://upload.wikimedia.org/wikipedia/en/9/9e/Flag_of_Japan.svg" style="width: 640px; height: auto; border: 1px solid black"/>

となり、引きずられてFailOverしていないことが分かる。

また、逆にzoneprinterにFaultInjectionし、 bookinfoにアクセスすると、 台湾クラスタの方のログにアクセスが来たことが分かる。

まとめ

  • GCLBはヘルスチェックによってのみFailOverする
  • MCSに対して一つずつNEGが作られるので、複数のNEGを作ることで複数のフロントサービスに対してヘルスチェックを行うことができ、サービスごとにFailOverできる
  • 今回のコード

IstioのTraffic Managementの動作イメージを掴もう

本記事について

Istioを使う上で、Traffic Managementを司るVirtual Service, DestinationRuleについて、 どう作用しているのかという点がわかりにくかったため、本記事にて整理し実際に動かしながら検証する。 前提として、サービスメッシュやEnvoyの基本的な設定についてはざっくりと理解している読者を想定している。

Istioとは?

Istioは、サービスメッシュの機能を統括的に導入、管理を行うためのOSSである。 内部ではEnvoyを使用していて、IstioとしてはとしてはこのEnvoyのコントロールプレーンとしての機能も担っている。 What is Istio?

主に、

  • Traffic Management

    • 割合やヘッダーによるルーティングのコントロールやロードバランシングなどを行うための機能
    • Envoyの機能であるような、サーキットブレーカー、Fault Injectionなどの機能も基本的に使える
  • Observability

    • Promheusなどメトリクスを集計するためのソフトウェアにデータを転送する機能
  • Security

    • mTLS通信による認証された通信や、JWTを活用した認可処理をプロキシとして挟んだりすることができる
    • mTLSに用いられる証明書はIstioが独自証明書を発行してくれ、特に意識せずともmTLSをサービス間通信に導入することができる

といった機能を提供しており、Kubernetsなどのプラットフォーム連携が充実している。

Traffic Managementについて

Istioは、k8sにデプロイされたPodに自動的にサイドカーとしてEnvoyベースのProxyをInjectする機能が備わっている。 基本的には、リクエスト元サービス -> Envoy -> Envoy -> リクエスト先サービスという流れで通信することになる。 そして、このEnvoyが実際にルーティングの制御を行っている。 このEnvoyに設定情報を提供するのがk8sのCRDである下記3種類のリソースである。

  • Virtual Service
  • DestinationRule

各リソースについての概念説明などはなどは、 公式ドキュメント に記載されている通りである。 このさきは、minikubeにIstioと、Istioのデモアプリである、Bookinfo をデプロイして挙動を確認していく。

事前準備

  • minikube v1.9.2
  • Istio v1.5.2
  • Kubernets v1.18.0

Ingress Gatewayの実態を見てみよう

初期状態

Ingress Gatewayを有効にしてIstioをk8sクラスタにインストールすると、

  • Service(Load Balancer Type)
  • Deployment

のリソースがそれぞれ作られていることが分かる。

❯❯❯ kubectl get service -n istio-system
istio-ingressgateway        LoadBalancer   10.103.255.236   10.103.255.236   15020:31097/TCP,80:31587/TCP,443:32153/TCP,15029:30056/TCP,15030:32475/TCP,15031:32379/TCP,15032:32322/TCP,31400:32143/TCP,15443:32376/TCP   3d15h
❯❯❯ kubectl get deployments.apps -n istio-system
NAME                   READY   UP-TO-DATE   AVAILABLE   AGE
istio-ingressgateway   1/1     1            1           3d15h

そして、このDeploymentによって管理されているPodの定義を見てみると、

❯❯❯ kubectl get pod -n istio-system istio-ingressgateway-6489d9556d-sp8f2 -o yaml | grep image:
    image: docker.io/istio/proxyv2:1.5.2

のようになっているのが分かる。 このproxyvというのが中でEnvoyが動いてる。 この状態でRoutingの設定を覗いてみると、まだなにも定義していないためすべて404になるようになっている。

❯❯❯ istioctl -n istio-system proxy-config route istio-ingressgateway-6489d9556d-sp8f2 -o json | yq -y '.' -
- name: http.80
  virtualHosts:
    - name: blackhole:80
      domains:
        - '*'
      routes:
        - name: default
          match:
            prefix: /
          directResponse:
            status: 404
  validateClusters: false

GatewayリソースとGatewayリソースに紐付けたVirtualServiceの作成

では次に下記リソースをapplyし、IngressGatewayにAttachしたGatewayリソースと、VirtualServiceを作ってみる。

❯❯❯ kubectl apply -f samples/bookinfo/networking/bookinfo-gateway.yaml

そうすると、下記のようなルーティングルールが作成されている。

❯❯❯ istioctl -n istio-system proxy-config route istio-ingressgateway-6489d9556d-sp8f2 -o json | yq -y '.' -
- name: http.80
  virtualHosts:
    - name: '*:80'
      domains:
        - '*'
        - '*:80'
      routes:
        ...
        - match:
            path: /productpage
            caseSensitive: true
          route:
            cluster: outbound|9080||productpage.default.svc.cluster.local
            timeout: 0s
            retryPolicy:
              retryOn: connect-failure,refused-stream,unavailable,cancelled,resource-exhausted,retriable-status-codes
              numRetries: 2
              retryHostPredicate:
                - name: envoy.retry_host_predicates.previous_hosts
              hostSelectionRetryMaxAttempts: '5'
              retriableStatusCodes:
                - 503
            maxGrpcTimeout: 0s
          metadata:
            filterMetadata:
              istio:
                config: /apis/networking.istio.io/v1alpha3/namespaces/default/virtual-service/bookinfo
          decorator:
            operation: productpage.default.svc.cluster.local:9080/productpage
          ...

このように、IngressGatewayにAttachしたGatewayを作成し、さらにそのGateway指定したVirtual Serviceを作ると、 L4/LBから直接ルーティングされてくるEnvoyにルーティングルールが追加される。 また、向き先としてはPodのIPが解決されている。

❯❯❯ istioctl -n istio-system proxy-config endpoint istio-ingressgateway-6489d9556d-sp8f2 -o json | yq -y '.' - | grep "outbound|9080||productpage.default.svc.cluster.local" -A 30
- name: outbound|9080||productpage.default.svc.cluster.local
  addedViaApi: true
  hostStatuses:
    - address:
        socketAddress:
          address: 172.17.0.12
          portValue: 9080
      weight: 1

Gatewayに紐付けないVirtualService

次に下記リソースをapplyし、reviewsサービスにはv2に50%、v3に50%の割合でルーティングされるようにしてみる。

❯❯❯ kubectl apply -f samples/bookinfo/networking/destination-rule-reviews.yaml
❯❯❯ kubectl apply -f samples/bookinfo/networking/virtual-service-reviews-v2-v3.yaml

まずIngress Gateway側のEnvoyの設定を確認する。

❯❯❯ istioctl -n istio-system proxy-config route istio-ingressgateway-6489d9556d-sp8f2 -o json | yq -y '.' -

今回はGatewayにAttachしていないVirtualServiceを作成したので、特にルーティング設定に変化がないことが分かる。

ではアプリケーション側のPodに配置されたEnvoyの設定はどうだろうか?

❯❯❯ istioctl proxy-config route productpage-v1-7f44c4d57c-nfnjd -o json | yq -y '.' -
- name: reviews.default.svc.cluster.local:80
      domains:
        - reviews.default.svc.cluster.local
        - reviews.default.svc.cluster.local:80
      routes:
        - match:
            prefix: /
          route:
            weightedClusters:
              clusters:
                - name: outbound|80|v2|reviews.default.svc.cluster.local
                  weight: 50
                - name: outbound|80|v3|reviews.default.svc.cluster.local
                  weight: 50
            timeout: 0s
            retryPolicy:
              retryOn: connect-failure,refused-stream,unavailable,cancelled,resource-exhausted,retriable-status-codes
              numRetries: 2
              retryHostPredicate:
                - name: envoy.retry_host_predicates.previous_hosts
              hostSelectionRetryMaxAttempts: '5'
              retriableStatusCodes:
                - 503
            maxGrpcTimeout: 0s
          metadata:
            filterMetadata:
              istio:
                config: /apis/networking.istio.io/v1alpha3/namespaces/default/virtual-service/reviews
          decorator:
            operation: reviews:80/*

長いので省略しているが、reviewsへのトラフィックのときに下記2つの向き先へ50%の割合でルーティングしているのが分かる。

  • outbound|80|v2|reviews.default.svc.cluster.local
  • outbound|80|v3|reviews.default.svc.cluster.local

endpointの設定を見てみると、それぞれv3のPodのIP、v2のPodのIPを指していることが読み取れる。

❯❯❯ istioctl proxy-config endpoint productpage-v1-7f44c4d57c-nfnjd -o json | yq -y '.' - | grep reviews -A 30
- name: outbound|9080|v3|reviews.default.svc.cluster.local
  addedViaApi: true
  hostStatuses:
    - address:
        socketAddress:
          address: 172.17.0.10
          portValue: 9080
- name: outbound|9080|v2|reviews.default.svc.cluster.local
  addedViaApi: true
  hostStatuses:
    - address:
        socketAddress:
          address: 172.17.0.11
          portValue: 9080

ちなみにこのルーティング設定は、Bookinfoアプリケーション内のそれぞれのサービスのサイドカーEnvoyの設定に組み込まれている。 (正確にはIstio Pilotの仕組みを使って設定が動的にInjectされている) つまり、リクエストを送る側、つまりクライアント側のプロキシ(Envoy)でロードバランシング、ルーティング制御を行い、Podにトラフィックを送っているのである。

DestinationRuleを削除してみる

最後に上記までの状態からDestinationRuleだけ削除してみる。

❯❯❯ kubectl apply -f samples/bookinfo/networking/destination-rule-reviews.yaml

ルーティングルールとしては変化していないように見える。

❯❯❯ istioctl proxy-config route productpage-v1-7f44c4d57c-nfnjd -o json | yq -y '.' -
clusters:
                - name: outbound|80|v2|reviews.default.svc.cluster.local
                  weight: 50
                - name: outbound|80|v3|reviews.default.svc.cluster.local
                  weight: 50

だが、エンドポイント定義としてはv2, v3が消えてしまっているのが分かる。

❯❯❯ istioctl proxy-config endpoint productpage-v1-7f44c4d57c-nfnjd -o json | yq -y '.' - | grep reviews

- name: outbound|9080||reviews.default.svc.cluster.local

まとめ

  • VirtualService, DestinationRuleはすべてIngress GatewayのEnvoyもしくは、サイドカーEnvoyの設定に作用する
    • Gatewayに紐付けたVirtualServiceは、Ingress GatewayのEnvoyのルーティング設定に作用する
    • Gatewayに紐づけていないVirtualServiceは各サービスのサイドカーEnvoyに、アウトバウンドのルーティング設定として作用する
      • これによってクライアント側でロードバランシング、ルーティングの制御を行っている
    • DestinationRuleはEnvoyのEndpoint設定に作用する

【GCP Anthos】Anthos Service Mesh + Ingress For Anthosを組み合わせたk8sのマルチクラスタを構築する

この記事について

前回の記事でIngress For Anthosを使ったマルチクラスタによるk8s冗長化について触れた。 今回はManagedなIstioを提供するAnthos Service Meshも取り入れ、 サービスメッシュを導入したマルチクラスタを実際に構築してみる。

Anthos Service Meshとは?

  • 基本的にはIstioだが、Configuration ProfileがAnthos用にカスタムされて提供されている。

何がマネージドになっているのか?

  • サービスの依存関係、SLOモニタリング、各サービスのメトリクス収集がManagedに集約され、ダッシュボードで統括的にみることができる

    • Istio Proxyのmetrics送信部分がおそらくManagedになっていて、ほぼ何もしなくてもAnthosに自動的に送られるようになっているぽい
  • GKE環境下ではCitadelがManagedになっていて、ルート証明書サーバ証明書周りの管理をGoogleに任せられる

    • ほぼ何も意識せずにmTLS機能を有効にできる
    • Cloud IAPと連携ができる(GCPのサービスアカウントによる認証プロキシをメッシュに挟める)
  • Traffic DirectorがManagedなIstio Pilotとして提供されているらしい(まだ未検証)

    クラスタの要件

  • 4つのvCPUを持つ、n1-standard-4のノードが4つ以上存在すること https://cloud.google.com/service-mesh/docs/gke-install-existing-cluster#requirements
Your GKE cluster must meet the following requirements:

- At least four nodes. If you need to add nodes, see Resizing a cluster

- The minimum machine type is n1-standard-4, which has four vCPUs. If the machine type for your cluster doesn't have at least four vCPUs, change the machine type as described in Migrating workloads to different machine types

- Use a release channel rather than a static version of GKE

本記事で構築するクラスタアーキテクチャ

  • アプリケーションとしては、Istioで用意されているでもアプリのBooknfoを用いる。

f:id:taisho6339:20200508211741p:plain
アーキテクチャ

  • 注意点としては、
    • LBからIngress Gatewayにルーティングする
    • Ingress Gatewayを作ると自動的にLoadBalancerタイプのServiceが作られるが、Ingress For AnthosはLBから直接Ingress GatewayのServiceにルーティングするので、NodePortタイプに書き換える

事前準備

n1-standard-4, 4つのvCPUのインスタンスを余裕を持って5つ程度でノードプールを構築し、クラスタを作っておく

手順について

基本的にはこの手順でIstioをインストールする ドキュメントの通りではあるが、ピックアップした手順を記載しておく。

Anthosの仲間に加える

  • role/gkehub.connectの権限を持ったサービスアカウントを作成する
  • 環境変数の設定
export PROJECT_ID=
export PROJECT_NUMBER=
export CLUSTER_NAME=
export CLUSTER_LOCATION=
export WORKLOAD_POOL=${PROJECT_ID}.svc.id.goog
export MESH_ID="proj-${PROJECT_ID}"
export SERVICE_ACCOUNT_NAME=anthos-connector
export SERVICE_ACCOUNT_KEY_PATH=
gcloud container hub memberships register $CLUSTER_NAME \
   --gke-cluster=${CLUSTER_LOCATION}/${CLUSTER_NAME} \
   --service-account-key-file=$SERVICE_ACCOUNT_KEY_PATH

Anthos Service Meshの設定

  • クラスタのラベルにメッシュIDを設定(モニタリングに用いられる)
gcloud container clusters update ${CLUSTER_NAME} \
  --update-labels=mesh_id=${MESH_ID}
  • Workload Identity(k8sのサービスアカウントをGCPのサービスアカウントとして認証する機能)
gcloud container clusters update ${CLUSTER_NAME} \
   --workload-pool=${WORKLOAD_POOL}
  • Stackdriver MonitoringとLoggingを有効化
gcloud container clusters update ${CLUSTER_NAME} \
   --enable-stackdriver-kubernetes
kubectl create clusterrolebinding cluster-admin-binding \
  --clusterrole=cluster-admin \
  --user="$(gcloud config get-value core/account)"
istioctl manifest apply --set profile=asm \
  --set values.global.trustDomain=${WORKLOAD_POOL} \
  --set values.global.sds.token.aud=${WORKLOAD_POOL} \
  --set values.nodeagent.env.GKE_CLUSTER_URL=https://container.googleapis.com/v1/projects/${PROJECT_ID}/locations/${CLUSTER_LOCATION}/clusters/${CLUSTER_NAME} \
  --set values.global.meshID=${MESH_ID} \
  --set values.global.proxy.env.GCP_METADATA="${PROJECT_ID}|${PROJECT_NUMBER}|${CLUSTER_NAME}|${CLUSTER_LOCATION}" \
  --set values.global.mtls.enabled=true
  • 任意のnamespaceに対してSidecarのAuto Injectionをdefault有効にする
kubectl label namespace default istio-injection=enabled

アプリケーションのデプロイ

kubectl apply -f samples/bookinfo/platform/kube/bookinfo.yaml
  • VirtualServiceをデプロイ
kubectl apply -f samples/bookinfo/networking/virtual-service-all-v1.yaml
  • DestinationRuleをデプロイ
kubectl apply -f samples/bookinfo/networking/destination-rule-all-mtls.yaml
kubectl apply -f samples/bookinfo/networking/bookinfo-gateway.yaml

Ingress For Anthosと連携する

  • Config用のクラスタを登録(本記事ではアプリケーションと兼用している)
gcloud alpha container hub ingress enable \
  --config-membership=projects/$PROJECT_ID/locations/global/memberships/$CLUSTER_NAME
  • Ingress GatewayのService TypeをNodePortにして不要なLoadBalancerを消す
    • istio-ingressgateway-patch.json
[
  {
    "op": "replace",
    "path": "/spec/type",
    "value": "NodePort"
  },
  {
    "op": "remove",
    "path": "/status"
  }
]
kubectl -n istio-system patch svc istio-ingressgateway \
    --type=json -p="$(cat istio-ingressgateway-patch.json)" \
    --dry-run=true -o yaml | kubectl apply -f -
  • MultiClusterServiceをデプロイ
kubectl apply -f - <<EOF
apiVersion: networking.gke.io/v1
kind: MultiClusterService
metadata:
  name: bookinfo
  namespace: istio-system
  annotations:
    beta.cloud.google.com/backend-config: '{"ports": {"80":"zone-health-check-cfg"}}'
spec:
  template:
    spec:
      selector:
        app: istio-ingressgateway
      ports:
        - name: web
          protocol: TCP
          port: 80
          targetPort: 80
EOF
  • BackendConfigをデプロイ
kubectl apply -f - <<EOF
apiVersion: cloud.google.com/v1beta1
kind: BackendConfig
metadata:
  name: bookinfo-health-check-cfg
  namespace: istio-system
spec:
  healthCheck:
    checkIntervalSec: 30
    timeoutSec: 10
    healthyThreshold: 1
    unhealthyThreshold: 2
    type: HTTP
    port: 80
    requestPath: /productpage
EOF
  • MultiClusterIngressをデプロイ
kubectl apply -f - <<EOF
apiVersion: networking.gke.io/v1
kind: MultiClusterIngress
metadata:
  name: bookinfo-ingress
  namespace: istio-system
spec:
  template:
    spec:
      backend:
        serviceName: bookinfo
        servicePort: 80
EOF

結果

  • AnthosによってGlobalなGCLBが作成され、Ingress GatewayにルーティングされるNEGがバックエンドとして登録されている f:id:taisho6339:20200508211903p:plain

  • GCLBのVIPにアクセスすることでアプリケーションが表示される f:id:taisho6339:20200508211934p:plain

  • サービスメッシュのダッシュボードはこんな感じになっている(SLOモニタリングの機能もある) f:id:taisho6339:20200508212043p:plainf:id:taisho6339:20200508212049p:plainf:id:taisho6339:20200508212053p:plainf:id:taisho6339:20200508212100p:plainf:id:taisho6339:20200508212111p:plain

試しにIstioでルーティングを変えてみる

Istioの代表的な機能である、Traffic Managementを試してみる。 今回は試しにreviewsサービスのv1に50%、v3に50%の割合でルーティングされるようにしてみる。

kubectl apply -f samples/bookinfo/networking/virtual-service-reviews-50-v3.yaml

何度かリロードするとReviewの部分が切り替わることが分かる。 各クラスタごとにIstioのコントロールプレーンを持っているので、 マルチクラスタによって冗長化しているケースはどちらにもapplyしてあげる必要がある。 (v3は赤い星、v1は星無しで出る) f:id:taisho6339:20200508212505p:plain

まとめ

Anthosを活用することでGKEに関しては、 マルチクラスタ構成による冗長化、ManagedなIstioの導入を組み合わせてかんたんに構築できることが分かった。 まだまだAnthosに関しては発展途上で情報も全然ない印象ではあるが、機能としては便利なので今後に期待したい。

参考

https://speakerdeck.com/jukuwa/gcpug-tokyo-istio-1-dot-5-day-anthos-service-mesh

【GCP Anthos】 Regionに跨って冗長化したKubernetsのマルチクラスタをロードバランシングする

Anthosとは?

本記事の目的

本記事ではマルチクラスタを構築し、Anthosの機能を使ってクラスタにまたがって負荷分散することができるかに焦点をあてる。 GKE HubのIngress For Anthosという機能を用い実現する。 本当はマルチクラウドクラスタで負荷分散させたかったが、GKE以外のクラスタはまだサポートされていないようだ。

Anthosへのクラスタ登録とクラスタモニタリングの仕組み

Anthosはクラスタ管理のため、各クラスタ内にGKE Connector Agentをデプロイする。 このAgentがAnthosに情報を集約し、Anthosのコントロールプレーンからの命令を各クラスタで実行することになる。 AgentがAnthosの命令を実行する流れ 引用: https://cloud.google.com/anthos/multicluster-management/connect/security-features

Ingress For Anthosのアーキテクチャ

Anthosによるクラスタ間のロードバランシングは、Ingress For Anthosという仕組みを用いて実現する。 これは下記イメージのようなアーキテクチャになっている。

アーキテクチャ 引用: https://cloud.google.com/kubernetes-engine/docs/concepts/ingress-for-anthos

登場人物について

  • Config Cluster

    • Ingress For Anthosの設定リソースを配置するクラスタ
    • アプリケーション用のクラスタと一緒にしてもいいが、Config Clusterの障害がサービス影響を出さないためにも分離しておいたほうが良い
  • Anthos Member Cluster

  • Anthos Ingress Controller

    • クラスタの外でGoogleマネージドで稼働しているグローバルなIngress Controller
    • Config Clusterを監視し、リソース作成、変更のタイミングでクラスタ間ロードバランシングの設定を行う
  • MultiClusterService(MCS)

    • クラスタに作成されるServiceのテンプレートであり、MultiClusterIngressから参照するServiceの論理的な単位となる
      • MultiClusterIngressはこのMultiClusterServiceを指定してルーティングする
    • このリソースが作成されると、各クラスタにテンプレートから生成されたServiceが作成される
  • MultiClusterIngress(MCI)

    • このリソースが作成されると、GCLBとZone NEG(MCSによって各クラスタに作成されたServiceに紐づく)が作られ、GCLBのバックエンドにはNEGが紐付けられる

動作イメージ

  • MultiClusterIngressがConfig Clusterに作成されたタイミングで、Anthos Ingres Controllerが単一のGCLBを作成し、それぞれのクラスタに紐づくZone NEGをバックエンドとしてぶら下げる
    • このNEGは、MultiClusterServiceを元に各クラスタに作成されたServiceにルーティングされる
    • 一つのNEG(クラスタ)へのアクセスが正常でない場合、すぐに別のNEGへfallbackされる

Config Clusterの可用性について

当然疑問に上がってくるのは、Config Clusterの可用性はどう担保するのか?というところ。 ちなみにConfig Clusterが死ぬとMultiClusterIngressとMultiCluserServiceの設定変更ができなくなる。 つまりすでに適用した設定でのロードバランシングには影響しないが、なるべくRegionalなクラスタで運用するようなどしたほうが良い。 ただ、個人的にはサービスダウンに直結するわけではない & MultiClusterServiceとMultiClusterIngressを頻繁にメンテすることはあんまりなさそうなのでそこまでガチな可用性はなくても良さそうに感じている。 (ここにマルチクラスタ冗長化とかあんまやりたくはない...)

実際に動かしてみる

セットアップ

GKEクラスタをAnthosのクラスタメンバーに登録

gcloud container hub memberships register  gke-anthos-asia    \
   --project=[project_name] \
   --gke-cluster=asia-northeast1-a/gke-anthos-asia \
   --service-account-key-file=./service-account.json
gcloud container hub memberships register gke-anthos-eu   \
   --project=[project_name] \
   --gke-cluster=europe-north1-a/gke-anthos-eu \
   --service-account-key-file=./service-account.json

ConfigクラスタをAnthosに設定

gcloud alpha container hub ingress enable \
  --config-membership=projects/[project_name]/locations/global/memberships/gke-anthos-config

アプリケーションのデプロイ

結果

  • クラスタがAnthosのメンバーとして認識された様子(本記事の手順では割愛しているが、EKSも検証のため入れてみた) f:id:taisho6339:20200506210718p:plain

  • ロードバランサーのバックエンドに自動的に作成されたNEGが紐付いている f:id:taisho6339:20200506210647p:plain

  • 中身はクラスタのMultiClusterServiceによって作成されたHeadlessServiceから取得されたPodのIPに紐付いている f:id:taisho6339:20200506210714p:plain

  • 試しにcurlしてみると、GCLBがAnyCastで最寄りのクラスタ(ネットワーク的に)にルーティングしてくれるため、常に東京クラスタの結果が帰ってくる

❯❯❯ curl 34.107.232.154/ping
{"Hostname":"34.107.232.154","Version":"1.0","GCPZone":"asia-northeast1-a","Backend":"zone-ingress-5f6ff94966-2phdz"}%

試しにAsiaクラスタを殺してみる

  • deploymentを殺し、Podが存在しないようにする
❯❯❯ kubectl delete -f deployment.yaml
deployment.apps "zone-ingress" deleted
❯❯❯ curl 34.107.232.154/ping
{"Hostname":"34.107.232.154","Version":"1.0","GCPZone":"europe-north1-a","Backend":"zone-ingress-5f6ff94966-7hlt7"}%

まとめ

GKEのクラスタであればかんたんにロードバランシングできるので、 クラスタ自体のアップデートをしたいが、ダウンタイムを許容したくないときやRegion障害などに活用できそうだということがわかった。 EKSなどのサポートも進めばよりいっそう便利なので対応を待望しています。

参考