こんにちは。デジスマチームの伴です。この記事はデジスマチームブログリレーの5日目の記事です。
先週は SRE チームとして GitHub 移行をやっている話を投稿しましたが、兼務しているデジスマチームではインフラ周りを担当しており、Kubernetes のメンテナンスなどをしています。
デジスマ診療(以降、デジスマ)では、利用者および利用施設の拡大に伴い、サーバが処理するリクエスト数も増加しています。インフラ側ではこれに対応するため、様々な施策を講じてきました。
本記事は、講じてきた様々な施策のうち、KubernetesのPodを予定した時間に事前にスケールアウトさせる仕組みを整備して、システム負荷のスパイクに効率的に対応し、同時にコストも最適化した具体的な仕組みをご紹介します。
- デジスマでの Kubernetes 利用状況
 - デジスマの課題
 - CronJobで時間に合わせてスケールさせてみたが…
 - ExternalMetrics と Datadog を使ってスケールさせる
 - 結果
 - おわりに
 - We are Hiring!
 
デジスマでの Kubernetes 利用状況
デジスマは、QRコードによるチェックインや自動後払い、オンライン診療など、新しい医療体験を提供するサービスです。
デジスマではマイクロサービスアーキテクチャを採用しており、各業務ドメインを扱うサービスや、フロントエンド向けの BFF (Backend For Frontend) に相当するサービスが存在します。これらのマイクロサービスは、EKS で構築された Kubernetes クラスター上にデプロイされており、Flux CD を使って管理されています。
Flux CD は、Kubernetes クラスターの構成リソースを Git リポジトリなどと同期させ、リソースに変更があった際に自動で更新する GitOps を実現するためのツールです。類似のツールには ArgoCD などがあります。
Flux CDは定期的に Reconciliation を行い、ソースとしている Git リポジトリの状態とクラスタリソースが同じになるよう維持します。この機能により、Git のソースを正しい状態として、常にクラスターの状態を維持できます。
また、Pod のスケールアウトには HPA (Horizontal Pod Autoscaler) を利用しています。 HPA は、名前の通り Pod を自動で水平スケールアウトするためのリソースです。アプリケーションの負荷などのメトリクスを利用して、事前に指定された値を超えた場合に Pod を水平スケールアウトします。CPU 負荷のメトリクスなどを利用するのが一般的ですが、私たちのアプリの性質上 CPU の消費はほぼ一定であるため、スケールアウトの条件には RPS 利用しています。RPS メトリクスを Datadog 上から ExternalMetrics として取得して利用しています。
デジスマの課題
冒頭でも述べた通り、デジスマでは利用者増加に伴ってリクエスト数が増加傾向にあります。それに合わせて Pod の CPU やメモリのリソースサイズを増やしたり、JVM のチューニングを行ったりと対策を進めてきました。
しかし、その中で新たな課題が見えてきました。特に、ほぼ毎日決まった時間に秒間リクエスト数 (RPS) がスパイク(急増急減)するタイミングが顕著に現れるようになりました。
RPSがスパイクする原因はいくつかあります。たとえば、デジスマには、診療予約の一定時間前になると、予約された患者にリマインド通知する機能があります。リマインドの多くは決まった時間に行われるため、その時間にリクエストが集中してしまいます。
現時点ではアクセスに耐えられないほどではありませんでしたが、今後の成長予測を考えると無視できない状態でした。各Podの起動には1分から2分前後かかるため、RPSがスパイクしてしまうと、リクエストを処理したい時間帯にPodのスケールが間に合わない可能性がありました。もしそのような状況になってしまうと、システムのパフォーマンス低下だけでなく、予備のPodを常時稼働させることによるNodeの増加、ひいてはコスト面での課題も生じるだろうと議論していました。
CronJobで時間に合わせてスケールさせてみたが…
まず最初に導入した方法は、CronJob 方式でスケールアウトさせる方法でした。これは、CronJob で指定した時間に Pod 数をスケールさせるコマンド(例: kubectl scale deploy/service-a --replicas=10)を実行する方法です。この方法は一時期使っていましたが、Flux CD との相性が悪いことが問題でした。
Flux CD は、ソースとなる Git リポジトリを正しい状態と判断するため、Deployment の Replicas を一時的に変更しても、一定周期で行われる Reconciliation によって Replicas が元の値に戻ってしまいます。これを避けるためには、Reconciliation の対象外とするラベル (kustomize.toolkit.fluxcd.io/reconcile: disabled) をリソースに付与する必要があります。
しかし、このラベルを付与してしまうと、付与されたリソースは Flux CD の管理対象外となるため、そのリソースだけは手動でのデプロイが必要になってしまいます。
つまり、Replicas 以外の設定等が変更された際に忘れず、手動でのリリース作業が必要になる訳です。
これでは Flux CD の利点を最大限に活かせませんし、リリース作業の手間が増えてしまうため、この方法を現在では採用していません。
ExternalMetrics と Datadog を使ってスケールさせる
Flux CD との相性問題を解決するために、次に私たちが取った方法は、コントロール可能なカスタムメトリクスを HPA で用いて、Pod 数をスケールさせる方法です。
冒頭でも述べた通り、RPS を HPA のメトリクスに使用しています。この HPA では、複数のメトリクスをスケールアウトの条件に指定できます。
この仕様を活かして、RPS に加えて Pod の期待台数が取得できる Exporter を独自に用意し、取得できるカスタムメトリクスを使って Pod をスケールさせることにしました。
ちなみに、同様の試みをされている記事もありましたので、当時参考にさせていただきました。
pod-scaler-exporter
時間毎の期待する Pod 台数を取得するための Exporter を pod-scaler-exporter と名付け、次ような構成で作成することにしました。

pod-scaler-exporter 自体の仕組みは至って単純です。
後述するスケジュールコンフィグに従って、決まった時間に、決まった値になるメトリクスを取得できる Exporter です。
通常の Exporter と同様に /metrics  エンドポイントでメトリクスを取得できるようにしておきます。これは、Datadog Agent がメトリクスを取得しやすい様に /metrics エンドポイントのままにしています。
メトリクスのラベルには、対象となる HPA の名前(=Deploymentの名前)がラベリングされており、どの Deployment がいくつの Pod になることを期待しているかが分かるようになっています。
例えば、次のようなメトリクスが取得できます。
# HELP digisma_expect_min_replicas Expect minimum number of replicas for each service.
# TYPE digisma_expect_min_replicas gauge
digisma_expect_min_replicas{env="prod",namespace="default",service="service-a"} 2
digisma_expect_min_replicas{env="prod",namespace="default",service="service-b"} 2
digisma_expect_min_replicas{env="prod",namespace="default",service="service-c"} 6
digisma_expect_min_replicas{env="prod",namespace="default",service="service-d"} 5
digisma_expect_min_replicas{env="prod",namespace="default",service="service-e"} 3
これらが Datadog Agent 経由で Datadog にアップロードされます。 DataDog に上がった メトリクスの値は、HPA の ExternalMetrics で監視され、Pod の台数を決めることに使われます。
スケジュールコンフィグ
時間でスケールさせるため、事前に「何時から何時まで」「どの Deployment が」「何台増えるのか」を決めておく必要があります。これを設定したファイルをスケジュールコンフィグと呼んでいます。
スケジュールコンフィグは、次の形式で ConfigMap を使って設定できるようにしました。
apiVersion: v1 kind: ConfigMap metadata: name: schedule-config namespace: default data: schedule.yml: | services: - name: service-a schedules: - replicas: 3 cron: "20 7 * * *" duration: 12h - name: service-b schedules: # 7:20~19:10は、4 をメトリクス値とする - replicas: 4 cron: "20 7 * * *" duration: 12h # 7:20~12:20は、6 をメトリクス値とする - replicas: 6 cron: "20 7 * * *" duration: 5h # 月曜日限定 - replicas: 7 cron: "20 7 * * 1" duration: 5h
各 Deployment ごとに、cron 式で開始時刻を定義し、どれくらい継続させるかを duration で定義できます。また台数も合わせて定義すればスケジュールの完成です。このスケジュールは複数定義でき、その時間ごとの最大値が採用される仕様です。
複数のスケジュールを定義できるため、毎日のスケールスケジュールや、連休明けや年末年始などの特定日時なども一緒に設定しておくことができます。
この ConfigMap も Flux CD で管理されているため、Git リポジトリにコミットするだけで更新が可能です。
メトリクスの収集
pod-scaler-exporter からスケジュールコンフィグに合わせて出力されたメトリクスを Datadog Agent で収集する必要があります。例えば、次の様な annotation を定義するだけで可能です。
annotations: ad.datadoghq.com/app.checks: | { "openmetrics": { "instances": [ { "openmetrics_endpoint": "http://%%host%%:%%port%%/metrics", "namespace": "digisma", "metrics": [{"expect_min_replicas": "expect_min_replicas"}] } ] } }
実際に取得できたメトリクスは Datadog 上からも確認できます。

HPA の修正
まず DatadogMetrics を次のように定義しておきます。
apiVersion: datadoghq.com/v1alpha1 kind: DatadogMetric metadata: name: service-a-scaler namespace: default spec: query: "max:digisma.expect_min_replicas{service:service-a, env:prod}"
そして、メトリクスを HPA へ次のように追加します。
apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: service-a namespace: default spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: service-a minReplicas: 2 maxReplicas: 20 metrics: - type: External external: metric: name: datadogmetric@default:service-a-rps target: type: AverageValue averageValue: 100 - type: External external: metric: name: datadogmetric@default:service-a-scaler target: type: AverageValue averageValue: 1
これによりHPAは、設定された複数のメトリクス(ここではRPSとpod-scaler-exporterの出力)とminReplicasを比較し、その中で最大の推奨レプリカ数を採用してPodをスケールします
結果
pod-scaler-exporter を稼働させてからは、RPS の値がスパイクするより前に、スケールアウトしておく事ができるようになりました。
この計画的なスケールにより、十分な台数の Pod でリクエストを受けられるようになり、監視アラートの発生も大幅に減少しました。HPA を活用したこの方法は、CronJob 方式のような手動介入の必要がなく、Flux CD ともスムーズに共存できました。
さらに、この取り組みはシステム負荷の最適化だけでなく、コスト面でも大きな成果をもたらしました。必要なときに必要なだけ Pod をスケールできるようになったため、平常時の稼働台数を最低限まで削減できました。以前はRPSのスパイクに備えて余分な Pod を常時稼働させる対策を取っていましたが、その必要がなくなり、運用コストを大幅に削減できました。同様に、夜間などの低負荷時にも自動でスケールインすることで、継続的なコスト削減を実現しています。
おわりに
本記事では、時間毎の期待する Pod 台数を取得するための Exporter を作ることで、RPS のスパイクに間に合うスケールアウト方法が確立され、さらに、余分な Pod をスケールしておく必要がなくなり、コスト削減効果も得られた事例をご紹介しました。
本記事でご紹介したアプローチが、Kubernetes 環境でのシステム負荷とコストの最適化を目指す皆さんの参考になれば幸いです。
We are Hiring!
エムスリーではエンジニアを募集中です。
もしデジスマの仕事に興味をお持ちいただけたならぜひ詳細をご確認ください。デジスマ以外にも多数の職種がありますので他の職種ももちろん大歓迎です!
エンジニア採用ページはこちら
カジュアル面談もお気軽にどうぞ
エンジニア新卒採用サイトもオープンしました!
インターンもこちらから。常時募集しています!