この記事はGMOペパボエンジニアカレンダーの12/20の記事です。7日遅れです。ほんとごめんなさい。
こんにちは。技術部プラットフォームグループのshibatchです。私は2022年に入ってからSUZURIのプラットフォーム改善─とりわけK8sとAWS周りの改善に取り組んできました。そういえば自分の成果について一年を通した振り返りはしてないなぁ、と気づいたので、この機会にSUZURIのK8s環境の何をどう改善していったかをアウトプットする試みをします。
結論だけを言うと
- K8s環境でコスト周りの改善、認知負荷を減らす改善を続けていました
かんたんに環境を説明
振り返りの前に、まずSUZURIにおけるK8s環境についてかんたんにご説明します。SUZURIはユーザーさんがアップロードした画像をTシャツなどに商品化することができるサービスです。そのために、アップロードした画像とTシャツ画像の合成処理が必要になります。その画像合成をK8s環境で行なっています。
K8s環境はProduction(本番)環境はNKE(ペパボのプライベートK8s環境) / GKE(Google Kubernetes Engine)という2つのK8s環境にまたがっています。GKEではStaging環境、Sandbox(テスト)環境、Production環境を構築しており、NKEではProduction環境のみを構築しています。
また、以下の3つのdeploymentが動いています。
- lens1 … 画像合成アプリケーション。Ruby製。現在いちばん多く使用している。CoffeeScript製。
- lens2 … lensの後継となる画像合成アプリケーション。徐々にlens1から移行してきている。Rust製。
- lens2-a(ここでの便宜上の呼び方です) … lens2で特定の商品用に動いている画像合成アプリケーション。Rust製。
※lens2(Rust製)については日経XTECHに記事がありますので併せてご参照ください!
課題は何か?
SUZURIのK8s環境を触ってみると、複数課題がありそうなことがわかってきました。
- lens1でEvictedになっているPodが多い
- アプリケーションのメモリの使用量がPodやノードのリソース設定と合っていない可能性がある
- lens2 / lens2-aが環境ごとにノードのリソース設定が揃っていない
- productionではCPUリソースを6000m(!)Requestしている
- staging/sandboxではリソースのRequest / Limitは設定されていない
- stagingとproductionで設定が揃っていないので検証が正しくProductionのリハーサルにならない恐れがある
- 環境でリソースが揃っていないのは認知負荷が高いように思う
- lens2 / lens2-aではメモリのRequest / Limitが設定されていない
- どの程度メモリリソースを使うのが正常なのかよくわかっていない
- GKEではlens1 / lens2 / lens2-aがProductionでそれぞれ専用ノード化していて、柔軟性がない
- ノードプールが分かれている(=専用ノード化している)
- lens2 / lens2-aでは上記の通りCPUリソースが6000m要求している。1ノードに1Pod動く構成になっている
- K8sの長所である柔軟性が生かされていない
- そもそもCPUリソースが6000m必要なのかがわからない
こうしてみると以下の2点に問題が集約されます。
- Podのリソース設定が適正かがわかっていない
- ノードのリソース設定がアプリケーションと合っているかわかっていない
これらの問題の解決に取り組みました。
実施したこと
Podのリソース使用量を計測し、調整をする
まずはlens / lens2 / lens2-a すべてに垂直オートスケール(VPA)を導入しました。VPAはPodのCPU / メモリリソースのリクエスト量を自動でスケールアップ / スケールダウンする機能ですが、スケールアップ / スケールダウンの自動化まではさせず、適正なリソース量をサジェストしてもらう目的で使いました。
なぜVPAで自動化しないかというと、水平オートスケール(HPA)を利用しているからです。HPAはリソース使用量に応じて自動でPod数を増減(スケールイン/スケールアウト)してくれる機能ですが、VPAでスケールアップ/スケールダウンされると何の値を評価してスケールイン/スケールアウトすればよいかがわからなくなってしまいます。
さてVPAによって、適正なリソース量が把握できたため、以下のようにリソース使用量を調整しました。(lens2-aはlens2と同様の調整になったため省きます)
Request
Pod | 変更前-CPU | 変更前-Memory | 変更後-CPU | 変更後-Memory |
---|---|---|---|---|
lens1-Production | 1000m | 2Gi | 1150m | 6Gi |
lens2-Production | 6000m | 設定なし | 600m | 6Gi |
lens2-Staging/Sandbox | 設定なし | 設定なし | 600m | 6Gi |
Limit
Pod | 変更前-CPU | 変更前-Memory | 変更後-CPU | 変更後-Memory |
---|---|---|---|---|
lens1-Production | 3500m | 12Gi | 1500m | 10Gi |
lens2-Production | 設定なし | 設定なし | 1000m | 8Gi |
lens2-Staging/Sandbox | 設定なし | 設定なし | 1000m | 8Gi |
これにより、以下の効果がありました。
- lens1のメモリリクエストが適正化されたことにより、OOMでEvictedになってPodが再作成されることがなくなった
- lens2でSandbox / Staging / Productionで同じリソース量になったので、認知負荷が減り、性能差がなくなった
NKE(lens1)のノードをスケールアップする
NKEでは8vCPU / 16GBメモリのノードを50台用意し、lens1を乗せて運用していました。 前述したリソース調整により1Podにつきメモリを6Giリクエストするため、1ノードにつき2Podしか載らない状況であることがわかりました。 そのかわりCPUは8vCPUなのに2300mのリクエストに留まっており、ノードのリソースを使いきれていません。
そこで8vCPU / 16GB -> 8vCPU / 50GB のノードにメモリのみスケールアップさせ、台数を50台→30台にしました。
これはスケールアップと台数削減を同時に行うことでコストは変わらないのですが搭載できるPod数が100Pod->180Podに1.8倍になりました。
GKEでlens1 / lens2 / lens2-aを同じノードで動くようにする
さて今度はGKE側の改善です。実施したのは以下の2つです。
- 今までlens2 / lens2-aが専用のノードプールで、1Podにつき1ノードで運用されていたものを、同じノードで動くように統一する
- ノードのリソースをNKEのものと同じにする
これによって
- リソースの無駄がへり、コストが削減されました。
- NKEと同じ認知負荷を減らし、わかりやすい構成になりました。
- Datadogを搭載しているのですがノード数で課金されていたので、ノード数が減ったことでコスト削減しました。
NKEのノードを大幅に増やし、lens1をほぼNKEのみでの構成に変更
私が担当に入った頃はlensのトラフィックはRoute53の重み付きラウンドロビンでNKE:GKEが100:30で運用されていました。
NKEはまだノードを増やしても大丈夫ということがわかったので、30台あったノードを50台までスケールアウトし、NKE:GKEを99:1にしました。緊急時のみGKEを使うようにし、普段はほぼNKEを使う運用に変えました。これによりGKEのノード数が減り、コストの大幅な削減になりました。また、Datadogのコストも削減されました。
トラフィックが増えるとRoute53でGKE側のweightを増やす仕組みを作った
表題通り、トラフィックが増え、NKE側で受けきれなくなった場合はRoute53の重みを自動で調整する仕組みを作りました。詳しくはペパボテックポータルにアクセス数に連動してDNSの重み付けを自動制御する仕組みをAWSで作った話という記事を書いたのでご覧ください。
そのほか
- Kubernetesのバージョンアップメンテナンス。1.21〜1.24まで段階的に逐次バージョンアップしていきました。Ingress-nginxといった周辺コンポーネントも同時に上げていきました。
- NKEとGKEでHPAでの最大Pod数の上限に差をつけて自動デプロイされる仕組みにしました。GitHub ActionsとKustomizeを組み合わせました。
こんなところかなぁ。だいたいここまでの改善は1月〜8月までに固めてやったものです。細かいものはまだまだありそうですが長いブログになったのでこのへんで。
来年は何するの
本当は今年完遂したかったlens2をNKEで動かす施策は来年ぜひ完了させたい…!