1. 概要
この記事では、履修登録や予約申請のような先着順の申請システムで発生する大量リクエストの処理について、DBトランザクションを中心とした最適化の経験をまとめています。 このシステムは、申請開始直後に 短時間で数万件のリクエストが集中するという特徴があります。ユーザーは照会、申請、キャンセルを繰り返しながら、最大単位数、申請可能人数、予約上限など、複数の条件を満たす必要があります。そのため、データの整合性を維持するためのデータベーストランザクション処理が非常に重要です。
実際の運用環境では、約2万人のユーザーが同時にサービスを利用し、5分から10分の間に、約8万件から10万件程度の申請およびキャンセルリクエストが発生しました。この環境では、申請情報の作成と申請者数集計データの更新が主要なトランザクション処理の対象でした。
2. 初期実装と性能問題
プロジェクトの開発完了後、Apache JMeterを使用して、運用環境と類似した条件で負荷テストを実施しました。初期実装では、申請APIとキャンセルAPIのそれぞれで、APIの開始時点から終了時点まで全体にトランザクションが適用されていました。
しかし、申請可能かどうかを判断するための事前条件が多く、レガシーシステムから移行されたSQLの性能問題も存在していました。そのためAPIの処理時間が長くなり、トランザクションの保持時間も増加しました。結果として、データベースとの接続が長時間維持され、コネクションプールが不足しました。
大量のリクエストが発生する状況では、DBCPでコネクションを取得するための待機時間が長くなり、一部のリクエストはタイムアウトによって失敗しました。正常応答率が期待する水準に達しないという問題が発生しました。最終的な目標は、すべてのリクエストが正常な応答を受け取ることです。
3. 性能改善の過程
最初の改善段階では、DBインデックスを追加し、SQLチューニングを実施しました。これにより検索速度は向上し、APIの処理時間も短縮されました。しかし、コネクションプール不足の現象は依然として解決されませんでした。
第2段階では、MSA環境でサービスのPod数を増加させてスケールアウトを実施しました。最終的に約30個のPodを運用し、スループットは向上しました。しかし、ボトルネックがデータベース接続に存在していたため、リクエスト失敗の問題は完全には解決されませんでした。
第3段階では、HikariCPのコネクション数をデフォルトの10個から100個まで拡大しました。その結果、WASで同時に処理できるリクエスト数は増加しましたが、メモリ使用量が急激に増加し、Out Of Memoryエラーが発生しました。
第4段階では、TomcatのHeapメモリを256MBから4GBに増設しました。OOMの問題は解消されましたが、コネクション不足の現象は引き続き発生しました。ただし、全体の失敗率は一部低下しました。
4. WASとDBのリソースバランス調整
問題の本質は、単純な処理能力不足ではなく、データベースコネクションの使用方法にあることが確認されました。DBコネクションのリクエスト部分がボトルネック区間になった場合、k8sが該当するPODをサービス不可状態に切り替えてボトルネック区間を解消する必要があります。しかし、Tomcat WASが継続的にリクエストを受け付けられるため、リクエストの失敗が発生していました。
これに伴い、Tomcat WASがリクエストを同時処理するリクエストスレッド数をデフォルトの200個から50個に縮小し、DBCPのコネクション数も100個から50個再調整しました。
この変更により、DB接続不足の現象は大幅に緩和されました。しかし同時に、処理可能なリクエスト数が制限されたことで、別の性能上の制約が発生しました。
さらに、統合データベース環境による制約も存在しました。複数のシステムが1つの物理データベースを共有していたため、利用できる最大DB接続数が限られていました。したがって、単純に接続数を増やすだけでは問題を解決できませんでした。
5. リクエスト制御とトランザクションの最適化
運用の安定性を確保するため、従来のレガシーシステムで使用していたリクエスト待機ソリューション(ネットファネル)を再導入しました。これにより、一時的なトラフィックの急増を制御でき、運用中に発生していた大量の失敗問題は解消されました。
その後は、トランザクションの範囲を最小化する作業を進めました。申請可否を判断するさまざまな条件のうち、リアルタイムでの変更が不要で、今後もリアルタイムに変更されないデータの参照処理を、トランザクションの範囲外へ移動しました。これにより、DB接続の全体的な占有時間が短縮され、処理可能なリクエスト数が増加しました。
6. デッドロック問題と解決
トランザクションの最適化後、新たな問題が発生しました。申請者数の集計情報を更新する過程で、select for updateクエリ、すなわち悲観的ロックを使用していましたが、同一レコードに対する大量のリクエストが集中したことで、デッドロックが発生しました。
特に、1つのトランザクションが追加のDB接続を必要とする状況で接続プールが不足すると、デッドロック状態が発生する可能性がありました。これを解決するため、1リクエストあたりに必要な最大接続数を分析し、適切な接続プールサイズを算定しました。
DB Pool Size = T × (C - 1) + 1
ここでTはWASの最大スレッド数、Cは1つのリクエストが必要とする最大データベース接続数です。この式に基づいて設定を調整した後、デッドロックは解消されました。
7. 申請人数超過問題への対応
大量リクエスト環境では、申請制限数を超えて申請が完了してしまう現象も発生しました。これを解決するため、申請完了直後の時点を基準として、もう一度検証を実施しました。先着順で制限数を超過したかどうかを確認する方法であるため、該当時点を過ぎた後でも確認できました。
検証の結果、制限人数を超過した場合は、該当するトランザクションをロールバックするか、申請を取り消す方法でデータの整合性を維持しました。これにより、最終データの正確性を確保できました。
8. 悲観的ロックから楽観的ロックへの移行
性能分析の結果、悲観的ロック(Pessimistic Lock)は大量リクエスト環境における主要なボトルネック要因となっていました。そのため、悲観的ロックベースの構造を楽観的ロック(Optimistic Lock)ベースの構造へ変更しました。
楽観的ロックを適用するため、更新日時属性をバージョン情報として利用しました。エンティティの更新時刻が変更されるたびにバージョンが増加するよう構成し、競合が発生した場合にはOptimisticLockException例外が発生するようにしました。
また Spring Retry機能を活用して 衝突発生時 例外発生を条件として 自動再試行を実行するようにしました。再試行間隔は0.5秒未満に設定し、最大10回まで再試行するよう実装しました。
このように構造を変更した後、データベースコネクションの使用効率が大幅に向上しました。デッドロックの問題は事実上解消され、コネクション不足の現象も解決しました。最終的に、WASスレッド数50本, DBコネクション数20本の水準でも安定した運用が可能でした。
9. 結論
本事例は、単純なサーバーの増設やコネクション数の増加だけでは、大量リクエストの問題を解決できないことを示しています。実際のボトルネックを正確に分析し、トランザクションの範囲を最小化するとともに、適切なロック戦略を選択することが重要です。
特に、悲観的ロックを楽観的ロックに切り替え、再試行戦略を導入したことは、最も効果的な改善策の一つでした。また、データベースコネクションプール、WASスレッド数、トランザクション範囲の間のバランスを取ることが、大量リクエスト処理システムの安定性と性能を確保するための重要な要素であることを確認できました。
この経験を通じて、履修登録、予約システム、イベント応募、先着順販売など、さまざまな大量トラフィックシステムの設計に適用できる重要な要素を知ることができました。
Daniel(K)