GKE CronJobとcloud-sdk-goでElasticCloudのスケーリングを自動化しコスト削減した

はじめに

こんにちは、検索エンジニアの安達(id:kotaroooo0)です。 10Xで検索基盤・検索機能の開発運用をしています。 最近は推薦システムの開発もちょっとやり始めました。

負荷に合わせてElasticsearch(ElasticCloud)をスケーリングする機能を作りコスト削減したので、その取り組みについて経緯と内容を紹介します。

前提

背景と課題

10Xでは小売チェーン向けECプラットフォームStailerにおいて、検索機能の開発運用にElasticsearchを利用してしています。 Elasticsearchクラスタの基盤にはElasticCloudを採用していますが、ディスク利用量に応じたオートスケール機能しかなく、メモリ利用量やCPU利用量に基づくオートスケーリングはできません。 そのため、負荷が少ない時間帯でもピークタイムのリクエストを処理できるクラスタを稼働させる必要がありました。

また、日に日に負荷が増え続けている影響で、最近インスタンスサイズをスケールアップしないと安定した運用ができなくなりました。 これによりコストが約2倍になり、何らかの対応が必要になりました。

対応方針

リソース利用率に合わせてElasticsearchをスケーリングすることで解決しました。 他の打ち手の候補として、データ更新の時間をずらす、データ更新の速度を落とす、インデックスのデータ構造・設定の変更がありましたが、調整によるリードタイムや工数が大きかったり、効果の不確実性が高かったため見送りました。

StailerのElasticsearchに関する特性

StailarのElasticsearchはCPU boundです。 検索リクエストのピークタイムが8:00 - 12:00、更新リクエストのピークタイムが9:30 - 12:00で重なっており、結果的に CPU利用率 のピークタイムは 9:30 - 12:00になっています。 細かいデータ更新は常にあるものの、スーパーやドラッグストアの在庫データを一括で更新するバッチが日次かつ定時で動いており、更新リクエストのピークタイムは固定です。

以下はCPU利用率のメトリクスです。 縦はCPU利用率、横は時刻です。

検索・更新リクエストのピークタイムが被る9:45~あたりからCPU利用率が急上昇します。

要件

スケールイン・アウトではなくスケールアップ・ダウン

ノード数を増減させるスケールイン・アウトではなく、ノードスペックを増減させるスケールアップ・ダウンを採用しました。

理由

  • ElasticCloudは2(+1 TieBreaker) or 3台構成しかできない(低可用性なら1台でも可)
    • シャード・レプリカ構成上、ノード数を2~3と変動させるとややこしい
      • 負荷・ディスク占有が不均一になる可能性がある
      • シャードの再編成が走りデータ通信量が大きくなりコストがかかる

リソース負荷トリガーではなくスケジュールトリガー

スケジュールトリガーを採用しました。

理由

  • スケジュールトリガーの方が開発工数が小さい
  • StailerのElasticsearchの特性
    • 更新リクエストのピークタイムは固定、検索リクエストのピークタイムもほぼ一定のため
    • データ更新バッチが走り始めると一気にCPU負荷が高騰するため、CPU負荷が高騰に合わせてスケールアップするより事前にスケールアップしておいた方が好ましい
      • ElasticCloudのスケールアップは1台ずつクラスタから切り離されスケールアップされていく
        • 通常3ノードでリクエストを捌くところ一時的に2ノードでリクエストを捌く必要がある
          • そのため、高負荷時にスケールアップするとさらに負荷が高騰するリスクがある

設計

cloud-sdk-goを利用したGoスクリプトをDocker ImageにしGKE CronJobで実行し、定期的なスケールアップ/ダウンを実施しました。

アーキテクチャ図は以下です。

他の選択肢とPros/Consは以下です。 #2を採用しました。

# スケール方法 定期実行方法 Pros Cons 選択候補になった理由
1 Terraform + GitHub ActionsでPull Requestマージ GitHub Actions • 宣言的に管理
• Extensionsもコード管理できコンソールでの手作業が減る
・現状ElasticCloudのTerraform管理していないし、PR自動作成&マージは工数大
・GutHub ActionsのSLAに不安あり
クラスタのスケールアップ・ダウン方法はTerraformが一番良いと考えた。Pull Request作成&マージはGitHub Actionsが親和性が高い。
2 cloud-sdk-go GKE CronJob ・既存で多くCronをGKEで動かしており運用しやすく可用性も安心
・Go(プログラミング言語)で操作できる
・Go用のDocker ImageのBuild/Deployを実装する必要あり クラスタのスケールアップ・ダウン方法はTerraformの次点でcloud-sdk-goと考えた。GitHub Actionsと親和性がない方法にするのであれば別の定期実行方法にしたい。
3 cloud-sdk-go Cloud Functions + Cloud Scheduler ・CloudSchedulerはマネージドであり可用性が高い
・モニタリング・監視もしやすい
・Go(プログラミング言語)で操作できる
Cloud FunctionはGoランタム対応
・CloudSchedulerはこれまで利用されておらず、管理対象が増える
・処理時間の要件上、Cloud FunctionsをHTTP経由でフックするしかなくその場合デプロイが複雑
・長時間Functionsを動かす必要がありGoogleが期待するFunctionsの用途からずれていそう
同上

スケールアップが失敗すると、検索機能に障害が発生する可能性が高く、高い可用性が必要です。 しかしGitHub ActionsでのCronはこれまでの実績的に信頼が低いため、#1は選びませんでした。

また、当初Cloud Scheduler → Pub/Sub → Cloud Functionsも選択肢に入れていましたが、timeoutが最大10分なので選択肢から棄却しました。 スケーリングは10分以上かかってしまうので、成功確認できず失敗した時の検知やリトライの仕組みを別途作る必要がありました。

cloud-sdk-goでの処理

処理の流れは以下です。

注意すべき点は2点あります。

  • ノードのスケールダウン時はディスク容量も小さくなるので、スケールダウン後のディスク容量がパンパンになりすぎないかチェックする必要があります。ElasticCloudではディスクがパンパンになるとインデックスが自動でクローズしノードが機能しなくなることがありました。同様にスケールアップするときもディスク容量に一定の空きが必要です。おそらくシャードのコピーが走るためかと思います。
  • スケールプロセスでは既存のElasticsearchクラスタの設定をGET -> 設定の中のメモリ部分を書き換え -> 書き換えた設定をPUTという処理が必要になります。ここが複雑であるため、生APIを使ったシェルスクリプトでなく、cloud-sdk-goを使うと嬉しい点です。

モニタリング

スケールアップが失敗すると、弱いスペックのノードのまま高負荷状態を迎えることになります。 これによりElasticsearchがパンクし検索機能が動かなくなり、大きなユーザー影響につながるかもしれません。

CronJobでリトライを設定しJobの失敗はリトライするようにし、複数回失敗した場合にはDatadogで検知しSlack + PagerDutyで通知するようにしました。 実際に、リリースしてから3日後にElasticCloudの偶発的な問題によりスケールアップが失敗しリトライが走ることがありました。 リトライを設定しておいて良かったです。

おわりに

本施策により、本番Elasticsearchクラスタのコストを40%程度削減できました。 以下のグラフは、縦軸がElasticsearchクラスタごとのコスト、横軸が時間です。

緑の折れ線が対象のElasticsearchクラスタです。 元々のコストが左の区間、その後スケールアップしコスト増加したのが中央の区間、本施策導入後が右の区間です。 中央の区間で一時的にコストが下がっているのは、別の施策を試してみたが上手くいかなかったので戻した痕跡です。

また、本施策導入後もサービス品質を落とさずに運用できています。

余談ですが、Elasticsearch 7.17から8.12へアップデートも最近実施したのですがパフォーマンスが向上しさらにインスタンスサイズをスケールダウンできコスト削減できました。 おまけにレイテンシーも良くなりました。