こんにちは!プロダクト開発部データ価値基盤チームの関口です。今回の記事では、私たちが普段どのように開発を進めているのかをご紹介したく、私の所属するチームで取り組んだバッチ開発の事例について書きます。今回は設計について、どんなことを考えてどう決めていったかを詳述します。普段の弊社の開発がどのように進んでいくのかを知る一助になれば幸いです。
- 解決すべき課題
- 先に結論、こういう構成にしました
- 設計のポイント
- まとめ
解決すべき課題
今回開発したバッチは、弊社のICTプロダクトを通してお客さまからお預かりしているDBデータを保全するための月次のメンテナンスバッチです。以下が大まかな要件になります。
- 保全対象のデータは月初に登録される条件をもとにしてバッチが検索して特定する
- 対象になるデータ数は月によって変動するが、ひと月あたり数千万レコード程度
- 保全対象になったデータは20日以内にメンテナンスを終えること
また、以下のような非機能要件もあり、それも設計に織り込んでいきます。
- 稼働中のサービスの性能に影響を与えない
- 万が一、メンテナンスを誤った場合のコンティンジェンシープランを用意する
- メンテナンスするデータは秘匿性の高い情報が含まれる可能性もあるため、細大漏れなくメンテナンス内容が後から監査/追跡できるための記録をとる
大量のDBレコードの更新が発生するため、基本的にはサービスの利用ユーザが少ない深夜の限られた時間帯で稼働するバッチになります。
先に結論、こういう構成にしました
イベントストーミング図

毎月初の日中にメンテナンススケジュールを参照してメンテナンスデータを特定し、夜間にバックアップを取得した上でメンテナンスしていきます。やることをそれぞれ「フェーズ」と呼称する概念で分割し「メンテナンス完了フェーズ一覧」で進捗を管理します。各フェーズに該当する機能はスケジュールで起動され、その進捗を見て自分のタスクを把握し、稼働時間内に可能な限り自分の仕事を消化して完了を記録していく形になります。
なお、イベントストーミング図を用いているのは、バッチがpub/subによるイベントソーシングで実装しているためではなく、バッチの流れを容易に記述したい意図によるものです。実際の構成は管理するインフラ構成を最小にするため、DBに記録したイベントを後続の処理がポーリングする構成になっています。
構成図

本番ではAthenaを使ってDBのバックアップから対象を特定してS3に保存し、それをもとにサービスのRDS上にバックアップやメンテナンス内容を書き込んでいく構成です。一方、ローカルではAthenaを利用せず、localstackのS3とMySQLだけの構成です。gaugeのインテグレーションテストをもとにコンテナの各フェーズのコマンドをひとつひとつテスト駆動開発していきます。
以下、この構成に至るまでに重点的に検討した点をご紹介していきます。
設計のポイント
メンテナンス対象データを日中に特定したい
メンテナンス対象データの特定では大量の検索クエリをDBに対して送信することが見込まれます。DBへのアクセスはreadのみで済むので、なんとか工夫してサービスに無影響のまま日中に実行できないかを検討しました。日中であれば有事の際に即応できるし、開発者の負担も減るためです。以下が検討した案です。
案1 リードレプリカを処理時間帯で一時的に増やす
弊社はAWSのAurora MySQLを利用しているので、比較的容易にこの対策で量を捌けるようにすることが可能です。しかし、既にありったけのBuffer Poolをフル活用して稼働している弊社のサービスでは、メンテナンス対象データ特定用の大量のクエリが押し寄せることには懸念があります。このクエリ群にMySQLのBuffer Poolの多くが占有されてしまい、サービス側のクエリの処理速度が劣化する危険があるためです。
案2 バッチ専用のレプリカを立てて、それを利用する
この方法であれば稼働中のサービスからBuffer Poolを隔離することができますが、PHPのAPIサーバーのDB接続プーリングのためにRDS Proxyを利用している関係で、実現できないことが判りました。
案3 AthenaとS3に保管しているDBのバックアップを利用する
弊社では、ユーザからの問い合わせで実データの調査が必要になった場合に備え、日に数回DBのバックアップを取得してS3にexportしておき、本番DBにつながずAthenaから検索できるようしています*1。これを利用し、メンテナンス対象データ特定時に最直近のバックアップをAthenaからデータをクエリする案です。これであれば、サービスには無影響であることを担保しつつ、大量の検索を捌くことができます。RDBMSで生じがちな遅いクエリを探したりチューニングする、といった面倒な作業も不要です。テーブル定義はGlue Crawlerが作成してくれるものがあるため、追加で開発が必要なものもありません。
結果、DBを参照するのと比較しても問題にならない精度でメンテナンス対象データを特定できることも分かったので、メンテナンス対象データは案3のAthenaを利用してバックアップから特定する方式を採用しました。
| 実現方式 | サービス影響 | 特定の精度 | 実現可否 |
|---|---|---|---|
| リードレプリカ増設 | ありそう(Buffer Poolが汚れる) | ◎ | 可 |
| 専用リードレプリカ | なし | ◎ | 否 |
| Athenaでバックアップ検索 | なし | ⚪︎ | 可 |
インテグレーションテストをローカルで回せるようにしたい
Athenaを利用して日中に処理ができるようになった一方で、ローカルでインテグレーションテストを完結させることには課題が生じました。S3であれば無料版のlocalstackで精度高く環境を用意できるのですが、肝心のAthenaの機能は有料版にしかありません。コドモンの開発ではXPを原則として実践しているので、インテグレーションテストの早さや手軽さは重要になります。
案1 MySQLでAthenaを代用する
メンテナンス対象データの特定に利用するデータアクセス層を抽象化し、ローカルではMySQLとAWS SDKを用いてAthenaの動作を模倣する実装を利用する方法です。軽量で一般的かつ無料のDockerイメージをもとに環境構築できます。テストデータのセットアップもDBに対する操作になるので弊社QA謹製のテストフレームワークが利用でき、比較的容易に実装できそうです。コストも気にする必要がなく、ローカルに閉じてテストが可能です。
但し、ローカルと本番で動作するプロダクションコードに若干の差が生じてしまうのと、PrestoとMySQLで実行できるクエリの差異がありますので、そこをどう考えるかがこの方式のいちばんの争点になります。
案2 localstack有償版を利用する
こちらもDockerイメージがあるので環境構築は手軽にできます。また、Prestoのエンジンを利用できるので本番との差異がほとんどない形でテスト可能です。
一方で、テストのセットアップが最大の難点になりそうです。Athanaのデータベースおよびテーブルを作成し、S3にバックアップのCSV(もしくはParquet)をアップロードする必要があり、非常に悩ましいポイントです。テーブル定義やCSVを手動で作成して置くとなると、サービス側のテーブルの変更に追随するための仕組みをどうするべきかが大きな問題になります。かといって、本番さながらにDBにデータを入れてスナップショットを取ってRDS ExportしてGlue Crawlerを回して、セットアップの完了を検知してテストをキックして…という仕組みを作るのはあまりにも大掛かりで、実装にもテストの実行にも時間がかかりそうです。もし後者の仕組みを採用した場合は、データのクリーンアップが気軽にできないので全てのケースで一つのデータを共有することになり、テストケース間の独立性にも疑問符がつきそうです。
コストも気になります。Athenaをサポートするプランでは1ライセンスで月に$89とあり、たった一つのバッチのテストのためにしてはかなり高額になってしまいます。CI環境で利用するためにカスタムしたイメージをECRのプライベートレジストリにあげていいのかとか、Github Actions上での利用制限など、ライセンス関連の制約も気になるところです。
案3 AWSに専用の環境を構築する
こちらは実際の構築になるので、弊社だとterraformでAWS上にリソースを一式用意する形になります。比較的手はかかりますが、実際のマネージドサービスを利用するため、本番と同等のテストが可能です。コストも、localstackに比べれば十分許容できる範囲でしょう。
一方、こちらはライセンス関連の悩みこそないものの、テストのセットアップについてはlocalstackと同じ悩みが付きまといます。また、CI環境上でテストを動作させるためにはセルフホステッドランナーを立ててIAMを付与して…などの考慮も別途必要になってきます。何より悩ましいのは、実際の環境を構築してしまう時点でほぼシステムテストに等しく、ここまでしてインテグレーションテストとして作る意味があるのか?という問いです….
検討の結果、案1のMySQLを利用する方式を採用しました。他の方式ではテストのセットアップの困難さが致命的であったことと、今回の要件では検索クエリが標準SQL程度の簡易さで十分ありMySQLとAthenaの差異があまり問題にならないことを踏まえての判断です。ローカルと本番の差異が問題ないかについては実際のAWS上の環境を利用したシステムテストで担保することとしました。この構成のおかげで、CIも必要なコンテナを集めてdocker composeだけで完結するため、非常にシンプルかつ短時間でフィードバックが得られるようになりました。
| 観点 | MySQL | localstack有料版 | AWS上に構築 |
|---|---|---|---|
| ローカルで完結 | ⚪︎ | ⚪︎ | x |
| フィードバックの速さ | ◎ | △ | △ |
| 本番の再現度 | △ | ⚪︎ | ◎ |
| コスト | ◎ | △ | ⚪︎ |
| 構築容易性 | ◎ | ◎ | ⚪︎ |
| テストの実装容易性 | ⚪︎ | x | x |
メンテナンスを誤った場合のコンティンジェンシープランの策定
これに対しては、メンテナンス前の何らかのバックアップを用意し、必要なデータを指定してリストアする仕組みを用意することにし、以下の方法を検討しました。
案1 RDSのスナップショットを利用する
この方式であればバックアップを取得/削除する処理は既にあるので新規に作成する必要がありません。しかしながら、リストアの方式には注意が必要です。スナップショットはDB全体のデータが含まれるため、メンテナンス対象でないデータも含まれてしまっています。単純にサービスのDBでそのままスナップショットを復元することはできません。やるなら別のRDS Clusterを立ててスナップショットを復元し、そこから復元対象のデータを抽出して何らかの方法でもとのDBに流し込むことになります。弊社のDBはデータサイズがとても大きいため、スナップショットの作成にも復元にもそれなりの時間がかかることを覚悟しなければなりません。
案2 バックアップのCSVをAthena参照で利用する
この方式はスナップショットを利用する方式と同様、バックアップを取得/削除する処理は既にあるものを利用できます。それだけではなく、スナップショットを復元する手間を省ける利点があります。しかしながら、やはりリストアにあたって復元対象のデータを抽出して何らかの方法でもとのDBに流し込む処理が必要となります。Athenaの抽出結果はCSVでS3に出力されるので、それをMySQLのDBに入れるとなると、流しこみのための仕組みづくりが必要になります。
案3 サービスのDBにバックアップテーブルを作成する
この方式では先の二つの方式と違って同じDBにテーブルとしてバックアップがあるので、ファイルなどの出力/取込などをせずにSQLだけで非常に簡易かつ正確にリストアすることが可能です。
その一方で、バックアップの取得/削除処理は別途実装する必要があります。また、取得/ 削除ともにサービスのDBへ書き込みが必要となるため、日中の実行は諦めることになります。
結果として、リストア時間と精度を担保できる案3の「サービスのDBにバックアップテーブルを作成する」を採用しました。バックアップの取得/削除をするための実装が増えてしまいますが、メンテナンス対象を誤った場合のユーザ影響を考えるとリストアが「早く、正しい」のが最善との判断です。安全のため、バックアップテーブル群はサービス側と同じDB内ではありますが、別の専用MySQLデータベース(他製品のスキーマのようなもの)とユーザを新規に追加し、サービス側からは利用できないよう論理的に隔離する設計としました。
| 観点 | DBのスナップショットから復元 | Athenaで復元 | DBにバックアップテーブルを作成 |
|---|---|---|---|
| バックアップ取得/削除実装工数 | ⚪︎ | ⚪︎ | △ |
| リストア工数 | × | △ | ⚪︎ |
| リストアにかかる時間 | × | △ | ⚪︎ |
| リストアの精度 | △ | △ | ⚪︎ |
データベースユーザーをバッチ専用に追加する
一連のバッチの処理でDBにアクセスする際に利用するユーザーをバッチ専用に払い出します。これは、先ほど作成したバックアップ専用のデータベースへの接続が可能な唯一のユーザーとして定義します。サービス側のデータベースへはメンテナンスに必要な最小の権限を与え、このユーザーに対してMySQLのauditログを有効にします。これにより、非機能要件として定義した「メンテナンス内容が後から監査/追跡できるための記録」を残せるようになりました。
まとめ
うまくいった点
- 全体でやることをフェーズ分割した
- フェーズ毎にコンピューティングリソースや並列度、開始終了条件などを柔軟に調整できるようなった
- シンプルにフェーズがそのままコマンドのインターフェースになり、そのままインテグレーションテストやストーリーの単位になった
- 単機能に分けてIN/OUTを明確にすることで、テストコードもプロダクションコードもシンプルになった
- ローカルでインテグレーションテストが完結する
- 早い/安い/安定
- CIもローカルと全く同じDocker Composeで回せるため、パイプラインの構築が比較的楽だった
- setup/teardownが容易なのでケース毎にデータの作成・破棄ができ、ケース間の独立性が保てる
- 結果、開発の速度が上がり、予定になかった「もしもの時のリストア」もコマンドとして実装できた
受け入れたリスク
- インテグレーションテストでAthenaを利用した検証ができない
- メンテナンス対象特定時に、Presto固有の関数や構文はインテグレーションテストにパスしないので、今のままでは利用できない
- インテグレーションテストだけではAthena周りの動作の再現性がやや低いので、メンテナンスデータ対象特定周りを修正する場合はシステムテストを回す必要がある
- フェーズ分割により、メンテナンスの進捗状態の管理が必要になった
- 各フェーズの先頭でready状態のタスクを判断する微妙に複雑な処理を実装せざるを得なかった
- 今は単純にシリアルに進めていくだけで済むが、もう少し複雑になることがあればフロー制御のための仕組みを導入する必要があるかもしれない
- バックアップ関連で夜間実行すべきフェーズが2つ増えた
- 定期で作成/削除しているDBバックアップを利用しないので、自前で冗長にバックアップを作成/削除するフェーズを実装した
うまくいかなかった点
- メンテナンス処理時間を過剰に見積もってオーバーエンジニアリングした
- メンテナンスの処理に何日もかかる想定でいたが、実際に負荷試験で大きなメンテナンスをシミュレートしてみたら一夜で終わった
- 時間が来たらグレースフルに終了する、翌日以降に処理を引き継げるように細やかなステータス管理、と割と頑張ってしまった
- もっと前段階でメンテナンス処理だけでも負荷試験していたら、いらない複雑さを持ち込まずに済んだかもしれない、という学び
- 負荷試験でAthenaの大量クエリが失敗した
- 一番心配していたAthenaのレートリミットではなく、KMSの方がレートリミットに引っかかって原因が分からず途方に暮れる
- バックアップデータにカスタマーマネージドキーを利用していたため、クエリの度にスキャンデータにKMS APIが走っていた
- 保管先のS3でバケットキーにそのカスタマーマネージドキーを設定したことでKMS APIの呼び出しが減り、負荷試験を無事終えられた
- しかしながら減ったとはいえ依然大量のKMS API呼び出しが発生するためにテスト環境のアカウントのKMS利用コストが跳ね上がってアラートが上がり、弊社SREよりお咎めが入る
- 本番でこの料金が発生したとてROIを考えたら適正なコストであることの説明がついたこと、「もうしません、どうしてもするなら事前に相談します」との陳謝により無罪放免
- Athenaに大量のクエリを投げるときはKMSに注意しましょう、という学び(危うく開発の最終局面でリアーキテクトが必要になるところだった…)
いかがでしたでしょうか。至らない点はたくさんあると思いますが、我々が作る上で大事にしていること、日頃から考えていることは大体こんな感じです。共感した!という方も、いや、もっと大事にするべきこんな軸や考え方がある!という方も、ぜひ一度弊社のイベントやカジュアル面談にお越しください。お待ちしております。
*1:当然ながら、開発者によるAthena検索は本番DBと同様の監査の下で行われます