本番環境のマルチテナント Kubernetes クラスタへの Istio 導入

Hiroki Tanaka
google-cloud-jp
Published in
20 min readOct 7, 2019

--

※ この投稿は Mediumに投稿された、Adopting Istio for a multi-tenant kubernetes cluster in Production の日本語訳です。著者の方の許可を得て翻訳しています。

これは Mercari Bold Challenge Month の3番目の記事です。

Mercari ではモノリスなサービスからマイクロサービスのアーキテクチャへと移行を行っている間、長期的な観点からみて、サービスメッシュの導入とその重要性を理解することが必要だと感じていました。ほとんどのインシデントレポートに対する現実的な対策としてあがるのが、レートリミットの導入、適切なカナリアリリースのフローの導入、適切なネットワークポリシーの導入などでした。そしてこれらこそがサービスメッシュによってもたらされる機能です。

前四半期では、私達はついに Istio の導入に挑戦することに決め、調査を開始しました。結果として、100 以上のマイクロサービスをホストするマルチテナント環境のシングル Kubernetes クラスタを深刻な障害を発生させずに本番運用を行うことができています。この記事では Mercari の Istio 導入の際の戦略と、導入工程の概要について説明します。この記事の読者は、サービスメッシュと Istio 自体については基本的な理解がある前提です。Istio の公式ドキュメントはその理解を得る上で優れた資料です。

Istio 導入のきっかけ

Mercari のビジネスとエンジニアの数の急成長から、私達のモノリスなサービスがスケーラブルなものではなくなっていたため、マイクロサービスへのアーキテクチャへの移行を決断したのが2017年のことです。マイクロサービスアーキテクチャでは、モノリスでは起きなかったネットワーク的な問題に直面しました。ロードバランシング、トラフィックコントロール、可観測性、セキュリティなどです。可観測性においては、トレーシング、ロギング、そしてメトリックス収集を Datadog を使って集約された基盤で構築できていましたが、トラフィックコントロールと、セキュリティについてはまだ基礎的な状態でした。マイクロサービスアーキテクチャを採用する主な理由の1つは、さまざまなアプリケーションコンポーネントを疎結合することでしたが、サービス間の最新のトラフィック制御ポリシーがないため、依然として連鎖的な障害に直面しています。サーキットブレーカーレート制限を導入すると、これらの連鎖的な障害を解決できる可能性があります。

もう1つのネットワーク関連の大きな問題は、gRPC ロードバランシングです。 Mercari ではマイクロサービス間通信には gRPC を使用しています。Kubernetes で gRPC を使用している場合はかならず gRPC ロードバランシングの問題に直面します。この問題を簡単に言うと、 gRPC は HTTP2 に基づいており、多重化を使用して RPC 呼び出しを送信しますが、 Kubernetes サービスは L4 レイヤーで動作し、 HTTP2 のロードバランシングができません。この問題の一般的な回避策は、クライアント側の負荷分散を使用することです。この方法ではアプリケーション側にインフラストラクチャと密結合する負荷分散ロジックを追加する必要があります。しかしこれは私達が実施したくない回避策です。envoy proxy のビジョン「ネットワークはアプリケーションに対して透過的である必要がある」と同じビジョンを、我々も持つためです。当社のマイクロサービスエコシステムはすでに様々なプログラム言語で多言語化されており、各言語のインフラストラクチャ固有のライブラリを維持し、それらを使用してサービスを更新し続けることは、開発者が望むことではありません。

サービスメッシュは、アプリケーションコードを変更せずにプロキシサイドカーを各アプリケーションと共に実行することで、これらの問題を解決するのに役立ちます。

なぜ Istio か?

Istio サービスメッシュは、モダンなマイクロサービスアーキテクチャのすべてのネットワーク関連の問題に対して、接続、セキュリティ、制御、監視という単一のソリューションを提供します。 Istio は envoy proxy をサイドカーとして使用して、アプリケーションのソースコードの変更なしに、これらのソリューションを透過的に提供します。Istio はオープンソースであり、 Google がバックアップしています。

Istio がプロダクションレディとして発表されてから1年が経ちましたが、本番環境での成功事例を見ることは多くはありません。これは、Istio を確実にセットアップするために求められる最初の構築手順が原因であると考えています。しかし、一度正しくセットアップされると、開発者がすでに慣れている Kubernetes ネイティブなユーザーエクスペリエンスを提供する CRD があるため、開発者が Isotio を使用しはじめるためのランニングコストは低くなります。こうした背景から、私たちは Istio に時間を投資し始めました。

Mercari microservices architecture and cluster ownership model

Istio の話の前に、Mercari のマイクロサービスアーキテクチャと Kubernetes クラスタのオーナーシップの設計についてご紹介します。

Mercari のすべてのステートレスなワークロードは、GCP のマネージド Kubernetes サービス(GKE)を使っています。クライアントからのすべてのリクエストは API ゲートウェイを通して各マイクロサービスにルートされます。まだマイクロサービス化されていないエンドポイントに対してはゲートウェイは単純に Proxy として動き、リクエストをオンプレで動いているモノリス上のバックエンドにリクエストをルートします。この方法をつかって Mercari では徐々にモノリスからマイクロサービスへと移行を実施しています。エンドポイントに応じて各サービスはゲートウェイと HTTPも しくは gRPC を使って通信し、内部の通信はすべて gRPC を使っています。

クラスタの管理コストを下げるために、Mercari ではマルチテナントでクラスタを運用しています。マルチテナントクラスタとは複数のユーザーやチームでクラスタを共同で運用する方法です。Mercari では各サービスがそれぞれネームスペースをもち、各サービスのチームメンバーはネームスペースに対するフルアクセスの権限を持ちます。筆者が所属する Microservices Platform Team と呼ばれるチームが、Mercari の中でクラスタ自体の運用と、ネームスペース, kyube-system, istio-system のなどの Kubernetes コンポーネントの管理を行っています。この方法を取ることでそれぞれのチームの責任が、それぞれのフォーカスするエリアに基づいてはっきり定義されます。サービスに焦点を当てたマイクロサービスバックエンドチームがマイクロサービスのネームスペースを管理し、インフラストラクチャ全体に焦点を当てたプラットフォームチームがクラスタを管理します。クラスタ全体の機能とその信頼性の導入は、プラットフォームチームの責任です。このスライドからアーキテクチャの詳細を確認することができます。

クラスタ運営に関する数字

  • API Gateway はピーク時に4M RPMを受け取ります
  • 100以上のマイクロサービス(100以上のネームスペース)
  • 200人以上の開発者が一部のネームスペースに直接アクセスできます
  • Mercari、Merpay、内部サービス — すべて同じクラスタで実行

これらの数字からわかるように、このクラスタは私たちにとって非常に重要です。 もっとも高い SLO を持ちます。クラスタ全体に影響を与える可能性のあるものを導入するときは、非常に注意する必要があります。 ですから Istio の信頼性を確保することが最重要課題になります。また、マルチテナントクラスタでは、1つのチームがいくつかの構成ミスを犯す可能性があります。 Istio のセットアップは、これらのミスから「十分に保護」される必要があります。それは1つのサービスまたはネームスペースの設定ミスが他のサービスに影響を与えることがない状態です。そうした状態を保てないと Istio の導入により悪影響を与える可能性があります!これについては後で詳しく説明します。

Istio 導入戦略

簡単に言えば、私たちの戦略は次のとおりです。

一度に1つずつのことを行う!

つまり、一度に1つずつの機能の導入です。一度に1つのネームスペースに Istio を導入していきます。

Istio’s feature selection

Istio の使命は、マイクロサービスの世界におけるすべてのネットワークの問題、つまりトラフィック制御、可観測性、セキュリティに対する単一のソリューションになることです。そのため Istio には様々な機能があります。前回 Istio に関連する CRD の数を数えたとき、約53ありました。各機能には unknown unknowns のものがいくつかあります。たとえば、突然、一部のサービスのイングレスリクエストは、アウトバウンドポート名の競合のために機能しなくなりました。機能を制限することにより、これらの unknown unknowns の範囲を最初に絞り込むことが必要です。幸いなことに、Istio のコンポーネントは、機能要件に基づいて必要なコンポーネントのみをインストールする必要があるように設計されています。すべてのコンポーネントをインストールする必要はありません。

私達は次の3つの大きな機能カテゴリ:トラフィック制御、可観測性、およびセキュリティのうち、最初にトラフィック制御から始めることにしました。トラフィック制御カテゴリにも、ロードバランシング、レート制限、再試行、カナリアリリース、サーキットブレーカー、フォールトインジェクション、フォールトトレランスなどのたくさんの機能があります。実際には、これらの機能のほとんどをすぐにでも必要な状況でしたが、、最初の Istio の導入では、機能要件をロードバランシングだけに絞り込みました。理由は既に動機付けのセクションで説明されているとおり、gRPC ロードバランシングを正確に実施するためです。

ここまでで、最初の Istio 導入のゴールを次のように設定しました。

アプリケーション Pod への istio-sidecar プロキシ(envoy)を有効にし、すべてのネームスペースでサービスメッシュを徐々に拡張する。

Istio フィジビリティ調査

リリース目標を決定した後、サンドボックスクラスタでの Istio のフォジビリティのテストを開始し、実際に戦略を立てることができるかどうかを確認しました。調査アプローチと実行可能性テストの要件は次のとおりです。

  1. Istio のインストールによるクラスタの副作用が発生しないこと
  2. Istio を徐々に導入できるようにすること
    一度に1つのマイクロサービスに対して導入(ネームスペース)。これにより後方互換性と、何の変更も加えずダウンストリームサービスの正常動作を保つ。ダウンストリーム側では、サイドカーの有無は問わない。マイクロサービスのネームスペースは開発者チームによる管理のため、Istio の導入を必須にはできないので。
  3. Istio は、クラスタ内のすべてのタイプの通信(gRPC、HTTP、HTTPS)で正常に機能すること
  4. レイテンシーまたは新しいエラーで顕著なパフォーマンスの低下がないこと
  5. Istio のコントロールプレーンコンポーネントがダウンした場合、クラスタに大きな影響がないこと

これらの調査はそれほどスムーズではありませんでした。Istio 導入にブロッカーとなる多くの課題に直面しました。クラスタ SLO が低い場合には、これらの問題は大きなハードルではないかもしれませんが、実現可能性テストの要件を満たすための回避策と戦略を把握する必要がありました。

課題

以下の点が、私たちが実際に直面しなければならなかったいくつかの課題です。詳しく説明するにはそれぞれ別の記事が必要になりますが、ここではできるだけ簡単に説明します。

1. Managing istio-proxy lifecycle

Istio のサイドカーが Pod で有効になっている場合、すべてのインバウンドおよびアウトバウンドトラフィックはサイドカーコンテナを通過するので、次の点を確認することが非常に重要です。

  • サイドカーは、アプリケーションコンテナが起動する前に起動し、正常であること。そうでない場合、データベース接続などのアウトバウンドリクエストは失敗する。
  • アプリケーションコンテナは、サイドカーが終了する前に終了すること。終了しない場合、その期間中のアウトバウンドリクエストが失敗する。

残念ながら、サイドカーは Kubernetes においてファーストクラスのオブジェクト( first-class citizen )ではないため、Kubernetes でこのライフサイクルを制御する簡単な方法はありません。動作はランダムであり、どのコンテナでも最初に開始または終了できます。この問題を解決するためのプロポーザルはすでにアクセプトされていますが、、まだ進行中であり、Kubernetes でこの機能を確認するにはいくつかのリリースが必要です。この問題を解決するには、私達は次の回避策を使用しています。

確実にサイドカーの後にアプリケーションコンテナを起動する

Kind: Deployment
spec:
template:
spec:
containers:
- command: ["/bin/sh", "-c", "while true; do STATUS=$(curl -s -o /dev/null -w '%{http_code}' http://localhost:15020/healthz/ready); if [ "$STATUS" -eq 200 ]; then exec /app; break; else sleep 1; fi; done;"]

これにより、アプリケーションのコンテナプロセスは、envoy sidecarが正常になり、トラフィックを処理できる状態になった後にのみ開始されます。

確実にアプリケーションコンテナが終了した後に envoy サイドカーを終了させる

containers:
- name: istio-proxy
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "while [ $(netstat -plunt | grep tcp | grep -v envoy | wc -l | xargs) -ne 0 ]; do sleep 1; done"

これは、Istio の sidecar-injector-configmap.yaml に追加する必要があります。これにより、アプリケーションコンテナとのすべての接続が終了するまで、envoy sidecar が待機するようになります。この代替案はこの Issue から採用しました。

2. Zero downtime rolling updates

Istio のプロジェクトをモニターしていると、ローリングアップデート中におきる散発的な 503 エラーは、Istio のユーザーが持つ非常に一般的な苦情であることがわかります。 これはIstioのせいではなく、Kubernetes の 設計そのものにあります。 Kubernetes は強整合性はなく、「結果整合性」を重視しています。 Istio 自体は、通常よりも多い 503 エラーで不整合期間が少し長くなるという、複雑さを追加するだけです。この問題に対する一般的な回答は、リクエストを再試行することですが、ダウンストリームサービスで Istio が有効になっていない場合、またはサービスのべき等性が不明な場合、再試行は実行できません。

Kubernetes は、開発者が不整合の副作用を減らすために使用できる preStop などのコンテナライフサイクルフックを提供します。私達はプロトコルに基づいてアプリケーションポッドで preStop を構成しました。これを詳細に説明するには、別の記事が必要になるのでここでは構成のみを紹介します。

gRPCを使用したサービス用

apiVersion: apps/v1
Kind: Deployment
spec:
template:
spec:
terminationGracePeriodSeconds: 60
containers:
- lifecycle:
preStop:
exec:
command: ["sh", "-c", "sleep 30"]

どちらのシナリオでも、ダウンストリームで Istio が有効になっているかどうかにかかわらず、クライアント側の負荷分散が使用されます。クライアント側の負荷分散では、クライアントは接続プールを維持し、アップストリームサービスのエンドポイントが更新されたときに接続プールを更新します。このスリープにより、エンドポイントが更新されるまで、アップストリームサービスが新しい接続をリッスンしていることが保証されます。

HTTPを使用したサービス用

apiVersion: apps/v1
Kind: Deployment
spec:
template:
spec:
terminationGracePeriodSeconds: 75
containers:
- lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "wget -qO- --post-data '' localhost:15000/healthcheck/fail; sleep 30"]Kind: Deployment

ダウンストリームサービスで Istio が有効になっていれば、gRPC の場合と同様に、エンドポイントが更新されたときに Istio サイドカーが接続を更新するため、”Sleep 30” で十分です。ダウンストリームで Istio が無効で、クライアント側のロードバランシングが使用されていない HTTP リクエストの場合、アップストリームは接続を徐々に閉じる必要があります。そうしないと、クライアントは接続が閉じられたことを認識せず、リクエストを送信し続けます。Istio では、サイドカーはアプリケーションコンテナではなくクライアントとの接続を作成するため、サイドカー自体から接続を閉じる必要があります。 envoy の “healthcheck/fail” エンドポイントを呼び出すことで、ローリング更新中にアップストリームからすべての接続を強制的に閉じていくことできます。

3. Istio’s Kubernetes Service port-name convention

Kubernetes Services は L4 レイヤーで動作し、L7 レイヤープロトコルを認識しません。 Istioは、サイドカーを構成できるように、head の前に高レベルのアプリケーションプロトコルを知る必要があります。そのために、Kubernetes サービスのポート名 `<protocol> [-<suffix>]>`にプレフィックスを追加する規則を使用します。一部のサービスがこの規則に従っていない場合、競合が発生し、メッシュ内の他の多くのサービスに影響を与える可能性があります。ヘッドレスサービスがある場合、状況はさらに悪化します。

マルチテナントクラスタでは、各サービスが正しい規則に従うことは期待できません。これを解決するために、集約化したマニフェストリポジトリに、カスタムポリシー設定したYAMLバリデーターである stein を使用します。カスタム Kubernetes バリデーション アドミッション Webhook は導入中です。

調査結果

1. Istio のインストールによるクラスタの副作用が発生しないこと

Istio をインストールしただけでは、クラスタの状態に悪影響はなかった。 Istioコントロールプレーンはそのネームスペースで実行され、他のサービスとまったく同様。

2. Istio を徐々に導入できるようにすること

非常にトリッキーで、サイドカーのライフサイクル管理、ローリング更新エラー、ポート名の規則に関連するすべての回避策を考え出す必要があった。最終的にいくつかの代替案を用いることで対応が可能だった。

3. Istio は、クラスタ内のすべてのタイプの通信(gRPC、HTTP、HTTPS)で正常に機能すること

動作確認はできた。ローリングアップデートの際に発生するエラーのために、プロトコル固有の回避策が必要。

4. レイテンシーまたは新しいエラーで顕著なパフォーマンスの低下がないこと

Istioを導入する前後にレイテンシを測定たが、レイテンシの大幅な増加は見られなかった。 99パーセンタイルでは5〜10ミリ秒の範囲でした。この低レイテンシの理由の1つは、まだミキサー機能を使用していないため。

5. Istioのコントロールプレーンコンポーネントがダウンした場合、クラスタに大きな影響がないこと

現在使用している Istio コンポーネント(パイロット、サイドカーインジェクター)の回復力( Resiliency )のテストを実施したところ、これらのコンポーネントのいずれかが一時的にダウンしても、大きな影響がないことを確認した。サイドカーは、パイロットがダウンしている場合でも正常に機能するが、最新の構成を更新することはできない。これらのシナリオに対処するために必要な監視とプレイブックをすべて準備した。

結論

マルチテナントクラスタで Istio を採用するには、適切な戦略が必要です。クラスタの SLOが 高い場合はより大変です。 Istio を本番クラスタに確実に導入するには、四半期で2人のチームメンバーが必要でしたが、最終的な結果には満足しています。 gRPC の負荷分散に Istio を使用するという当初の目標は達成されており、他のトラフィックコントール機能の導入を既に作業を開始しており、徐々に本番環境に展開していく予定です。

将来的には、コントロールプレーンを GKE と同じようにフルマネージドにしたいと考えており、Istio CRDのサポートを開始したら Traffic Director を楽しみにしています。これがそもそもIstio を選択した主な理由の1つでした。

最後に、Mercari の Microservices Platform チームでの作業に興味がある場合は、採用ページをご覧ください。多くの興味深いプロジェクトが進行中です。

--

--

Hiroki Tanaka
google-cloud-jp

Customer Engineer at @GoogleCloud. Disclaimer. Opinions are my own, NOT the views of my employer. All posts here are my personal opinion.