※本記事は2022年2月18日に公開された記事の翻訳版です。
この記事は、「Developer Productivity Engineering Camp」ブログシリーズの一環として、Dynamic Service Routingと呼ばれる、優れた機能の1つを紹介します。この機能を使用すると、各マイクロサービスの異なるバージョン間でトラフィックをルーティングできます。それでは実際に仕組みを見てみましょう。
はじめに
メルカリでは、各マイクロサービスが特定のビジネスロジックに注力するマイクロサービスアーキテクチャを使用しています。これらのマイクロサービスはすべてコンテナ化され、Kubernetesクラスタ上にデプロイされます。また各マイクロサービスはスタンドアロンサービスではなく、すべて依存関係があり、それぞれが他のマイクロサービスを呼び出す必要があります。たとえば、認証サービスを呼び出してユーザーを認証するといった単純なものから、より複雑な依存関係のものまで多数あります。
上の図では、Microservice-AからMicroservice-Bにトラフィックを送信していますが、このときMicroservice-BのServiceのアドレスはMicroservice-AのDeployment内で指定されています。また複数の依存関係がある場合、それら全てのアップストリームサービスのエンドポイントをDeployment内で指定することで、トラフィックを送信できるようにします。
Dynamic Service Routing
「Dynamic Service Routing」は、その名前が示すとおり、マイクロサービス間のトラフィックを動的にルーティングする機能です。モノリスアーキテクチャでは、ユーザーからのあらゆるリクエストを処理するための全ビジネスロジックを備えた単一の巨大なサービス構造を持ちますが、マイクロサービスアーキテクチャの場合、特定のビジネスロジックに焦点を当てた複数のサービスで構成されます。つまり、単一ユーザーからのリクエストが、依存先のサービスによって複数のリクエストになる可能性があるのです。
上の図では、Microservice-Aへの単一のユーザーリクエストが、Microservice-B、Microservice-C、Microservice-Dに送信された後、最終的にレスポンスがユーザーに送信されます。
ここで、Microservice-BにUser1向けの新機能を導入するために、開発とテストが必要だとします。このままMicroservice-Bを拡張して新たにデプロイした場合、トラフィックルーティングは以前と同じになります。
問題なさそうに見えますよね?実は、これでは次のような問題に直面することになります。
- 別のチームがUser2向けの機能に取り組んでいるとします。新しいMicroservice-Bにバグがあると、User2のリクエストR2も失敗することになります。 User2もそのリクエストであるR2も、無関係なサービスの変更が原因で影響を受けてはいけません。変更をマスターに反映する前に、リクエストR1でのみ使用できる、異なるバージョンのMicroservice-Bが必要です。
では、上記の問題をどうやって解決するのでしょうか。
1つの方法は、各マイクロサービスに対して複数のインスタンスを設けることです。たとえばMicroservice-AがMicroservice-Bに依存しているとしましょう。その場合、Microservice-Bに変更を加えるたびに、まずMicroservice-AとMicroservice-Bの個別のインスタンスを作成して、Microservice-Bに加えた変更で何も影響がないことをテストします。こうすれば、他のサービスに影響を与えることなく変更をテストできます。
上の図が示すように、いくつかある依存サービスを複製し、Microservice-Bの動作を確認します。確認が完了したら、元のMicroservice-Bに更新したバージョンを置き換えて、複製したものを削除します。
開発者は、ダウンストリーム(呼び出し元)マイクロサービスを複製して変更を確認できますが、ここでQAメンバーについて考えてみましょう。QAメンバーは、変更があるたびにワークフロー全体をテストする必要があるため、単一のマイクロサービスを複製してテストすることはできません。したがって、上記のシナリオのMicroservice-Bに依存するマイクロサービスが他にもある場合は、それらもすべて複製する必要があります。
メルカリには、300を超えるマイクロサービスがあり、それと並行して数百ものサービスの開発とテストをおこなっているため、すべての機能開発に対して依存マイクロサービスを複製することはできません。
上記のような問題を解決するために開発したのが、Dynamic Service Routing(DSR)と呼ばれる社内機能です。DSRを使用すると、トラフィックの宛先を動的に切り替えて、任意の機能をテストできます。以下に示すように、変更対象となるサービスを複製するだけで、その依存先サービスを複製する必要はありません。
ここではUser1からのリクエストのみが複製されたサービスMicroservice-B-v2に送信され、残りのトラフィックは以前と同じようにMicroservice-Bに送信されます。
技術的な詳細
このセクションでは、Dynamic Service Routingがどのような仕組みで複数の異なるバージョンのマイクロサービス間でトラフィックをルーティングするかについて説明します。
Dynamic Service Routingは、ヘッダーを使用したIstioのTraffic Shiftingに基づく機能です。以下でIstioのTraffic Shiftingの仕組みについて簡単に説明しますが、詳細についてはIstioのTraffic Shiftingの公式ドキュメントを参照してください。
Dynamic Service Routingは、主に次のリソースを使用して、複数のDeploymentバージョン間でのトラフィックをルーティングします。
- Kubernetes Services (各Deploymentバージョンを対象とした個別のService)
- Deployment (複数のDeploymentバージョン)
- Istio VirtualService 特定のKubernetesサービスにリクエストをルーティングする)
VirtualServiceの構成は次のようになります;
上記のVirtualServiceには、httpブロックの下に2つのルートがあります。1つ目はヘッダーベースでのルーティングで、{HEADER_NAME}ヘッダーが{HEADER_VALUE}に等しいリクエストを照合し、リクエストを特定のホストにルーティングします。2つ目は通常ルートで、何も照合せず、リクエストを別のホストに直接ルーティングします。
構成例
reviews namespaceにreviewsというServiceがあるとします。開発者が2つの異なる機能を開発しており、元のreviews Serviceに影響を与えずにテストしたいと考えた場合、次のようなリソースを作成する必要があります。
For original service, we already have:
- Kubernetes service called reviews
- Kubernetes deployment called reviews
For feature-1, we will have:
- Kubernetes service called reviews-v1
- Kubernetes deployment called reviews-v1
For feature-2, we will have:
- Kubernetes service called reviews-v2
- Kubernetes deployment called reviews-v2
最後に、次の構成を持つIstio VirtualServiceが、ヘッダーに基づいて特定のKubernetes Serviceにリクエストをルーティングします。
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: reviews
namespace: reviews
spec:
hosts:
- reviews.reviews.svc.cluster.local
http:
- match:
- headers:
feature:
exact: v1
name: feature-v1
route:
- destination:
host: reviews-v1.reviews.svc.cluster.local
- match:
- headers:
feature:
exact: v2
name: feature-v2
route:
- destination:
host: reviews-v2.reviews.svc.cluster.local
- name: default
route:
- destination:
host: reviews.reviews.svc.cluster.local
メルカリでのトラフィックルーティング
メルカリには、開発者チームとQAチーム向けの共通のKubernetesクラスタがあり、マイクロサービスを本番環境にデプロイする前のすべてのテストがここで実行されます。ここではトラフィックの種類とサービスレイアウトの観点から、Dynamic Service Routingを使用して開発者チームとQAチームがどのようにテストを容易にしているかを示します。
上のトラフィックフローの図を簡単に説明すると、以下のような構成になっています;
- SREチームが元のサービスを監視
- QAチームはsvcAで導入された新機能のテストを実施
- DEVチームがsvcBの新機能を開発
Dynamic Service Routing機能の導入以前、他のユーザーやサービスに問題を引き起こすことなく変更対象サービスをテストするためには、ユーザーごとにGatewayサービスのPodを複製する必要がありました。これがDynamic Service Routing機能の導入によって、変更対象サービスのみを複製すれば良くなり、構成およびリソースのコストを大幅に節約できるようになりました。
技術的な詳細のセクションで説明したように、変更対象となる機能ごとにKubernetes ServiceとDeploymentを作成すると同時に、VirtualServiceリソースも構成する必要があります。これらを実行するための自動化ツールとして以下の2つを利用しています。
- プルリクエストベースのレプリケーションコントローラ
- サービスルーター
プルリクエストベースのレプリケーションコントローラ
このツールは、開発者がmasterブランチにプルリクエストを送信するたびに、特定のマイクロサービスのKubernetes ServiceとDeploymentを作成します。上のトラフィックフローの図では、svcAとsvcBの両方にsvcA-pr1とsvcB-pr2というレプリカがあることがわかります。すべてのレプリカのサフィックスにはPR番号が含まれています。
サービスルーター
サービスルーターは「プルリクエストベースのレプリケーションコントローラ」に沿って動作し、VirtualServiceリソースを設定するツール(Kubernetesコントローラ)です。機能として、プルリクエストベースのServiceとDeploymentが作成されるたびに、既存のVirtualServiceリソースに次の設定ブロックを追加するという非常に単純なジョブを実行します。
- match:
- headers:
service-router-{MICROSERVICE_NAME}-{NAMESPACE}:
exact: {NEWLY_CREATED_KUBERNETES_SERVICE_NAME}.{NAMESPACE}
name: {PR_NUMBER}
route:
- destination:
host: {NEWLY_CREATED_KUBERNETES_SERVICE_NAME}.{NAMESPACE}.svc.cluster.local
以下はVirtualServiceの設定例です。
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: svcA
namespace: {NAMESPACE}
spec:
hosts:
- svcA.{NAMESPACE}.svc.cluster.local
http:
- match:
- headers:
service-router-svcA-{NAMESPACE}:
exact: svcA-pr1.{NAMESPACE}
name: pr1
route:
- destination:
host: svcA-pr1.{NAMESPACE}.svc.cluster.local
- name: default
route:
- destination:
host: svcA.{NAMESPACE}.svc.cluster.local
ヘッダーベースのトラフィックが特定のPRベースのサービスにルーティングされ、ヘッダーなしのトラフィックがデフォルトの安定したサービスに送信されることがわかります。これにより、SREチームは問題なく元のサービスを監視し、QAチームは新機能のテストを実行すると同時に、DEVチームが他のチームに影響を与えることなく新機能の開発とテストをおこなうことができます。
Dynamic Service Routingのメリット
上記のように、マイクロサービスの開発とテストにおいてDSRが開発者とQAに役立つ点は多々あります。
- シンプルなセットアップ:単一のPRベースのレプリカセットをデプロイするだけで済むため、すべてのダウンストリーム(呼び出し元)サービスで複数バージョンをデプロイする必要はない
- 新機能のテストにかかる時間を短縮し、迅速にリリースできる
- テスト環境のプロビジョニングにおける複雑さが軽減される
- アプリケーションのソースコードを変更する必要がない
- リクエストを動的にルーティングすることでバグを簡単に再現できる
まとめ
ここでは、開発チームとQAチームがマイクロサービス間でトラフィックを動的にルーティングするのに役立つDSR(Dynamic Service Routing)についてご紹介しました。
これは、Istioに基づく優れた機能の1つですが、私たちNetworkチームは他にも数多くの機能を駆使してサービス運営に取り組んでいます。この記事に興味を持ち、開発者を応援するためにより優れた機能を開発したいと思われた方は、ぜひチームにご参加ください。