この記事は @hatappi によって提供される Developer Productivity Engineering Camp blog series の1つです。
序論
私達 Microservices Network team の管理するコンポーネントの1つに Istio があります。
メルカリでは2017年から既存のシステムを Microservices におきかえるプロジェクトが始まり、Microservice の数は今も増え続けています。Microservice の数が増えるにつれて起きる問題に対処するために Service Mesh の実装の1つである Istio を私達は導入しました。
この記事ではどのような問題があり、それを Istio がどのように解決するのかを紹介します。
Microservice Architecture におけるネットワークの課題
Microservice Architecture を採用した場合、Monolithic Architecture を採用した時と比較して解決しなければならないネットワークの課題があります。実際にその例を見ていきましょう。
Monolithic Architecture を採用して下記の図のようなウェブアプリケーションを作成したとします。ユーザーから受け取ったリクエストは Application が受け取り、 datastore から必要な情報を取り出し、ユーザーへレスポンスを返します。datastoreへのアクセスはありますが、それ以外の処理はすべて Application が行います。
一方、Microservice Architecture を採用した場合は下記の図のようにリクエストに対して複数のサービスがネットワークを介して通信を行いレスポンスを返します。そのため1つのアプリケーションでは考慮する必要がなかったようなサービス間の Discovery や Timeout、Retry のような Reliabilityを考慮する必要があります。サービスが数個であれば個別に対応することが可能ですが、サービスが数百、数千と増えるにつれてより複雑性が増すため、難易度も高くなります。
各サービスの開発者はビジネスロジックの実装に集中し、Microservice Architecture 固有の問題は別のレイヤーやコンポーネントによって解決されるのが理想です。これを実現する方法の1つが Servie Mesh です。
Service Mesh とは何か?
Service Mesh は一言でいうと「サービス間の通信を担う専用のインフラストラクチャーのレイヤー」です。Service Mesh ではサービスの Sidecar として Proxy をデプロイして、サービス間のすべての通信を Proxy を介して行います。そして Proxy が Service Discovery や Security、Observability などの機能を提供するため、サービス自体にこれらの機能を実装することなく透過的に使用することができます。
この Service Mesh を実現するための実装の1つが、私達が採用している Istio です。
Istio とは何か?
Istio は data plane と control plane という2つのコンポーネントから構成されます。
data plane では Sidecar pattern を用いてアプリケーションと一緒に Envoy Proxy のコンテナをデプロイします。そしてこの Envoy proxy がアプリケーションのすべてのトラフィックをインターセプトすることでサービス間の通信における機能を提供します。Envoy Proxy が入力のトラフィックをどのようにインターセプトするか興味がある方は私が以前書いた記事をご覧ください。
control plane では設定を管理し、data plane からメトリクスを受け取りながら Envoy proxy が提供する API を介して動的に設定を変更します。つまり data plane はサーバーを再起動することなく処理を変更することができます。
Istio が解決する課題
ここからは Isito が解決する課題をいくつか見ていきましょう。ここに書いたのは Istio が解決する課題の一部です。もっと詳しく知りたい方は Istio の公式ドキュメントを参照してください。
Istio / Concepts
Traffic Control
A/B テストや重み付けルーティングのような柔軟なルーティングを実現したい場合、 Service resource のみを使って行うには限界があります。例えば A/B テストの実現を考えた時、実現方法の1つとしてまず各バージョンのアプリケーションを Pod として起動して、各 Pod 用の Service resource を作成、呼び出し元でそれぞれの Service resource を登録して A/B テストを行う方法が考えられます。しかし、この方法の場合 A/B テストを変更するためにはすべての呼び出し元を変更する必要があるため、サービスの数が数百のように多い Microservice Architecture では現実的ではありません。
Istio は data plane (Envoy proxy) によって提供される API を介して動的にルーティング情報を変更することができるため、柔軟な Service Discovery を実現することができます。例えば、 version A には 70%、version B には 30% のリクエストを振り分けたい場合は下記のような Istio の VirtualService を定義します。この定義は control plane を介して Envoy proxy に渡されます。そして Envoy proxy が比率に応じてリクエスト先を切り替えるため、呼び出し元はリクエスト先をどのように切り替えるかを意識する必要がありません。
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
...
spec:
hosts:
- http-server
http:
- route:
- destination:
host: http-server
subset: version-a
weight: 70
- destination:
host: http-server
subset: version-b
weight: 30
Reliability
例として、各サービスが下記の図のように依存していたとします。
このサービスにおいてReliability の観点から起きる問題はなんでしょうか?
例えば Service – D がなんらかの理由によりレスポンスが遅くなったとします。その場合、呼び出し元の Service – C のレスポンスも遅くなり、さらに Service – A、Service – B へと伝搬していきます。この現象は Cascading failure です。これを解消する手段の1つは Service – D にサーキットブレーカーを実装することです。これにより Service – C から Service – D へのリクエストは遮断され、障害の伝搬を防ぐことができます。
次は Service – A が実装ミスなどにより大量のリクエストを Service – C に発行した場合を想定してみます。この時 Service – C は想定以上のリクエストを受け取るため、処理できなくなるかもしれません。これを防ぐために Rate Limit の実装を検討し、サービスが想定しない量のリクエストからサービスを守ることができます。
他にも呼び出し元として適切なタイムアウトやリトライの実装も検討することも Reliability の観点からは重要です。
さて、これらすべてのケースへの対応をアプリケーションのコードに組み込みますか?もちろんそれは技術的には可能だと思います。しかし数百のサービスに対して同じように実装するのは現実的ではありません。また別の方法としてライブラリを作成し、アプリケーションがそのライブラリを使うことで、サービスごとに0から実装することを回避できます。しかし、ここでもサービスが使用する言語ごとに実装が必要になります。またライブラリを修正した場合はすべてのサービスでアップデートを行う必要があります。
Isto を使った場合、 data plane となる Envoy proxy がこれらの機能を提供します。そのためアプリケーションが使用する言語やフレームワークに依存せずにこれらの機能を使うことができます。また各機能の変更は data plane と control plane で API を介して行われるのでアプリケーション自体のアップデートは必要ありません。
Security
通常外部にサービスを公開する時は HTTPS としてエンドポイントを使用します。一方 Kubernetes クラスター内のサービス間の通信はどうでしょうか? HTTP で通信をしている場合、例えばライブラリに脆弱性があり、攻撃者がクラスター内に侵入した場合などにサービス間の通信を盗み見ることが可能になります。これを防ぐために内部のサービス間の通信であっても HTTPS で提供することが望ましいです。
サービス間の HTTPS での通信を実現するためには、各サービスで証明書や秘密鍵を管理する必要があります。サービスごとに管理した場合の問題として、証明書が更新されないなどの間違った設定よって HTTPS 通信が正常に行えない可能性があります。そのため、各サービスで個別に管理するのは現実的ではありません。
Istio では Mutual TLS authentication (a.k.a mTLS) をサポートしており、data plane と control plane 間で TLS 証明書や鍵の受け渡しが行われます。そしてサービス間の通信では data plane がリクエストを透過的に TLS 暗号化します。そのためアプリケーションは今まで通り HTTP でのリクエストを使用し続けることができます。
またセキュリティの別の観点としてアクセス制御があります。
例えば Service – A は Service – B からのリクエストしか許可しないようにする時、Kubernetes の場合は NetworkPolicy によって実現することができます。NetworkPolicy は OSI の Layer 3,4 でのアクセス制御になります。そのため Layer 7 のパス制御 (Service – A が /foo は Service – B からアクセスを許可して、/bar は Service – C から許可する) などはNetworkPolicy のみでは実現できません。
Istio では AuthorizationPolicy を使用して下記のように実現できます。また mTLS と併用することで特定のサービスアカウントを持つ Pod からのリクエストのみを許可するなど、より柔軟な制御も実現できます。
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
...
spec:
action: ALLOW
rules:
- from:
- source:
namespaces: ["service-b"]
to:
- operation:
methods: ["GET"]
paths: ["/foo"]
- from:
- source:
namespaces: ["service-c"]
to:
- operation:
methods: ["GET"]
paths: ["/bar"]
メルカリで直近取り組んでいる課題
gRPC Load Balancing
メルカリではサービス間の通信は基本的に gRPC が使われます。gRPC には HTTP/2 が使用されますが、現在Kubernetes は Service resource (L4 Load Balancer) しか提供していないため、HTTP/2 で通信を行う gRPC は Load Balancing ができません。解決策の1つとして Headless Service を使用して Client 側で 宛先となる Pod の IP address を管理し、Client-side Load Balancing を行います。
メルカリでは基本的にサービスは Go で作成されており、Client-side Load Balancing を行うパッケージが社内で提供されているため、サービスはそれを使用します。しかしこの方法には2つの課題があります。
- アプリケーションを開発する際、このライブラリの導入を忘れないようにしなければなりません。もし導入を忘れた場合、うまくリクエストが分散されず、思わぬ障害につながる可能性があります。
- Go 以外で作成されているサービスへの対応の検討が必要です。サービスの開発者が Go 以外を選択した場合、彼らは自ら Client Load Balancing を行う方法を検討する必要があります。
特に後者に関しては Microservice Architecture における Polyglot Programming を意識していく上では重要なポイントです。
Istio は HTTP/2 の LoadBalancing に対応しているため、サービスは Istio を使用するだけで gRPC Load Balancing を実現できます。また Load Balancing は Envoy proxy 上で行われるので、サービスがどの言語を使用しているかに依存しません。
Istio Ingress Gateway
メルカリでは Microservices への移行に API Gateway を使用した Strangler Fig pattern を採用しています。この API Gateway は Go によって実装されており、開発者がサービスを公開したい時はそのサービスのエンドポイント情報を、API Gateway に PR を作成して追加します。マイクロサービスの数が増えるにつれて、エンドポイントの追加や変更に伴うレビューの負荷、およびリリースにともなう負荷が高くなります。
理想としては、ルーティング情報は各サービスがオーナーシップをもっており、実際にルーティングを行うアプリケーションやロギングや認証などの共通機能の管理は私達ネットワークチームが行えると良いです。これにより各サービスはエンドポイントに関する変更を、いつでも自分たちのタイミングで行うことができるようになります。
この解決策の1つとして、現在 Istio Ingress Gateway の導入を検討しています。Istio Ingress Gateway は実体としては Envoy Proxy です。アプリケーションと一緒にデプロイされる Envoy proxy と同じcontrol plane となる Istiod と、 API を介してコミュニケーションを行い設定を更新します。どのようなエンドポイントを公開してどのサービスにリクエストをプロキシするかの情報は Istio の CRD である Gateway, VirtualService を使って行います。Istiod はこれらの CRD から情報を収集して、Istio Ingress Gateway へ渡します。
また各リソースは各サービスの namespace に配置できるため、下記の図のようにエンドポイント、ルーティング情報は各サービスのオーナーが管理して、Pod やロギングや認証などの共通機能の管理は私達ネットワークチームが行うことができるため、責務の分離を行うことができます。
Istio を使った機能の提供
私達は Istio の機能を組み合わせてより抽象化された機能も作成しています。
例えば Istio の Traffic Management を使用した Canary Release です。Istio は VirtualService を使うことで重み付けされたルーティングが可能なため、DestinationRule と組み合わせて徐々に新しいバージョンのアプリケーションにトラフィックを移すことができます。
他にも Istio の Traffic Management を使用した PR 環境の提供も行っています。これは来週公開される記事に詳細が書かれるので、興味がある方はチェックしてみてください。
Istio を意識せず Istio を使う
ここまで Istio が解決する課題を紹介してきました。しかし、実際にこれらを解決するためには VirtualService や Gateway など Istio の CRD を定義する必要があります。つまり開発者は Istio の知識をもっている必要があります。これは理想的な状態でしょうか?理想的な状態は基本的なユースケースにおいて Istio を意識せずに使用できることです。
例えば開発者はリトライを実現したい時に Istio の VirtualService をゼロから作成するのではなく、リトライをx回するという宣言のみを行うようにしたいです。 これらを実現するために現在私達は今回のブログシリーズでも公開された CUE の使用を検討しています。
Kubernetes Configuration Management with CUE | Mercari Engineering
CUE を使うことにより開発者は Istio を意識せずとも Istio の機能が使えるようになると考えています。
まとめ
この記事では Istio がどういった問題を解決するのかを見てきました。
ネットワークチームでは Istio だけでなく CDN や DataCenter など north-south, east-west に関する幅広いコンポーネントを扱っています。これらを使ってメルカリ内でおきる問題をどのように解決するかを考えて実行するのは楽しいと思うので、興味がある方はぜひ下記のリンクから応募してください!