はじめに
本記事では、数千万規模のデータを扱う検索基盤において、当初採用していたOpenSearch ServerlessからOpenSearch Service(Provisioned)へデータの更新性能を求めて移行を決断した経緯と、その際に行ったチューニングの内容を共有します。
「サーバー管理不要」というメリットからServerless版が第一候補に上がることが多い昨今ですが、「大量データの更新・登録」という特定のワークロードにおいては、Provisioned版で明示的なリソース管理とチューニングを行う方が、性能・コスト面で有利になるケースがありました。
本記事が技術選定の一助となれば幸いです。
導入の背景と当初の課題
本システムでは、大量のデータ(数千万規模)に対する検索性能が課題となっていました。
フリーテキスト検索や複合条件による検索レスポンスがボトルネックとなっており、これを解消するために検索特化のエンジンとしてOpenSearchの導入を決定しました。
OpenSearchに知見のあるメンバーがいなかったため、インフラ管理コストを考慮しOpenSearch Serverlessを選定しました。
検索性能に関しては全く問題ない検証結果が得られ、よしよしと思ったのも束の間、データ更新処理のパフォーマンスが課題となることが判明しました。
本システムでは、検索対象となるデータに対してバッチ処理で定期的にデータの一部を一括更新する必要があり、このデータ更新処理に時間を要することが判明しました。
運用上、「限られた時間内に処理を完了させる」という制約があり、更新性能の改善が急務となりました。
Serverless環境での「機能制約」と「大量データ更新」のジレンマ
しかし、更新性能の改善は一筋縄ではいきませんでした。ボトルネックの根本原因は、単純なリソース不足ではなく、Serverless特有の機能制約と、それを回避するために複雑化したアーキテクチャにありました。 具体的には、以下のような経緯で処理時間が肥大化していました。
1. update_by_query 未サポートによるアーキテクチャの複雑化
Serverless(執筆時点)では、条件に合致するドキュメントを一括更新するupdate_by_queryがサポートされていません。 数千万件規模のデータを更新するためには、以下の「2ステップ方式」をアプリケーション側で実装する必要がありました。
- 検索フェーズ: 更新対象のドキュメントIDを検索してリスト化する
- 更新フェーズ: 取得したIDに対して、
Bulk APIで更新をかける
Bulk APIは数千万件を一気に処理することはできないため、StepFunctions で処理を分割し、Lambda を並列起動して少しずつBulk Updateを投げ込むバッチ構成を作り込みました。
(イメージ図です。)
時間の都合でしっかりとした検証をしたわけではないですが、update_by_query APIそのものも、数千万規模のデータ一括更新に耐えられるかというとそうでもないようです。
もし初めからProvisionedを選定してupdate_by_query APIを利用していても性能面で課題になった可能性はあります。
2. スロットリング(429エラー)との戦い
しかし、このバッチ構成でLambdaからBulk APIを一斉に実行したところ、Serverless側のオートスケール(OCUの拡張)がスパイク負荷に追従できず、429 Too Many Requests(スロットリング) が多発しました。
エラーをハンドリングするために、クライアント(StepFunctions)側には複雑なリトライ処理の実装を余儀なくされました。また、エラー率を下げるために「あえて並列数を絞る」「ウェイトを入れる」といった調整も必要となり、処理速度を上げたくても上げられないジレンマに陥りました。
3. システム要件(制限時間)の未達
「2ステップ方式による通信オーバーヘッド」に加え、「スロットリング回避のためのリトライと待機時間」が積み重なった結果、全体の処理時間は肥大化しました。
本番相当のデータ量で検証を行ったところ、「指定された時間枠内に更新処理が終わらない」 ことが判明し、Serverless構成でのパフォーマンス限界という結論に至りました。
Service(Provisioned) への移行
上記の課題を受け、Serverless版ではチューニングの限界があるとチームで判断。「Provisioned構成(OpenSearch Service)へ切り替え、明示的なチューニングを行う」 方針へ転換しました。
結果として同一データ量・同一アプリケーションからの実行で、処理時間は以下のように劇的に短縮されました。
| Serverless版実行時間 | Service版実行時間 | |
|---|---|---|
| バッチ① | 1時間30分 | 9分 |
| バッチ②(データ抽出・加工の時間を含む) | 1時間40分 | 25分 |
このような高速化を実現したのは、単なるインスタンス性能の違いだけではなく、Provisioned環境だからこそ可能なインデックス設定(Settings)の最適化によるものです。
実施したチューニング内容
大量データの登録・更新処理(Bulk)において、書き込みスループットを最大化するために以下の設定を適用しました。
1. refresh_interval の一時的な無効化
OpenSearchは書き込んだデータをすぐに検索できるようにデフォルトで1秒ごとにリフレッシュしています。本システムではバッチによるデータ更新中は検索可能である必要はなかったため、この値を-1(無効)に設定しました。これによりディスクへの書き込み頻度が減り、スループットが劇的に向上します。
PUT /my-index/_settings
{
"index": {
"refresh_interval": "-1"
}
}
※ バッチ処理完了後に値を元に戻します。
2. レプリカ数 (number_of_replicas) を0にする
データ登録中は number_of_replicas を0にしました。複製を作るコストがなくなるので、その分書き込みが速くなります。
PUT /my-index/_settings
{
"index": {
"number_of_replicas": 0
}
}
※ こちらもバッチ処理完了後に本来の冗長構成に戻します。
3. インスタンスタイプを最適化
Serverlessでは隠蔽されているコンピュートリソースですが、Provisionedではワークロードに合わせて選択可能です。今回はOpenSearchに最適化された or2.xlarge.searchを採用しました。S3を活用したストレージアーキテクチャにより、高いインデックス性能とコスト効率を両立できるインスタンスファミリーです。
おまけ:コストも安くなった
性能要件を満たすための移行でしたが、コスト試算では、Serverless版の約6割ほどの月額料金となることがわかり、コスト面からもメリットがあることがわかりました。
Serverlessはバッチ処理のスパイク負荷に応じて課金(OCU)が跳ね上がるという点が、大量データを更新する上では無視できないものになるのではないかと思います。
性能が向上し、かつコストも下がるという、プロジェクトにとって最良の結果となりました。
まとめ
OpenSearch Serverlessは「インフラ管理からの解放」という強力なメリットを持ちますが、データ規模や更新パターンといったワークロードの特性を見極め、適切な構成を選択することの重要性を再認識しました。
最後に
本記事で紹介した検証結果や移行の意思決定はプロジェクトのチームメンバー全員で議論を重ね、地道な検証に取り組んだ結果です。最高のチームワークでプロジェクトを完遂できたことに、深く感謝します。