-DBコスト爆弾を解決した救世主:S3ベースのデータティアリング導入記-
増え続けるサービス、そして静かに迫るDBコスト爆弾
現在進行中のプロジェクトでは、サービスの数が増え、多くの開発項目が追加されていました。その結果、幾何級数的に増加するデータと、それに比例して高騰するインフラコストに直面することになりました。
特にマイクロサービスアーキテクチャ(MSA)を採用していたため、ログの増加速度は想像を絶するものでした。たった一度のユーザーリクエスト(Request)であっても、それを処理するためにゲートウェイを経由し、数多くのマイクロサービス間で内部の分散呼び出しが連鎖的に発生したからです。システム全体のトランザクションを追跡・監視するため、各独立サービスで分散ログを漏れなく記録する必要がありました。その結果、モノリシック構造とは比較にならないほど膨大な量のログデータが、同時多発的に生成されることになりました。
当初、こうした分散ログの保存先としてMongoDBを選びました。MSA環境ではサービスごとにログの形式が少しずつ異なる場合がありますが、MongoDBの柔軟なドキュメント(Document)構造のおかげで、スキーマ変更の負担がまったくなかったためです。また、大量のログが複数のマイクロサービスから同時多発的に流れ込んでも問題のない高速な書き込み性能と、ダッシュボードとの連携のしやすさも大きな魅力でした。
しかし、マイクロサービスの数が増え、サービスが急速に成長するにつれて、毎月数百GB程度だったログデータはいつの間にか数TBに膨れ上がり、データベースの容量を瞬く間に圧迫し始めました。さらに深刻だったのは性能の問題です。MongoDBは性能を維持するため、インデックスや頻繁に使用するデータをメモリ(RAM)に載せておくWiredTigerキャッシュエンジンを使用します。ログデータが増えるとインデックスのサイズも膨らみ、限られたRAM容量を超過したことで、次のような深刻な連鎖反応が起こりました。-
-
RAMの圧迫とディスクI/Oのボトルネック:メモリが不足すると、データベースがディスクからページを読み書きする頻度が急増し、すぐに本番サービスのクエリ遅延につながりました。
-
レプリカセット(Replica Set)によるコストの3倍増:可用性を確保するため、高性能SSDを搭載したEC2インスタンス3台でクラスターを構成していたため、1TBのログを保存するのに、実際には3TB分の高額なストレージ費用を支払う必要がありました。
運用状況を分析したところ、ログ参照リクエストの大半は直近3日以内のデータに限られており、それ以前の過去ログは障害発生後の分析(ポストモーテム)や規制監査など、断続的な目的でしか使われていませんでした。高価なDBストレージとRAMを占有している古いデータを切り離し、インフラコストを大幅に削減することを決断しました。その解決策として、性能とコストの両方を最適化できるアーキテクチャであるデータティアリング(Data Tiering)を導入しました。
データティアリング(Data Tiering)アーキテクチャの設計
データティアリングの要点は、すべてのデータを同じストレージに詰め込むのではなく、アクセス頻度(Access Frequency)とビジネス要件に応じてデータの保存先を最適化することです。運用負荷(Overhead)を最小限に抑えつつ、コスト削減効果を最大化するため、複雑な多段階の階層ではなく、Hot TierとCold Tierの2つに絞って、次のようにストレージをすっきりと分けました。
- Hot Tier(直近30日):リアルタイムデバッグ向けの高性能領域
-
ストレージ:MongoDB
-
目的:ライブエラー監視、直近でリリースした機能の問題追跡、MSAの分散トレーシング
-
設計方針:1か月以内のデータは、開発チームと運用チームが障害対応やデバッグのために最も頻繁に参照する「ホットデータ」です。そのため、性能向上を最優先します。
- Cold Tier(31日~1年):規制遵守とアーカイブ向けのコスト削減領域
-
ストレージ:AWS S3 Standard
-
目的:数か月前に発生したまれなバグの追跡、長期的なデータ統計分析、セキュリティおよび法的監査への対応
-
設計方針:1か月を過ぎたログは、参照頻度が急激に下がる「コールドデータ」です。この領域ではインデックスをすべて取り除き、データを圧縮ファイルとしてS3 Standardに保存します。S3 Standardにはデータのインデックス作成費用がかからず、容量あたりのコストもMongoDBに比べて10分の1から25分の1以上安くなります。
この2-Tierアーキテクチャモデルが持つ、エンジニアリング上の最大の価値は、ストレージ(Storage)とコンピューティング(Computing)を完全に分離した点にあります。従来の構造では、ログ容量が増えるとディスク容量だけでなく、肥大化したインデックスを処理するためにMongoDBサーバーのスペックまで強制的にスケールアップ(Scale-up)する必要がありました。つまり、単にデータを「保管」するだけのために、高価な演算装置とメモリのコストまでセットで支払っていたのです。しかし、30日を過ぎたコールドデータをS3というサーバーレスのオブジェクトストレージに移すことで、2つのリソースを独立して拡張できる基盤を整えました。MongoDBには常に予測可能な範囲である直近30日分のデータだけを保持するため、最小限の固定費で最高のパフォーマンスを維持できます。一方、数テラバイトに及ぶ過去データには、安価なS3の容量を無制限に活用することで、インフラの拡張性(Scalability)を最大限に高めました。
自動化パイプラインの構築
どれほど理想的なアーキテクチャを設計しても、データの移行や削除にエンジニアの手作業が必要であれば、持続可能とは言えません。30日を過ぎたログをMongoDBから安全に削除し、S3 Standardにすっきりとアーカイブする自動データパイプラインを構築しました。データパイプラインは、主に次のような構成です。
-
MongoDBから30日を経過したログを見つけてデータを加工
-
S3にデータをバックアップし、バックアップデータを1年後に自動削除するよう設定
-
バックアップしたデータをMongoDBから削除
実際の運用環境では、1日に数千万件ものログが流れ込むため、これらのデータを一度にメモリに読み込むと、すぐにOOM(Out Of Memory)障害につながります。そのため、メモリ負荷を軽減するために、次のような方針を立てました。
-
カーソルベースのページネーション(Pagination):データを数千件単位(2,000件)に分割し、安全に取得します。
-
ディスクへのストリーミング(Streaming Out):DBから読み込んだデータをメモリに保持せず、ローカルの一時ファイル(Temp File)にストリーミングし、ファイルシステムへ直接書き込みます。
-
バッチ削除(Batch Delete):移行(Migration)が完了したIDを集め、ディスクに負荷をかけないよう10,000件ずつに分けて安全に削除します。
この重要な処理の流れを組み込んだ、マイグレーションパイプラインのコードは次のとおりです。
private <T> void executeLogMigrationPipeline(...) {
List<String> logIds = new ArrayList<>();
// 1. 메모리 오버헤드를 방지하기 위한 임시(Temp) 파일 생성
Path tempFile = Files.createTempFile(filePrefix, ".json");
// 2. Jackson SequenceWriter를 활용해 디스크로 즉시 스트리밍 (JSON Array 구조)
try (var fileOut = Files.newOutputStream(tempFile);
var seqWriter = objectMapper.writer().writeValuesAsArray(fileOut)) {
int pageCounter = 0;
int pageSize = 2000;
while (true) {
// DB에서 정해진 배치 사이즈만큼 순차 조회
Pageable pageable = PageRequest.of(pageCounter++, pageSize, Sort.by("registrationTime"));
List<T> logs = logFinder.apply(targetEpochMillisToMove, pageable);
if (logs.isEmpty()) break;
// ID 수집 및 파일에 즉시 기록 (메모리 점유 방지)
logIds.addAll(logs.stream().map(idExtractor).toList());
LogHelper.appendLogsToJsonStream(logs, seqWriter);
if (logs.size() < pageSize) break;
}
}
// 3. S3 스토리지(Depot)로 안전하게 파일 업로드
if (!logIds.isEmpty() && Files.exists(tempFile)) {
depotProxy.uploadDTSPFiles(remoteFilePath, new FileSystemResource(tempFile.toFile()), targetEpochMillisToRemoval);
// 4. 대량의 DB 쓰기 부하를 방지하기 위해 10,000건씩 쪼개서 삭제(Bulk Delete)
int deleteBatchSize = 10000;
for (int i = 0; i < logIds.size(); i += deleteBatchSize) {
List<String> subBatchIds = logIds.subList(i, Math.min(i + deleteBatchSize, logIds.size()));
batchRemover.accept(subBatchIds);
}
}
// 5. 작업이 끝나면 로컬 임시 파일 자원 해제 (Retry 로직 포함)
cleanUpTempFile(tempFile);
}
ジェネリック(T)と関数型インターフェース(BiFunction、Consumer)を活用して設計したおかげで、ログの型や形式が異なっていても、同じマイグレーションパイプラインのロジックを共通して再利用できる拡張性も確保できました。MongoDBに保存するログデータを常に直近30日分に限定したことで、肥大化していたインデックスのサイズがRAM容量内にすっきり収まるようになりました。その結果、ディスクI/Oのボトルネックが完全になくなり、本番環境でのクエリ平均応答速度(Latency)が向上しました。また、従来高額だったDBストレージ費用も削減できました。1TBあたりのコストを比較すると、S3 StandardはMongoDBクラスターより圧倒的に安く、ログインフラ全体のコストを大幅に抑えることができました。
おわりに
多くのサービスでは成長期に入ると、データの増加に合わせてデータベースのスペックをむやみに引き上げる(Scale-up)ことで、ひとまず対応しようとしがちです。しかし、データのビジネス価値やアクセス頻度は、時間の経過とともに必ず変化します。今回のS3ベースのデータティアリング導入は、単にコストを少し削減しただけではなく、ビジネス要件と技術コストの不一致をアーキテクチャによって解決した経験でした。最も高価で大容量のストレージが、常に最適な選択とは限りません。システムが扱うデータの性質や温度を把握し、適切な場所に配置すること。それこそが、インフラコスト爆弾を解決するエンジニアの本当の役割ではないでしょうか。増え続けるデータとインフラコストに悩んでいる方々にとって、この記事がささやかなヒントになれば幸いです。
Hustle Paul