Microservices Platform Teamの@deeeeeeetと@dragon3です.
Microservices Platform TeamではGoogle Kubernetes Engine(GKE)をメインのコンポーネントとして利用し,その上にメルカリとメルペイのMicroservicesを動かすための基盤を構築しています.メルカリのMicroservices化のプロジェクト自体は2年ほど前から始めており,GKEも当時に構築したものを今日まで運用し続けてきました.
この2年間でGKEからは多くの機能がリリースされました.その中のほとんどはそのまま有効にすることができますが,中にはClusterを作り直す必要があるものもあります.例えばRoutes-based ClusterをVPC-native Clusterに,Zonal ClusterをRegional ClusterにするにはClusterの作り直しが必要です.特にVPC-native Clusterは大きな変更で,いくつかの機能がVPC-native Clusterであることを前提に作られ始めています.
現在Microservices Platform Teamが運用するGKE cluster上には100を超えるMicroservicesが動いています.Clusterの作り直しはこれらのサービスを動かしつつ,サービス開発に影響を与えない形で行う必要があり簡単なことではありません.しかし,GKEとその周辺のGCPサービスの恩恵をさらに受けるため,そしてPlatformの今後のさらなる発展のためにGKE Clusterの作り直しを避けることはできませんでした.そのためMicroservices Platform TeamはCluster移行の意思決定をし(ネットワークの再設計を含めて)約1年をかけてClusterの移行を完了させました.
本記事ではこの移行プロジェクトの概要や進め方,詳細な移行の方法を紹介します(概要部分を@deeeetが,詳細については@dragon3が執筆しています).
どのような問題を解いたのか?
今回我々が解いた問題設定を一言で書くと以下になります.
「メルカリとメルペイ合わせて100以上のMicroservicesが動き,ピーク時で80,000 req/sec を超えるリクエストを受けるMulti-tenant Kubernetes ClusterをZero downtimeかつプロダクト開発の影響を最小限に抑えて新Clusterに切り替えること」
ここではこれらを具体的に紹介します.
Multi-tenant Kubernetes Cluster
まず我々が運用しているKubernetes Clusterの大きな特徴としてMulti-tenantを採用していることが挙げられます.KubernetesのTenantパターンには大きく分けて以下の2つパターンが考えられます.
- Tenant(サービス)ごとに複数のClusterを立てるパターン(Multi-cluster)
- 複数Tenantを1つのClusterでホストするパターン(Multi-tenancy)
それぞれのパターンには利点と欠点があります.例えば複数ClusterパターンではTenant間やWorkloadやアクセスを厳密に隔離できるという利点がありますが,複数のClusterを管理する必要があり運用が大変になるという欠点があります.例えばKubernetesは毎Qごとに新たにマイナーバージョンがリリースされ,サポートは最新の3バージョンのみです.そのためClusterのバージョンアップはかなりの頻度で行う必要があり,Clusterの数が多いほど運用のコストが高くなるのは容易に想像がつくと思います.逆にMulti-tenancyではCluterの管理コストは減らすことができたり,Clusterの改善のLeverageが高いという利点がありますが,Control planeを共有する必要があるなどTenant間での隔離は完璧ではなく,アクセス制御の設計などをしっかりと行う必要があります.
メルカリがKubernetesの利用を開始した2年前はKubernetesの機能が十分でないこともありMulti-clusterパターンが多くの企業で採用されていたように思います.一方でRBACやNetworkPolicyといった機能がStableになりNamespace Tenantによる隔離がやりやすくなりつつある時期,Communityの方向性もMulti-tenantに動きつつある時期でもあったと記憶しています(現在はMulti-tenantの事例のほうが多く聞く気がします e.g., Building a Container Platform at Cruise.またKubernetesが多くの思想を受け継いでいるGoogleのBorgがそもそもMulti-tenantパターンを採用しています).
これらの状況を考慮してメルカリでは初期からMulti-tenantパターンを採用してきました.具体的には以下の図のようにMicroservicesごとにKubernetes namespaceを準備しています.そして各Kubernetes namespaceごとにRBACを準備し,そのNamespace内のリソース管理をそのサービスの開発チームに移譲してます.Platform TeamはMonitoring agentを始めとするCluster コンポーネントやClusterのNodeの管理,Clusterのアップグレードなどをメインに担当して役割も明確に分離しています.ただし開発環境と本番環境のClusterだけは分けています.
End-to-End Responsibility
これはMicroservices化の初期から徹底していることですが,メルカリのMicrosevicesのチーム体制では専門性でチームを割ることをしていません.つまり特定の専門チームがテストを担う,デプロイを担う,といったことはしておらず,サービス開発チームが開発からテスト,デプロイ,運用までの責任を持っています.
これはKubernetesのManifestの管理に関しても同様です.Platform TeamやSRE Teamがサービスチームの代わりにKubernetes manifestを書くことはしていません.Platform TeamはManifestを自動でApplyする仕組み(Spinnakerなど)を提供しているだけで,どのようにManifestを書くかやデプロイのタイミングなどは全てサービスチームに任せています.
Workload
このMulti-tenant Kubertentes上でメルカリとメルペイのMicroservicesを動かしています.KubernetesのNamespace数(=microservices数)で150+,Pod数は3000+が動いています.
メルカリのマイクロサービス移行の進捗 (2019年冬)で紹介されているようにメルカリのMicroservices化は完了しておらずMicroservicesとMonolithが共存している状態です.移行の方式としてAPI gatewayによるStranglerパターンを採用しており,そのAPI gatewayは同Cluster上で動いておりメルカリの全リクエスト(約80,000+ req/sec)を捌いています.
メルペイのマイクロサービスとCloud Nativeで紹介されているようにメルペイは初期からMicroservicesで開発が行われておりその全てがこのCluster上で動いていることになります.
このようなWorkloadが動いているためClusterの移行の際はメルカリの大規模なリクエストとメルペイというサービスに求められるReliabilityを考慮する必要がありました.
なぜ移行をしたのか?
このような大規模なClusterの移行をするには大きなコストがかかるためそれなりの理由が必要になります.以下では我々が直面していた問題について紹介します.
Routes-based Cluster
我々のClusterはRoutes-based Clusterでした.Routes-based clusterとはGoogle Cloud Routesを利用したClusterです.現在GKEではこのRoutes-based Cluterは非推奨になっており,Alias IPを利用したVPC-native Clusterがデフォルトになっています.
パフォーマンスやScalability的な観点から見てもVPC-native ClusterはRoutes-based Clusterと比較しても大きな利点があります.さらに以下のようなGKEの新しい機能はVPC-native Clusterであることが前提になっています.
- Container-native load balancing: これによりGoogle load balancerはNodeではなくPodを見てLoad balancingを行なうようになり,Performanceの改善やVisibilityの改善を見込めます.
- Traffic director: Container-native load balancingはNEGを利用しておりNEGはGoogleのManagedのTraffic Control PlaneであるTraffic directorがそれを前提としています.Microservices Platform TeamではIstioを利用したMesh アーキテクチャへの移行も進めており,Traffic directorを使うことでGKE clustert内部だけではなく他のClusterやVMとの接続もMeshアーキテクチャに追加することが可能になります
- Shared VPC: 後述するように今回の移行に伴いGCPのネットワークの再設計を行いました.その際にShared VPCを採用しましたがShared VPCを使うためにもVPC-native Clusterであることが必要になります
- Memorystore: 多くのMicroservicesにはパフォーマンスの改善のためにCacheを必要とします.現在は自分たちでKubernetes上にRedisを立てていますが,運用のコストが高いため,できればManaged serviceを使えるのが望ましいです.GCPはMemorystoreを提供していますが,VPC-native ClusterではないGKE上のPodからMemorystoreに接続するにはCustom iptablesを使ったWorkaroundが必要になってしまいます
- Optimizing IP address allocation: Kubernetesのネットワーク設計をしたことがあるひとは知っているかもしれませんがKubernetesはとにかく大量のIPを使います.限られたIPリソースを効率的に使うにはIPの割り振りを最適化する必要があります.これを行なうためにもGKE ClusterはVPC-nativeである必要があります
Zonal Cluster
これまでの我々のClusterはMulti-zonal Clusterでした.つまりControl plane(Kubernetes master)は1 ReplicaかつSingle zoneのみで動いている状態でした.そのためSLAは99.5%であり,ClusterのUpgradeの際はダウンタイムが生じてしまう状態でした(e.g., kubectlが使えなくなったり,SpinnakerなどのDeliveryツールも動かなってしまう).
Regional clusterにすることでUpgarde時におけるダウンタイムを避け,SLAは99.95%となりより高いAvailabilityを期待することができます(ただしRegionalによりOperationの時間が遅くなるという欠点はあります).
Default Network
ClusterはAuto modeによるDefault-networkを利用していました.つまりGCPが自動で生成するSubnet(例えばasia-northeastは10.146.0.0/20が自動でアサインされる)をそのまま使っており,他のプロジェクトや自社DCとの接続などを見越したIPデザインは全くされていない状態でした.
特に直近ではMonolithからMicroservices移行のために自社DCの環境とClusterとをプライベートに接続できることは必須です.また長期的にはGCPのみではなくAWSなどの他のクラウドにも基盤を拡張していく可能性があります.これらを実現するためにはネットワーク設計の根本的な見直しも必要でした.
Single Platform
1つの方法としてわざわざプロジェクト化して1つチームが移行を先導することなく,各チームの裁量に移行を任せることも考えられます.例えば,Platform Teamは新しいClusterを立てておくのみで,新規のサービスからそれを使ってもらい,古いサービスも可能なタイミングで移行を行ってもらい時間をかけて移行を完了するとう方法も考えられます.しかしこれまでの経験からしてそのやり方では一生移行は完了せず,新旧両方のClusterをずっと面倒見る羽目になりチームの大きな負担になります.また古いClusterで動くアプリケーションにも新しいClusterとの差分を吸収するためにWorkaroundが必要になってしまうことも往々にしてあります.そのためClusterに責任をもつチーム,Platform Team,がプロジェクトとしてそれを先導して特定の期間内で終わらせることは必須です.
これは今回のようなKubernetes Clusterの移行だけではなく,新しいツールの導入や,例えば将来的にKubernetes以外の基盤が登場してそれを使う場合でも同じです.何か新しいことをするときにその移行戦略まで考える,実装するのはPlatform Teamとしては非常に重要です.でないと,1つの組織に複数のツールや基盤が乱立することになり,基盤としてとても使いにくいものになってしまいます.
プロジェクトの概要
移行を決意したのは今から1年ほど前になります.Google Cloud Next SF 2019に参加し,GKEに関わる新製品についての話を聞いたり,またGKEのPMやEngineerとのディスカッションを経て,これはすぐに動き始めないとやばいという危機感を持ちました.
上述したように問題設定は非常に複雑で1-2ヶ月で終わるようなプロジェクトではないことは予めわかっていました.またPlatform Teamのみで完結する話ではなく,SRE TeamやBackend Teamの協力も不可欠でした.そのためになぜやるか?や何をやるか? を明確にするために以下のようなプロジェクトのドキュメントを書くところから始めました(この記事の前半に書いたことが書かれています).このドキュメントはプロジェクトの初期衝動を伝えるために最初から最後まで利用されました.
このドキュメントをもとにチームでディスカッションを行いプロジェクトを開始しました.
プロジェクトの優先度
Clusterの移行方法はあらゆる手段が考えられます.例えば,完全自動化をしてサービスチームの介在なしにバックグランドで移行を完了する方法も考えられますし,Platform TeamがManualで一つ一つのリソースを移していくといった方法も考えられます.これらはどのような要素に高い優先度をつけるかによって変わり,チームメンバーによってこの優先度は変わります.
プロジェクトは長期に渡り,常に同じメンバーがこれに関わるわけではなく様々なメンバーが関わることになります.メンバーが変わるたびに優先度が変わり方式がブレるのは望ましくありません.この課題を解くためにプロジェクトの初期にTrade-off Slidersを行いました.
Trade-off Slidersはプロジェクトにとって重要になる要素,Metricsを挙げてそれらに優先度をつけ,チームで合意を取る方法です.以下はそれを行ったときの様子です.
結果としては以下の要素と優先度が決まりました.
- Risk: どれだけ障害なく安全に移行を完了させることができるか?
- Developer experience:どれだけサービスチームの手を煩わすことなく移行を行うか?
- Time:どれだけの時間をかけるか?
- Reusability:どれだけ再利用性を考えるか?
- Money:どれだけコストを抑えるか?
プロジェクトの初期は移行の方法について多くの意見があり,まとまらないこともありました.しかしこの優先度が決まってからは,意思決定が非常にスムーズなったことを記憶しています.
移行方法
以下では実際にどのように移行を行ったかについて紹介します.
ネットワーク再設計と構築
まず,Default Network 問題解決のため,および自社DCや他の Cloud Provider との接続を見越したネットワークの設計および構築からはじめました.詳しい設計については,Raphael の記事「Network Architecture Design for Microservices on GCP」を見ていただければと思いますが,社内のさまざまなチームとのディスカッションや調整を行い,GCP からのサポートも受けつつ,約1ヶ月ほどで設計およびネットワークの構築を行いました.
新Clusterの機能や設定
VPC-native Cluster にすることで利用可能となる新しい機能や設定のうち,実際にどれを使うのか,また今すぐは使わなくても将来使う可能性があるのか,調査や検証を行いました.
また,無効から有効にする際にCluster再作成が必要となる機能もいくつかありました.(Workload Identity, Cloud TPU, Private cluster, Maximum Pods per Node 等 / しかし現在は再作成しなくても良いものもあり)
また,当初使う予定だった機能をいざ検証してみると,既存の Workload にうまく適用できず問題があり,今回は利用を見送ったものもありました (Workload Identity).
Cluster移行時の制約
Clusterを移行するうえで,以下の2点を制約としました.
- 既存のClusterと新Clusterをネットワーク的に接続しない
- Cluster移行がお客さまの利用に影響しない (ダウンタイム無し)
まず再設計したネットワークと既存のClusterのネットワークを接続し,各ワークロードとトラフィックを個別に移行する方式も検討しましたが,よりシンプルに移行を行うために,接続しないことを選びました.よって,Cluster内のすべてのワークロードを新Clusterに移行したうえでトラフィックを移行していく方式としました.
次にダウンタイムを伴うメンテナンス時間を設けずに,お客さまの利用には影響しないこと,を移行の制約(目標)としました.
Cluster移行フロー
上記の制約を踏まえ,移行を以下の4つのフェーズにわけました.
- Cluster作成 (Cluster Creation)
- リソース移行 (Resource Migration)
- トラフィック移行 (Traffic Migration)
- Cluster削除 (Cluster Deletion)
この4つのフェーズをまず開発用Clusterで行い,細かい問題の発見と改善をしたうえで,本番用Clusterにて実行しました.
1. Cluster作成 (Cluster Creation)
このフェーズでは,我々 Microservices Platform Team が GKE Clusterそのものの構築と全てのMicroservicesから利用される Platform コンポーネント,例えば Datadog Agent などのセットアップとテストを行いました.
2. リソース移行 (Resource Migration)
このフェーズでは,各Microservicesのアプリケーションと関連するリソースを新Cluster内に複製しました.まず,我々 Microservices Platform Team がアプリケーション以外のリソース,Secret や Configmap,ServiceAccount 等を一括で新Clusterに複製しました.この複製には Velero を利用しつつ,Velero ではカバーできないケース (リソースの更新や削除) のために独自の差分チェック・同期スクプリト群を作り,定期的に実行しました.その後,事前に準備していた移行ガイド等のドキュメントに沿って,各Microservices開発者がアプリケーションの新Clusterへの複製(配備)を行いました.この時点では,まだトラフィックは新Clusterに流れていません.
3. トラフィック移行 (Traffic Migration)
このフェーズで,いよいよトラフィックを新Clusterに移行します.
メルカリ・メルペイでは,構成が異なる複数の Ingress があり,その構成により可能な方法で,段階的にトラフィックを移行しました.例えば,Fastly を前段に利用している Ingress の場合は,Faslty の weighted routing を使い,Fastly を使っておらず Route53 を DNS サーバとして利用している場合は,Route53 の weighted routing を使い,1% から徐々にトラフィックを移行しました.1,2週間をかけて徐々に新Clusterへのトラフィック量を増やしながら,すべての移行を完了しました.
4. Cluster削除 (Cluster Deletion)
すべてのトラフィックを新Clusterに移行後,万一のリカバリにそなえて約2週間程,旧Clusterを残し,その後削除しました.
リソース移行の難しさ
上記移行フローのうち「リソース移行 (Resource Migration)」において,リクエスト起因ではないもので動くリソース,CronJob や Job,Pub/Sub Subscriber は,トラフィック移行のように徐々に移行ができないため,一筋縄ではいかない部分でした.
日時バッチやデータベースマイグレーションなどに利用している CronJob や Job は,依存するMicroservicesがあるものと無いもの,複数の環境で同時に動いてもよいものとダメなものがあり,旧Clusterで動かし続けつつ,トラフィック移行の完了後にひとつづつ移行していくことになりました.
また,メルカリ・メルペイでは多くの Microservices が Cloud Pub/Sub を利用しており,かつそのほとんどで Subscriber が gRPC/HTTP サーバーと同居しています.そのためアプリケーションを新クラスタにそのまま複製(配備)すると,Pub/Sub Subscriber も起動してしまいますが,依存するMicroservicesがまだ新Clusterで動いていない場合にはその処理に失敗してしまいます.失敗しても Pub/Sub のメッセージを Ack しないので,データの不整合は発生しませんが,エラーが出つづけることによるモニタリングへの影響や,Pub/Sub 処理の遅延を避けるために,Pub/Sub Subscriber を動かすかどうかの Flag を各Microserviceで実装してもらい,旧Clusterでは ON / 新Clusterでは OFF の状態でトラフィックを移行し,その完了後に逆の設定に変更する,という方法を行いました.
今後の課題
今回のCluster移行に関して最も大きな課題は(デザインを含めて)1年近くの時間がかかってしまったことです.今回の移行は「新機能を使うため」の移行ですが,見方を変えるとCluster移行はDisaster Recovery(DR)としても見ることができます.つまり問題設定を「現在のClusterで障害が起こってしまったときにどのようのWorkloadを別のClusterに逃がすか」として考えることもできます.GKEのMasterはGCPにより管理されていますが障害は起こりうるし,Tokyo regionで障害が発生したら他Regionへの移行が必要になります.もちろん今回は開発の影響を最小限にする,ダウンタイムなしで移行するといった制限があったり,年末年始のコードフリーズを挟んだために時間が多くかかってしまいましたが,DRのような状況を考えたときに現在のアーキテクチャや基盤の仕組みは十分ではないと移行を進める中で強く感じました.
これらの課題を解決するために今後はHomogeneous Multi-clusterアーキテクチャへの移行を考えています.つまり,全く同じ設定が適用され,同じWorkloadが動くClusterを複数準備するアーキテクチャです.これにより「あるClusterで問題が発生したらそのClusterへのリクエストを止め問題のないClusterにリクエストを流す」ことが可能になります.例えば,Cluster障害に対応することもできるし,今回のようなClusterの作り変えが必要になったとき(また必ず必要になると感じています)も容易にそれを行うことができるようになります.Cluster自体をEphemeral,つまり容易に消したり作り直したりできる状態,にしていくことが必要だと感じています.
またClusterを複数にしていくと,それを使うサービスチームから見たときに基盤は非常に複雑なものになっていきます.また現在はKubernetesのmanifestやSpinnakerのPipelineの管理は完全にサービスチームに移譲しており,基盤として共通の変更を加えることが難しくなっています.これらの課題を解決するために基盤の抽象化,Platform化をさらに推し進め,生のKubernetesのAPIをExposeすることを減らしていくことを考えています.
まとめ
本記事ではメルカリにおけるKubernetes Cluster移行について,なぜ移行を行ったのか,どのように移行を行ったのかを紹介しました.また移行によって明確になった今後の課題といくつかのアイディアについてまとめました.今後Clusterの移行を行う方やClusterの設計を行う方の参考になればと思います.
参考文献
参考文献としてはCase Studies in Infrastructure Change Managementがおすすめです.これはGFSからColossusへの移行(2年)とDisklessへの移行(6年!)というGoogle社内の大規模なインフラ移行の解説とそこから得られた学びについてそれに関わったSREのTPM(Technical Program Manager)が書いたレポートです.規模は大きく違いますがコミュケーションの仕方やプロジェクトの進め方など学びは多いです.