Locust で OpenSearch(Elasticsearch) の性能を測る

OpenSearch(Elasticsearch)をバックエンドとした API サービスを運用していて、Elasticsearch のバージョンアップや設定変更、構成変更などでどのくらいの性能改善ができたのか比較したいケースが多々あります。
先日 Public Preview で登場した OpenSearch Serverless は非常に魅力的な機能、アーキテクチャを持っています。
今回は Locust で OpenSearch Service、OpenSearch Serverless、Elasticsearch on EC2 それぞれの性能を比較するための計測をしてみたのでそのメモ。
性能試験結果

Locust とは?
Locust は Python 製の性能試験ツール(OSS)です。
複雑な設定は不要でシナリオを Python スクリプトを書くように記述すれば開始でき、分散実行やレポート、シーケンシャルなシナリオといった性能試験で欲しくなる機能も備えています。
機能
- Python のコードを書くだけで実行できる
- 分散処理・スケーラビリティ
- 標準で持つ Web UI
- Web だけでなく、いろいろなシステムに対して実行できる
- 拡張可能なアーキテクチャ
Getting started
最低限の実行であれば 3ステップで実行できます。
- インストール
pip install locust
- コードを書く
ここではログインして、マイページにアクセスするユーザーを想定したシナリオでテストします。
from locust import HttpUser, task
class LoginUser(HttpUser):
@task
def view_mypage(self):
self.client.post("/login")
self.client.get("/mypage")
- 実行する
GUI から実行することも可能ですが、今回はコマンドベースで実行してみます。
locust \
-f locustfile.py \
-u 100 -r 2 -t 5m \
-H http://localhost \
--headless \
--csv report/login --html report/login_report.html
option | 説明 |
---|---|
-f | シナリオファイル(locustfile.py の場合は省略可) |
-u | 最大ユーザー数 |
-r | ハッチレート(秒間に増やすユーザー数) |
-t | 実行時間 |
-H | 負荷試験先エンドポイント |
--headless | コマンド実行(指定しない場合、Webアプリが起動) |
--csv | csv レポートのファイルプレフィックス |
--html | html レポートのファイル名 |
最大ユーザー数 100、ハッチレート 2の場合、開始 1秒で 2ユーザー、2秒で 4ユーザー、...、50秒で 100ユーザーに達する。
- 結果を確認する
上のコマンドの実行結果ではないですが、HTML 形式で以下のようなレポートと
CSV 形式では 4種類のレポートが出力され、確認できます。
- HTML レポートを CSV で表現したようなレポート(suffix が
_stats.csv
) - 1秒単位のスループット、レイテンシーなどのレポート(suffix が
_stats_history.csv
) - 失敗したリクエストの情報(suffix が
_failures.csv
) - クライアント側の例外情報(suffix が
_exceptions.csv
)

性能試験要件
- 性能試験環境は AWS
- 比較対象
- OpenSearch Serverless、OpenSearch Service、OpenSearch on EC2、Elasticsearch on EC2 の構成の違い
- インスタンスタイプでの違い
- 今後はデータ件数での違いとか、シャード配置、refresh_interval、Bulk サイズ、TranslogやBuffer サイズといった細かい設定の違い
- 大規模クラスタでの性能というよりも同じ条件(リソース)での性能比較なのでクライアントは 1ノードで十分
- インデキシング、検索の 2つのワークロードの性能を計測したい
- データは EC の商品データ 100,000件ぐらい
- Analyzer は一般的な日本語検索
- 計測する項目
- スループット
- レイテンシー
- 最小、平均、最大、90とか95とかのパーセンタイル
- エラーレート
- CPU 使用率
- ここだけ CloudWatch から参照。EC2 は詳細モニタリング有効にしておく
- スレッドのキューイング状況とか、gc とかも見れたらいいけどそこまではやらない
性能試験シナリオ
インデキシング
- 初期処理
1.1. 認証情報のセットアップ
1.2. インデックステンプレートの適用 - 性能試験
2.1. 100,000件の内、50件取得して、bulk リクエストをひたすら繰り返し
クエリ
上のインデックステンプレート適用、インデキシングが終わっている前提
- 初期処理
1.1. 認証情報のセットアップ - 性能試験
2.1. データにマッチするようなキーワードを定義しておいてランダムで抽出して、検索。
2.2. できれば、シンプルなクエリ、nested を含むクエリなどを組み合わせたい

実装メモ
実行環境について
ローカル実行すると実行環境のネットワークに左右されたりするので、AWS 環境で実行する。
ECS Fargate か、EC2 かで実行する。
ちゃんとやるなら使い回ししやすい ECS Fargate が望ましいが、今回は早く実施したかったので EC2 で実行する。
認証について
OpenSearch Serverless は AWS 署名v4による認証、OpenSearch Service は Basic 認証、Elasticsearch は認証不要(認証ありにもできる)と認証方法がバラバラなので試験シナリオと認証情報のセットアップは切り離したい。
幸いにも Locust はシナリオとユーザーを分けて実装できるので簡単だった。
task
で HTTP リクエストを投げる場合、 self.client.get()
などを利用しますが、client には requests の Session オブジェクトがセットされている。初期処理で Session オブジェクトのヘッダに認証情報をセットすれば、リクエスト単位での認証は不要となる。
Task と User の分割
Getting Started の実装では HttpUser
クラスの中に @task
でシナリオを定義した。
今回のようにシナリオは共通、アクセス先によって認証方法を変えたい場合、シナリオを Task に実装して、認証方法を User に実装して、利用する Task を設定するような分割が可能だった。
以下、サンプル。
from locust import HttpUser, TaskSet, task
class IndexingTaskSet(TaskSet):
items: list[str] = None
@task
def indexing(self):
self.client.post(
url="/_bulk",
data=data.encode(),
)
def on_start(self):
with open(BULK_FILENAME, encoding="utf-8") as file:
self.items = file.readlines()
class IndexingBehavior(HttpUser):
tasks = {IndexingTaskSet}
def on_start(self):
self.client.headers = {
"Content-Type": "application/x-ndjson",
}
self.client.auth = (USERNAME, PASSWORD)