Out Of Memory解決記
低スペックコンテナ環境におけるSpring Bootサービスの安定性確保方法
クラウド環境の発展により、サービスを簡単にデプロイできるようになりましたが、限られたリソース内でアプリケーションの安定性を維持することは、依然としてエンジニアにとって重要な課題です。特にRailwayのようなPaaS環境のフリーティアで提供される1GB前後のメモリは、Spring Bootアプリケーションを動作させるにはやや挑戦的な数値です。
本投稿では、1GBメモリ環境で発生したOutOfMemory(OOM)問題を診断し、これを解決するために行ったJVMチューニング、コネクションプールの最適化、そしてバッチプロセスの改善過程を共有したいと思います。
1. 問題状況の認識と原因分析
初期設定では、アプリケーションが間欠的にプロセスが強制終了される現象を示しました。原因は、コンテナが許容するメモリ閾値を超えて発生したOOM Killでした。主な原因は以下の3つに要約されました。
-
過度に高いHeapメモリ占有率: 既存の設定は全体メモリの75%をHeap領域に割り当てていました。これはMetaspace、Stack、Code Cacheなど、JVMのNon-Heap領域とOSの最小動作メモリを考慮していない設定でした。
-
ビルド段階の過度なリソース使用: Dockerビルド時、Gradleデーモンがバックグラウンドで動作し、かなりのメモリを占有しました。
-
バッチ作業のメモリ負荷: DBに蓄積された孤立データを処理するバッチ作業が、データを
List形式で一度に照会し、Heapメモリを瞬間的に枯渇させていました。
2. インフラ最適化: JVMとDocker設定
まずインフラレベルでメモリ可用量を確保するためのチューニングを行いました。
JVMパラメータの再調整
Heapメモリの比重を50%に下げ、Non-Heap領域とのバランスを取りました。また、パフォーマンス最適化よりもメモリ節約が優先される環境であることを考慮し、Tiered Compilationレベルを調整しました。
-
MaxRAMPercentage=50.0: 1GB環境で約500MBをHeapに割り当て、残りを余裕空間として確保しました。
-
TieredStopAtLevel=1: C2コンパイラを制限してコンパイルタイムのメモリ負荷を減らし、起動速度を改善しました。
-
MaxMetaspaceSizeとReservedCodeCacheSize: 無制限に増加する可能性のある領域に上限を設け、予測不可能なメモリ膨張を防止しました。
ビルドプロセスの改善
Dockerfile内のビルド段階でGradleインメモリ引数を明示し、ビルド中に発生するメモリオーバーヘッドを制御しました。org.gradle.jvmargs="-Xmx512m"設定を通じてビルド環境の安定性を高めました。
3. リソース管理の最適化: コネクションプールダイエット
DBコネクションはそれ自体でメモリを占有するリソースです。トラフィックが集中しない初期サービス環境では、コネクションプールのデフォルト値である10個が不必要に感じられることがあります。
そこで、HikariCP設定を以下のように最適化しました。
-
maximum-pool-sizeを5に縮小: コネクション数を制限し、関連オブジェクトが占めるメモリフットプリントを減らしました。
-
idle-timeoutの短縮: 使用されていないコネクションをより速く回収し、システムにリソースを返還するよう誘導しました。
4. アプリケーションの最適化: バッチプロセスのページングとソフト削除導入
最も体感の大きい改善は、バッチ作業(R2ImageBatch)のロジック修正で行われました。既存の方式は、削除対象データを一度にメモリにロードする構造的欠陥がありました。
Sliceを活用したページング処理
Listの代わりにSpring Data JPAのSliceを使用してデータを適切な単位(BATCH_SIZE = 50)で区切って処理しました。Sliceは全体データの個数を数えるCountクエリを省略するため、大量データ処理時にPageよりパフォーマンス上の利点があります。これにより、数万件のデータが蓄積されてもHeapメモリは一定レベルを維持するようになりました。
Soft Deleteと状態管理
既存のハードデリート(Hard Delete)方式からソフトデリート(Soft Delete)方式に転換しました。
-
データ整合性: 外部ストレージ(R2)の実際のファイルは削除しつつ、DBレコードには
deletedAtフィールドに時刻を記録してデータの履歴を管理しました。 -
照会最適化: バッチ作業時に
deletedAt IS NULL条件を追加し、すでに処理が完了したデータを重複で読み込むコストを削除しました。
5. 結論と成果
以上のような一連の最適化過程を経た後、Railway環境でのOOM現象はもう発生しませんでした。リソースが極度に制限された環境では、単に高スペックサーバーへのスケールアップを考えるよりも、アプリケーションのリソース使用パターンを綿密に分析し、JVMレベルからコードレベルまで最適化するプロセスが先行されるべきであることを改めて確認しました。
今回の改善作業は、サービスの安定性を確保しただけでなく、効率的なリソース管理方式をチーム全体に内在化する契機となりました。類似の環境で問題を経験しているエンジニアの方々に、本事例が役立つことを願っています。
リンク:
リンク: » 韓国語で見る (한국어로 보기)
リンク: » 英語版を見る (Switch to English)
シェア: