1. 問題の始まり:ストレージコストの増加
サービスの運用期間が長くなるにつれて、MongoDBのストレージ容量も継続的に増加しました。
ビジネス要件上、すでに生成されたデータを削除することはできませんでしたが、すべてのデータを通常のストレージに継続して保持することは、コスト面で大きな負担になりつつありました。
特にOnline Archiveの適用対象となった特定のコレクションにはサービスの中核データが含まれており、データが継続的に蓄積されることで、約1.2TBの規模まで増加した状態でした。
これにより、単なるストレージコストの問題ではなく、長期的なデータ運用戦略そのものを見直す必要がある状況になりました。
そこで、一定期間が経過したデータを別のストレージへ移行する方法を検討し、MongoDB Online Archiveを導入することに決定しました。
- 作成後365日が経過したデータはOnline Archiveへ移行する。 -
この基準は、クエリパターンの分析に基づいて精緻に設計された値というよりも、ストレージコストの最適化を目的とした運用ポリシーに近いものでした。
ただし、Online Archiveにはストレージコストを削減できるというメリットがある一方で、クエリ時には別途コスト構造を考慮する必要があるという特徴がありました。
つまり、
何をArchiveするのか?
だけでなく、
Archiveされたデータをどのようにクエリするのか?
についても同時に検討する必要がありました。
2. Online Archiveの設計
Archiveポリシーは次のように構成しました。
Archive基準:timeフィールドを基準とした365日経過データ
Partition構成:dataId → time
実際のサービスにおける主要なクエリパターンは次のとおりでした。
特定のdataIdに対する全データの取得
そこで、同じdataIdのデータを優先的にグループ化できるようにdataIdを先行キーとして設定し、Archiveの基準となるtimeを後続キーとして構成しました。
これにより、実際のクエリパターンとArchiveポリシーの整合性を合わせることを目指しました。
また、Online Archive環境ではPartition基準を考慮したクエリがData Scannedの削減に有利になる可能性があるため、コスト最適化の観点でも重要な設計要素であると判断しました。
3. Archive運用のためのインデックス戦略
Archiveの導入にあたっては、単にArchiveポリシーを定義するだけでなく、Archiveの実行プロセスと実際のクエリパターンまで考慮してインデックスを構成しました。
3-1. Archive対象を識別するためのインデックス
{ time: 1, dataId: 1 }
Archiveポリシー上、作成後365日が経過したデータを継続的に識別する必要があったため、
Archive対象のデータを効率的に識別するため、専用のインデックスを作成しました。
3-2. Archive後のクエリのためのインデックス
サービスの主要なクエリパターンはdataIdを基準とした全データの取得であり、
Online ArchiveのPartitionも次のように構成しました。
{ dataId: 1, time: 1 }
そのため、統合Endpointを通じてArchiveデータも併せてクエリする場合を考慮し、Partition構成と同じ順序のインデックスを作成しました。
これにより、実際のクエリパターンとArchive構造の間の整合性を維持することを目指しました。
まとめると、次のとおりです。
|
目的 |
インデックス |
|---|---|
|
Archive対象の識別 |
{ time: 1, dataId: 1 } |
|
Archive後の統合クエリ |
{ dataId: 1, time: 1 } |
4. 既存の大容量クエリAPI
私たちのサービスには、特定の識別子に対する全データを返すAPIが存在しました。
GET /data/{dataId}
このAPIは単純なクエリAPIではありませんでした。
クエリしたデータをそのまま返すのではなく、レスポンス仕様に合わせたデータ加工まで実行する必要がありました。
MongoDBクエリ → レスポンスデータの加工 → クライアントへの返却
また、データの分布も均一ではありませんでした。
|
dataIdごとのデータ規模 |
件数 |
|---|---|
|
一般的なケース |
数十~数千件 |
|
大容量ケース |
最大数十万件 |
つまり、同じ API であっても、特定の dataId に対しては大量データの処理が必要な構成でした。
5. 従来の取得方式
従来は OFFSET ベースの反復取得方式を使用していました。
例えば、次のような構成でした。
LIMIT 1000 OFFSET 0
LIMIT 1000 OFFSET 1000
LIMIT 1000 OFFSET 2000 ...
一般的な MongoDB 環境では、大きな問題はありませんでした。
しかし、Online Archive の導入後は、従来の取得方式が Archive 環境でも適切かどうかを改めて検討する必要がありました。
6. OFFSET ベースの取得を見直した理由
Online Archive 環境では、Data Scanned の量がコストに影響する可能性があります。
つまり、
Data Scanned の増加 → 取得コスト増加の可能性
という関係を考慮する必要がありました。
従来の OFFSET ベースの取得では、目的の位置に到達するために、それより前のデータを繰り返し探索する必要があります。
例えば、
LIMIT 1000 OFFSET 90000
のような取得では、結果を返すために以前の 90,000 件をスキップする必要があります。
大量データを複数回に分けて取得する構成では、このような探索コストが繰り返し発生します。
Archive 環境では、このような反復探索が Data Scanned の増加につながる可能性があると判断しました。
OFFSET → 反復取得 → 反復探索の発生 → Data Scanned の増加 → 取得コスト増加の可能性
最終的に、従来の OFFSET ベースの取得戦略を見直すことになりました。
7. Cursor ベースの取得への移行
新しい取得方式として、Cursor ベースの取得を選択しました。
Spring Data MongoDB の Stream 機能を活用し、データを順次消費する構成にしました。
@Meta(cursorBatchSize = 3000)
Stream<DataDoc> streamByDataId(String dataId);
また、cursorBatchSize も併せて適用しました。
今回のケースで batchSize を使用した理由は、大量データを一度にメモリへ読み込むのではなく、一定のサイズ単位で消費するためでした。
Cursor → batch 単位で取得 → 順次消費 → メモリ使用量を制御
従来の構成と比較すると、次のようになります。
OFFSET → 反復取得 → 反復探索 Cursor → 順次取得 → 単一取得のフローを維持
Archive 環境では、このような構成のほうがコスト最適化の面でも適していると判断しました。
8. 振り返り
今回の経験を通じて、Online Archive の導入は単なるストレージコスト削減の作業ではないことを確認できました。
ストレージ戦略が変われば、取得戦略も併せて見直す必要があります。
特にコスト構造が変わる環境では、従来の方式が常に最適であるとは限りません。
今回のケースでは、Archive ポリシー、インデックス構成、取得パターン、取得戦略までを併せて考慮する必要がありました。
ストレージコストの最適化
→ Online Archive の導入
→ Partition の設計
→ インデックス構成
→ 取得コストの考慮
→ OFFSET の見直し
→ Cursor ベースの取得への移行
結局、今回のケースは単に Cursor を適用したという話ではありませんでした。
ストレージコストの最適化を目的に始めた作業が、取得コストについて考えるきっかけとなり、その過程で従来の取得戦略を改めて見直すことになりました。
インフラの変化がアプリケーションの取得戦略の変化をもたらし、これを通じて、ストレージ戦略と取得戦略は密接に結び付いているということを改めて実感できました。
参考資料
https://www.mongodb.com/docs/atlas/online-archive/
https://www.mongodb.com/docs/atlas/data-federation/billing/
https://www.mongodb.com/docs/manual/reference/method/cursor.skip/
ヨプ