目次
はじめに
2024年9月9日に公開されましたApache Solr 9.7.0にはセマンティック検索(ベクトル検索)におけるパフォーマンス向上が含まれています。詳細は、弊社ホームページの記事Apache Solr 9.7.0が公開されました!からご確認いただけます。
Apache Luceneが9.11.1にアップグレードされ、Java 21を使用したセマンティック検索などで大幅なパフォーマンス向上を実現しました。
このパフォーマンス向上は、Incubating Panama Vector APIの統合により実現され、Java 20とJava 21でベクトル計算におけるSIMD最適化を可能にしています。この機能をデフォルトで有効にするため、SolrのJavaコマンドにオプション--add-modules jdk.incubator.vector
が追加されました。
SIMD最適化とは
Single Instruction, Multiple Data
(単一命令で複数データを処理)の略で、同じ操作を複数データに同時に適用することで、計算効率を高める手法です。SIMDは科学技術計算や画像音声処理など、大量データを一度に処理する必要があるタスクにおいて効果を発揮します。
SIMD最適化のポイント
- ハードウェアのサポート
- SIMDの専用命令セットをサポートするCPUやGPUが必要となります(IntelのSSE、AVX、ARMのNEONなど)
- アルゴリズムやデータ構造のベクトル化
- SIMDを利用するためデータをベクトル化し効率的に並列処理を行えるようにする必要があります
- これにより実装やデバッグの難易度が高くなる場合があります
- データ並列処理によるパフォーマンス向上
- 同じ演算を複数データに対し同時実行することで、処理時間を短縮しパフォーマンスが大幅に向上します
- システムリソースを効率的に活用し、アプリケーションの性能を向上させるための強力な手法です
SolrでSIMD最適化を利用するメリット
Apache Solrにおいて、SIMD最適化は検索パフォーマンスを大幅に向上でき、特にセマンティック検索において効果を発揮します。SIMD最適化により、複雑なクエリ処理や大規模データセットに対し高速かつ効率的な検索を実現できます。
- テキスト解析の性能向上
- トークナイズや正規化プロセスが高速化され、全体的なパフォーマンスが向上します
- クエリの性能向上
- ベクトル計算が高速化されセマンティック検索のパフォーマンスが向上します
- スコアリングやフィルタリングなどを並列処理でき、検索クエリのレスポンスタイムが短縮されます
- スケーラビリティの向上
- SIMDに適した大規模データセットにより性能が改善され、スケーラビリティが向上します
- ユーザー体験の向上
- パフォーマンス向上により、迅速に関連性の高い結果を受け取ることができ、ユーザ満足度が向上します
Solrベクトル計算におけるSIMD最適化の導入に関するチケット
- Javaベクトル最適化の有効化
- ベクトルAPI統合、プランB #12302
- Incubating Panama Vector APIの統合 #12311
- Java 21 Project Panama Vector APIを使用したVectorUtilProviderの実装 #12363
セマンティック検索とその設定方法に関しましては、弊社クラウド型検索エンジンサービスKanadaSearchのドキュメントセマンティック検索とはをご参照ください。
目的
ベクトル計算性能におけるSIMD最適化の効果を評価します
特にSIMD最適化の有効化と無効化のパフォーマンス比較を目的としたため、ワークロードとして大規模データセットを使った長時間検証ではなく、短時間で実行可能なテストケースを使用し、より多くの検証パターンでパフォーマンス比較できるようにしました。具体的には、以下の視点からテストケースを準備しました。
- Java 20とJava 21でSIMD最適化を適用した場合、Solrベクトル計算のパフォーマンスがどれだけ向上するか?
- Java 11からJava 21にバージョンアップした場合、Solrベクトル計算のパフォーマンスは向上するか?
テスト方法は、Solrへの同時実行数とクエリ負荷を増やしながら、レスポンスタイムとリソース使用率(CPU/MEMORY/DISK I/O)を測定し、測定結果を用いてパフォーマンス比較を定量化、可視化します。
結論
セマンティック検索のパフォーマンスを以下の3つのJavaバージョンを使ってそれぞれ計測し、その結果を比較しました。
- ケース1:Java 11でSolrを実行します
- ケース2:Java 21(ベクトル最適化無効)でSolrを実行します
- Javaコマンドから
--add-modules jdk.incubator.vector
オプションを削除します
- Javaコマンドから
- ケース3:Java 21(ベクトル最適化有効)でSolrを実行します
- Solrデフォルト設定でJavaコマンドに
--add-modules jdk.incubator.vector
オプションが付与されています
- Solrデフォルト設定でJavaコマンドに
その結果、
- Solrのセマンティック検索でSIMD最適化を無効から有効に変更した場合、10%〜30%のパフォーマンス向上が確認できました
- Solrが使用するJavaをJava 11からJava 21にアップグレードした場合、パフォーマンス向上を確認できませんでした
検証に使用したAmazon EC2インスタンススペックとSolrヒープサイズ
EC2 instance | Processor | vCPU | Memory | Storage | Network performance | SOLR_JAVA_MEM | On-Demand hourly rate |
---|---|---|---|---|---|---|---|
t4g.small | ArmベースのAWS Graviton2(Armベース) | 2 | 2 GiB | EBS Only | Up to 5 Gigabit | "-Xms512M -Xmx925M" | $0.0168 |
t4g.medium | ArmベースのAWS Graviton2(Armベース) | 2 | 4 GiB | EBS Only | Up to 5 Gigabit | "-Xms512M -Xmx1920M" | $0.0336 |
t4g.large | ArmベースのAWS Graviton2(Armベース) | 2 | 8 GiB | EBS Only | Up to 5 Gigabit | "-Xms512M -Xmx3909M" | $0.0672 |
t4g.xlarge | ArmベースのAWS Graviton2(Armベース) | 4 | 16 GiB | EBS Only | Up to 5 Gigabit | "-Xms4G -Xmx8G" | $0.1344 |
c7g.large | AWS Graviton3(コンピューティング最適化のARMベース) | 2 | 4 GiB | EBS Only | Up to 12500 Megabit | "-Xms512M -Xmx1920M" | $0.0723 |
c7g.xlarge | AWS Graviton3(コンピューティング最適化のARMベース) | 4 | 8 GiB | EBS Only | Up to 12500 Megabit | "-Xms512M -Xmx3909M" | $0.1445 |
レスポンスタイム(ResponseTime): リクエストを発行してからレスポンスが返却されるまでかかった時間(秒)
以下の条件で検証を行いました
- セマンティック検索クエリの入力ベクトルは7367種類
- JMeterのループカウントを7367で検証
- JMeterの同時ユーザ数を20まで検証
- topKは1000
SIMD最適化有効時とSIMD最適化無効時のレスポンスタイム比較
- 全EC2インスタンスタイプでレスポンスタイムをソートした結果
考察
- CPUコア数が多いほど、ベクトル計算の同時実行数が増えるためセマンティック検索性能が向上しました
- 同じリクエスト数においてレスポンスタイムが一番短く検索が速かったのはコンピューティング最適化インスタンスc7g.xlargeでした
- t4g.xlarge(vCPU4/MEM16GiB)をt4g.large(vCPU2/MEM8GiB)と比べた場合、レスポンスタイムが半分に短縮されました
- EC2のコンピューティング最適化インスタンスが一般用途向けと比べプロセッサーパフォーマンスが優れていました
- c7g.large(vCPU2/MEM4GiB)をt4g.large(vCPU2/MEM8GiB)と比べた場合、レスポンスタイムが27%短縮されました。CPUコア数が同じで、メモリはt4gが倍ですが、プロセッサー性能が優れているc7g.largeが速くなりました
- ネットワークはc7g.largeがt4gより倍速いですが、今回ワークロードのネットワーク転送量から影響しません
- c7g.xlarge(vCPU4/MEM8GiB)をt4g.xlarge(vCPU4/MEM16GiB)と比べた場合、レスポンスタイムが17%短縮されました
- c7g.large(vCPU2/MEM4GiB)をt4g.large(vCPU2/MEM8GiB)と比べた場合、レスポンスタイムが27%短縮されました。CPUコア数が同じで、メモリはt4gが倍ですが、プロセッサー性能が優れているc7g.largeが速くなりました
- メモリ容量とSolrヒープサイズもセマンティック検索性能に寄与しているが、CPUほどではありませんでした
- t4g.large(vCPU2/MEM8GiB)をt4g.medium(vCPU2/MEM4GiB)と比べた場合、レスポンスタイムが2%しか速くならなかったです
- t4g.medium(vCPU2/MEM4GiB)をt4g.small(vCPU2/MEM2GiB)と比べた場合、レスポンスタイムが15%速くなりましたが、これはクエリ負荷を処理するのにメモリ2GiB、Solrヒープ1GiBでは不十分であることを示唆しています
- CPUコア数が多いほど、ベクトル計算の同時実行数が増えるためセマンティック検索性能が向上しました
SIMD最適化によるレスポンスタイム短縮率の比較
考察
- SIMD最適化の適用によりベクトル計算性能の向上を確認できました
- Java 21(最適化有効)をJava 21(最適化無効)と比べた場合、レスポンスタイムが10%〜30%短縮されました(平均で16%短縮)
- SIMD最適化の適用によりベクトル計算性能の向上を確認できました
topKによるレスポンスタイムの比較
考察
- セマンティック検索のtopK値が大きくなるにつれ、レスポンスタイムが伸びていました
- セマンティック検索でtopK値によりベクトル計算量が増えることを示唆しています
- セマンティック検索のtopK値が大きくなるにつれ、レスポンスタイムが伸びていました
同時実行数によるレスポンスタイムの比較
- 考察
- 同時ユーザ数が増えるにつれ、パフォーマンス向上幅が大きくなりました
- t4g.xlarge(vCPU4/MEM16GiB)で1ユーザー実行時のレスポンスタイム短縮率は10%、5ユーザー同時実行時は15%でした
- topコマンドのSolrプロセスのCPU使用率を比較すると、1ユーザーでは1.5のvCPUしか使えておらず、5ユーザーでは4vCPUを使い切っていました
- 1ユーザーと比べ5ユーザーでベクトル計算の同時実行数が増えたため、パフォーマンス向上幅もアップしたと考えられます
- t4g.xlarge(vCPU4/MEM16GiB)で1ユーザー実行時のレスポンスタイム短縮率は10%、5ユーザー同時実行時は15%でした
- 同時ユーザ数が増えるにつれ、パフォーマンス向上幅が大きくなりました
QPS(Quries/sec): 1秒間に処理したリクエスト件数
以下の条件で検証を行いました
- セマンティック検索クエリの入力ベクトルは7367種類
- JMeterのループカウントを7367で検証
- JMeterの同時ユーザ数を20で検証
- topKは1000
SIMD最適化有効時とSIMD最適化無効時のQPS比較
- 全EC2インスタンスタイプでQPSをソートした結果
- 考察
- 同時実行数が増えるにつれQPSが増えていきますが、最大値に達したらその後は変化しませんでした
Javaバージョンによるセマンティック検索性能の違い
- Javaバージョンによるベクトル計算性能の違いは確認できませんでした
- Java 21(ベクトル最適化無効)をJava 11と比べた場合、レスポンスタイムはほぼ同じでした
システムリソース
CPU使用率
- SIMD最適化の有効化/無効化によるCPU使用率の比較
考察
- SIMD最適化によるCPU使用率上昇はほとんど見られませんでした
同時実行数によるCPU使用率の比較
- 考察
- 同時実行数が1の場合、Solrは最大1.5のvCPUしか使っていませんでした
- 同時実行数が増えていくにつれ、CPU使用率が上昇し、同時実行数がvCPU数に達したら100%近くまで上昇しました
- SolrプロセスのCPU使用率が100%に達している場合、ベクトルの並列計算にすべてのCPUコアがフルで使用されていることを示唆します
メモリ使用率
- SIMD最適化の有効化/無効化によるメモリ使用率の比較
考察
- SIMD最適化によるメモリ使用率上昇はほとんど見られませんでした
同時実行数によるメモリ使用率の比較
- 考察
- 同時実行数によるメモリ使用率上昇はほとんど見られませんでした
- メモリ不足によりスワップアウトが発生するとパフォーマンスが著しく低下するため、負荷テスト全般でOSバッファキャッシュが枯渇しないようにワークロードを調整しました
Disk I/O使用率
- 考察
- 負荷テスト全般において、ディスクビジーによるI/O待ち状態は発生せず、I/O負荷が検索性能のボトルネックになることはありませんでした
考慮事項
検証サーバーのハードウェアとOSカーネルがSIMD最適化をサポートするか確認
今回の検証ではAmazon EC2のt4g.small
インスタンスを使用したため、t4g.smallを例にハードウェアとOSカーネルのレベルでSIMD最適化のサポートを確認した手順を共有します。
ハードウェアがSIMD最適化をサポートするか確認
- プロセッサ情報を
lscpu
コマンドを使って確認しました- Cpu architecture: aarch64
- Vendor ID: ARM
- Model name: Neoverse-N1
- Flags:
asimd
(Advanced SIMD)がセットされていました
- ARMドキュメントAbout-the-Advanced-SIMD-and-floating-point-supportから
Neoverse-N1
モデルがSIMDをサポートすることを確認しましたThe Neoverse™ N1 core supports the Advanced SIMD and scalar floating-point instructions in the A64 instruction set and the Advanced SIMD and floating-point instructions in the A32 and T32 instruction sets.
OS(Kernel)がSIMD最適化をサポートするか確認
float配列の積算を行うCサンプルプログラムを、gccのコンパイルオプション-O3
(最適化レベル)と-S
(アセンブリコード生成)を使ってコンパイルしました。その結果、生成されたアセンブリコードからベクトルタイプのSIMD積算インストラクションを確認できました。
fmul v2.4s, v2.4s, v6.4s
... ...
fmul v0.4s, v0.4s, v4.4s
セマンティック検索のクエリで使用する入力ベクトルセットについて
入力ベクトルとクエリの事前用意
ベクトル計算におけるSIMD最適化によるSolrセマンティック検索パフォーマンス向上を正確に評価するため、負荷測定時に関連しない他の処理がリソースを使用しないように工夫が必要です。
セマンティック検索クエリに含まれる入力ベクトル7367種類をあらかじめ計算(セマンティック検索時にオンザフライで計算するのではなく)し、JMeter負荷掛け用クエリとして使用します。
クエリで使用する入力ベクトル配列のバリエーション
ベクトルデータセットのバリエーションやデータ分布はSIMD最適化の効果に大きく影響します。データセットが均等に分布している場合、並列処理の効果が最大限に発揮されます。逆に、データ分布が偏っている場合、特定の要素に処理が集中するため、全体の性能向上が難しくなる場合があります。
試しに100種類のベクトル(768次元)のみ用意し、セマンティック検索で繰り返し使用してみたところ、パフォーマンス測定結果が不安定で評価が難しかったため、最終的には7367種類のベトクル(768次元)を負荷テストで使用しました。
テストの実行環境と実行方法
- 負荷掛けツールJMeterとSolrをAmazon VPCの同じサブネットにある二つのインスタンスに配置し、ネットワーク負荷による影響を抑えました
- セマンティック検索中に大量ベクトルデータがネットワーク経由でSolrに送信され、ネットワーク帯域幅がボトルネックとなりQPSやResponseTimeなどパフォーマンス指標に影響する可能性があるためです
- 複数テストケースの実行順序を変えながら負荷テストを行いました
- 実行順序がパフォーマンス測定に影響を与える可能性があるためです
- テストケースを実行するたびにSolrを再起動しました
- セマンティック検索時Solrキャッシュの再利用などがパフォーマンス評価の妨げにならないようにするためです
- Amazon EC2のバーストパフォーマンスインスタンス使用時の注意点
- バーストパフォーマンスインスタンスには一定のCPUバーストクォータが存在するため、高負荷によりCPU周波数がベースライン周波数より高くなる時間帯が敷居を超えると、CPU動作が遅くなります
- 検証途中にCPUクレジットが底をついてしまうと、バースト(ベースラインを超えたCPU使用率が使用できる状況)が機能せずSolrの検索性能が低下する可能性があります
- EC2インスタンスのCPUクレジットの使用率とバランスを監視しながら、バランスが枯渇しないか確認する必要があります
- CPU credit usage: The number of CPU credits spent during the measurement period.
- CPU credit balance: The number of CPU credits that an instance has accrued. This balance is depleted when the CPU bursts and CPU credits are spent more quickly than they are earned.
- バーストパフォーマンスインスタンスのCPUクレジットモードをUnlimitedモードにする方法もあります
- Unlimitedモードにより、CPUクレジットが枯渇した状態でもバースト状態を維持できますが、vCPU時間あたり追加コストが発生します
- バーストパフォーマンスインスタンスには一定のCPUバーストクォータが存在するため、高負荷によりCPU周波数がベースライン周波数より高くなる時間帯が敷居を超えると、CPU動作が遅くなります
テスト結果
結果マトリックスで各種項目の説明
- Users: JMeterのNum_threads値で、同時実行数(同時ユーザ数)
- QPS: JMeterのQPS値で、単位時間に処理されるクエリ数
- ResponseTime: クエリ発行からレスポンスが返却されるまでかかった時間(秒)
- %CPU: システムのCPU使用率(vmstatコマンドで計測)
- %MEM: システムのメモリ使用率(vmstatコマンドで計測)
- %I/O: システムのDisk I/O使用率(iostatコマンドで計測)
検索性能
負荷測定方法
- JMeterクエリに7367種類のベクトル値を使用した際の計測結果です
- JMeterクエリのループカウントは7367固定にしました
- 異なる同時実行数とJavaVerにおける負荷測定を2回ずつ実行し平均値を計算しました
- 検証パターンは72件です
EC2インスタンス/Javaバージョン/同時実行数 におけるパフォーマンス比較(リソース使用率とレスポンスタイム短縮率)
Id | EC2 Instance | JavaVer | SIMD Optimization | Concurrency | QPS | ResponseTime(sec) | Total %CPU Max | Solr %CPU Max | Total %MEM Max | Solr %MEM Max | Total %I/O Max | ResponseTimeReductionRate(%) By Number Of Concurrent Users | ResponseTimeReductionRate(%) By Vector Optimization | ResponseTimeReductionRate(%) By Ec2 Instance with small | ResponseTimeReductionRate(%) By Ec2 Instance with CPU cores | ResponseTimeReductionRate(%) By Java Version |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
1 | t4g.small | Java11 | off | 1 | 103 | 72 | 75 | 74 | 57 | 40 | 1 | 0 | 0 | 0 | 0 | 0 |
2 | t4g.small | Java11 | off | 5 | 174 | 212 | 100 | 99 | 60 | 44 | 1 | 41 | 0 | 0 | 0 | 0 |
3 | t4g.small | Java11 | off | 10 | 175 | 421 | 100 | 99 | 60 | 44 | 1 | 42 | 0 | 0 | 0 | 0 |
4 | t4g.small | Java11 | off | 20 | 179 | 823 | 100 | 99 | 61 | 45 | 4 | 43 | 0 | 0 | 0 | 0 |
5 | t4g.small | Java21 | off | 1 | 102 | 73 | 71 | 70 | 63 | 44 | 1 | 0 | 0 | 0 | 0 | -1 |
6 | t4g.small | Java21 | off | 5 | 171 | 215 | 100 | 99 | 64 | 44 | 1 | 41 | 0 | 0 | 0 | -1 |
7 | t4g.small | Java21 | off | 10 | 179 | 413 | 100 | 99 | 63 | 44 | 2 | 43 | 0 | 0 | 0 | 2 |
8 | t4g.small | Java21 | off | 20 | 180 | 821 | 100 | 99 | 65 | 44 | 15 | 44 | 0 | 0 | 0 | 0 |
9 | t4g.small | Java21 | on | 1 | 111 | 67 | 68 | 67 | 60 | 44 | 0 | 0 | 9 | 0 | 0 | 0 |
10 | t4g.small | Java21 | on | 5 | 201 | 184 | 100 | 99 | 60 | 44 | 0 | 45 | 15 | 0 | 0 | 0 |
11 | t4g.small | Java21 | on | 10 | 209 | 353 | 100 | 99 | 60 | 43 | 1 | 47 | 15 | 0 | 0 | 0 |
12 | t4g.small | Java21 | on | 20 | 215 | 686 | 100 | 100 | 63 | 45 | 48 | 48 | 16 | 0 | 0 | 0 |
13 | t4g.medium | Java11 | off | 1 | 110 | 67 | 79 | 79 | 28 | 20 | 1 | 0 | 0 | 7 | 7 | 0 |
14 | t4g.medium | Java11 | off | 5 | 173 | 214 | 100 | 99 | 29 | 21 | 0 | 36 | 0 | -1 | -1 | 0 |
15 | t4g.medium | Java11 | off | 10 | 177 | 418 | 100 | 99 | 31 | 23 | 0 | 38 | 0 | 1 | 1 | 0 |
16 | t4g.medium | Java11 | off | 20 | 177 | 834 | 100 | 99 | 33 | 25 | 1 | 38 | 0 | -1 | -1 | 0 |
17 | t4g.medium | Java21 | off | 1 | 108 | 69 | 77 | 76 | 29 | 21 | 2 | 0 | 0 | 5 | 5 | -3 |
18 | t4g.medium | Java21 | off | 5 | 172 | 214 | 100 | 99 | 29 | 21 | 2 | 38 | 0 | 0 | 0 | 0 |
19 | t4g.medium | Java21 | off | 10 | 176 | 420 | 100 | 100 | 29 | 21 | 2 | 39 | 0 | -2 | -2 | 0 |
20 | t4g.medium | Java21 | off | 20 | 176 | 838 | 100 | 100 | 29 | 21 | 1 | 39 | 0 | -2 | -2 | 0 |
21 | t4g.medium | Java21 | on | 1 | 134 | 55 | 75 | 73 | 29 | 21 | 0 | 0 | 20 | 17 | 17 | 0 |
22 | t4g.medium | Java21 | on | 5 | 231 | 160 | 100 | 99 | 29 | 21 | 0 | 42 | 25 | 13 | 13 | 0 |
23 | t4g.medium | Java21 | on | 10 | 247 | 299 | 100 | 99 | 30 | 21 | 0 | 46 | 29 | 15 | 15 | 0 |
24 | t4g.medium | Java21 | on | 20 | 253 | 583 | 100 | 99 | 30 | 21 | 0 | 47 | 30 | 15 | 15 | 0 |
25 | t4g.large | Java11 | off | 1 | 127 | 58 | 77 | 77 | 15 | 11 | 4 | 0 | 0 | 19 | 13 | 0 |
26 | t4g.large | Java11 | off | 5 | 202 | 182 | 100 | 99 | 16 | 12 | 1 | 37 | 0 | 14 | 15 | 0 |
27 | t4g.large | Java11 | off | 10 | 209 | 353 | 100 | 99 | 17 | 12 | 1 | 39 | 0 | 16 | 16 | 0 |
28 | t4g.large | Java11 | off | 20 | 214 | 687 | 100 | 99 | 23 | 19 | 1 | 41 | 0 | 17 | 18 | 0 |
29 | t4g.large | Java21 | off | 1 | 130 | 57 | 76 | 75 | 14 | 11 | 1 | 0 | 0 | 22 | 17 | 2 |
30 | t4g.large | Java21 | off | 5 | 199 | 185 | 100 | 99 | 16 | 12 | 1 | 35 | 0 | 14 | 14 | -2 |
31 | t4g.large | Java21 | off | 10 | 204 | 361 | 100 | 100 | 17 | 12 | 7 | 37 | 0 | 13 | 14 | -2 |
32 | t4g.large | Java21 | off | 20 | 215 | 684 | 100 | 100 | 16 | 12 | 26 | 40 | 0 | 17 | 18 | 0 |
33 | t4g.large | Java21 | on | 1 | 143 | 52 | 76 | 75 | 15 | 10 | 1 | 0 | 9 | 22 | 5 | 0 |
34 | t4g.large | Java21 | on | 5 | 241 | 154 | 100 | 100 | 16 | 12 | 1 | 41 | 17 | 16 | 4 | 0 |
35 | t4g.large | Java21 | on | 10 | 247 | 299 | 100 | 100 | 16 | 12 | 77 | 43 | 17 | 15 | 0 | 0 |
36 | t4g.large | Java21 | on | 20 | 259 | 570 | 100 | 100 | 17 | 12 | 80 | 45 | 17 | 17 | 2 | 0 |
37 | t4g.xlarge | Java11 | off | 1 | 126 | 59 | 38 | 37 | 22 | 20 | 1 | 0 | 0 | 18 | -2 | 0 |
38 | t4g.xlarge | Java11 | off | 5 | 401 | 92 | 100 | 99 | 22 | 20 | 1 | 69 | 0 | 57 | 49 | 0 |
39 | t4g.xlarge | Java11 | off | 10 | 417 | 177 | 100 | 100 | 22 | 20 | 1 | 70 | 0 | 58 | 50 | 0 |
40 | t4g.xlarge | Java11 | off | 20 | 443 | 333 | 100 | 100 | 23 | 20 | 1 | 72 | 0 | 60 | 52 | 0 |
41 | t4g.xlarge | Java21 | off | 1 | 126 | 59 | 39 | 38 | 31 | 29 | 1 | 0 | 0 | 19 | -4 | 0 |
42 | t4g.xlarge | Java21 | off | 5 | 405 | 91 | 100 | 99 | 31 | 29 | 0 | 69 | 0 | 58 | 51 | 1 |
43 | t4g.xlarge | Java21 | off | 10 | 425 | 174 | 100 | 100 | 31 | 29 | 0 | 71 | 0 | 58 | 52 | 2 |
44 | t4g.xlarge | Java21 | off | 20 | 431 | 343 | 100 | 100 | 31 | 29 | 0 | 71 | 0 | 58 | 50 | -3 |
45 | t4g.xlarge | Java21 | on | 1 | 140 | 53 | 40 | 39 | 31 | 29 | 0 | 0 | 10 | 20 | -2 | 0 |
46 | t4g.xlarge | Java21 | on | 5 | 482 | 77 | 99 | 99 | 31 | 29 | 0 | 71 | 15 | 58 | 50 | 0 |
47 | t4g.xlarge | Java21 | on | 10 | 506 | 146 | 100 | 100 | 31 | 29 | 0 | 72 | 16 | 59 | 51 | 0 |
48 | t4g.xlarge | Java21 | on | 20 | 518 | 285 | 100 | 100 | 31 | 29 | 1 | 73 | 17 | 58 | 50 | 0 |
49 | c7g.large | Java11 | off | 1 | 187 | 39 | 100 | 99 | 33 | 25 | 3 | 0 | 0 | 46 | 33 | 0 |
50 | c7g.large | Java11 | off | 5 | 322 | 115 | 100 | 99 | 28 | 21 | 1 | 41 | 0 | 46 | 37 | 0 |
51 | c7g.large | Java11 | off | 10 | 324 | 227 | 100 | 99 | 34 | 28 | 1 | 42 | 0 | 46 | 36 | 0 |
52 | c7g.large | Java11 | off | 20 | 336 | 439 | 100 | 99 | 34 | 28 | 1 | 44 | 0 | 47 | 36 | 0 |
53 | c7g.large | Java21 | off | 1 | 73 | 46 | 72 | 71 | 29 | 21 | 82 | 0 | 0 | 37 | 19 | -18 |
54 | c7g.large | Java21 | off | 5 | 312 | 118 | 100 | 100 | 28 | 21 | 1 | 49 | 0 | 45 | 36 | -3 |
55 | c7g.large | Java21 | off | 10 | 337 | 219 | 100 | 100 | 28 | 21 | 1 | 52 | 0 | 47 | 39 | 4 |
56 | c7g.large | Java21 | off | 20 | 344 | 429 | 100 | 100 | 29 | 21 | 1 | 53 | 0 | 48 | 37 | 2 |
57 | c7g.large | Java21 | on | 1 | 187 | 39 | 70 | 69 | 30 | 21 | 1 | 0 | 15 | 41 | 25 | 0 |
58 | c7g.large | Java21 | on | 5 | 348 | 106 | 100 | 99 | 30 | 21 | 1 | 46 | 10 | 42 | 31 | 0 |
59 | c7g.large | Java21 | on | 10 | 373 | 198 | 100 | 100 | 30 | 21 | 1 | 49 | 10 | 44 | 34 | 0 |
60 | c7g.large | Java21 | on | 20 | 383 | 385 | 100 | 100 | 30 | 22 | 2 | 51 | 10 | 44 | 32 | 0 |
61 | c7g.xlarge | Java11 | off | 1 | 203 | 37 | 39 | 38 | 16 | 12 | 1 | 0 | 0 | 49 | 37 | 0 |
62 | c7g.xlarge | Java11 | off | 5 | 625 | 59 | 99 | 99 | 16 | 12 | 1 | 68 | 0 | 72 | 36 | 0 |
63 | c7g.xlarge | Java11 | off | 10 | 677 | 109 | 100 | 99 | 16 | 12 | 1 | 71 | 0 | 74 | 38 | 0 |
64 | c7g.xlarge | Java11 | off | 20 | 708 | 208 | 99 | 99 | 18 | 15 | 1 | 72 | 0 | 75 | 38 | 0 |
65 | c7g.xlarge | Java21 | off | 1 | 206 | 36 | 99 | 99 | 17 | 13 | 1 | 0 | 0 | 51 | 39 | 3 |
66 | c7g.xlarge | Java21 | off | 5 | 638 | 58 | 99 | 99 | 18 | 13 | 2 | 68 | 0 | 73 | 36 | 2 |
67 | c7g.xlarge | Java21 | off | 10 | 669 | 110 | 100 | 99 | 17 | 13 | 2 | 69 | 0 | 73 | 37 | -1 |
68 | c7g.xlarge | Java21 | off | 20 | 693 | 213 | 100 | 99 | 18 | 13 | 1 | 70 | 0 | 74 | 38 | -2 |
69 | c7g.xlarge | Java21 | on | 1 | 231 | 32 | 39 | 38 | 15 | 11 | 7 | 0 | 11 | 52 | 40 | 0 |
70 | c7g.xlarge | Java21 | on | 5 | 766 | 48 | 99 | 99 | 18 | 14 | 5 | 70 | 17 | 74 | 38 | 0 |
71 | c7g.xlarge | Java21 | on | 10 | 812 | 91 | 100 | 99 | 18 | 14 | 4 | 72 | 17 | 74 | 38 | 0 |
72 | c7g.xlarge | Java21 | on | 20 | 849 | 174 | 100 | 99 | 18 | 14 | 4 | 73 | 18 | 75 | 39 | 0 |
検証環境
Amazon EC2インスタンス
JMeterとSolrがリソースを奪い合わないように、JMeterをSolrと分けて別サーバーにインストールします。 ただし、ネットワーク負荷が検証結果への影響を抑えるため、JMeterとSolrを同じVPCサブネットのEC2インスタンスに配置します。
JMeterサーバー用のEC2インスタンス
- t4g.small
Solrサーバー用のEC2インスタンス
- t4g.small
- t4g.medium
- t4g.large
- t4g.xlarge
- c7g.large
- c7g.xlarge
- OS: Ubuntu 20.04.6 LTS
検証手順
環境準備
SolrインスタンスにJava11, Java21をインストールし、切り替え可能に設定
- Java 11のインストール(参照: https://docs.aws.amazon.com/ja_jp/corretto/latest/corretto-11-ug/generic-linux-install.html)
$ wget -O- https://apt.corretto.aws/corretto.key | sudo apt-key add -
sudo add-apt-repository 'deb https://apt.corretto.aws stable main'
$ sudo apt-get update; sudo apt-get install -y java-11-amazon-corretto-jdk
- Java 21のインストール(参照: https://docs.aws.amazon.com/ja_jp/corretto/latest/corretto-21-ug/generic-linux-install.html)
$ wget -O - https://apt.corretto.aws/corretto.key | sudo gpg --dearmor -o /usr/share/keyrings/corretto-keyring.gpg && \
echo "deb [signed-by=/usr/share/keyrings/corretto-keyring.gpg] https://apt.corretto.aws stable main" | sudo tee /etc/apt/sources.list.d/corretto.list
$ sudo apt-get update; sudo apt-get install -y java-21-amazon-corretto-jdk
- Java Versionの切り替え
$ sudo update-alternatives --config java
There are 3 choices for the alternative java (providing /usr/bin/java).
Selection Path Priority Status
------------------------------------------------------------
* 0 /usr/lib/jvm/java-21-amazon-corretto/bin/java 12100004 auto mode
1 /usr/lib/jvm/java-11-amazon-corretto/bin/java 11100024 manual mode
2 /usr/lib/jvm/java-21-amazon-corretto/bin/java 12100004 manual mode
Press <enter> to keep the current choice[*], or type selection number: 1
update-alternatives: using /usr/lib/jvm/java-11-amazon-corretto/bin/java to provide /usr/bin/java (java) in manual mode
$ java --version
openjdk 11.0.24 2024-07-16 LTS
OpenJDK Runtime Environment Corretto-11.0.24.8.1 (build 11.0.24+8-LTS)
OpenJDK 64-Bit Server VM Corretto-11.0.24.8.1 (build 11.0.24+8-LTS, mixed mode)
Solrインスタンスにiostatコマンドをインストール
$ sudo apt install sysstat
負荷テストツールJMeterをSolrとは別サーバーにインストール
- JMeterをダウンロード
$ wget https://dlcdn.apache.org//jmeter/binaries/apache-jmeter-5.6.3.tgz
解凍
$ tar zxvf apache-jmeter-5.6.3.tgz
試験的に、jmeterコマンドをレポート付きで実行してみます
./apache-jmeter-5.6.3/bin/jmeter -n -t test.jmx -Jusers=$1 -l jmeter-query.log -e -o ./jmeter-report
試験的に、jmeterコマンドをレポートなしで実行してみます
./apache-jmeter-5.6.3/bin/jmeter -n -t test.jmx -Jusers=$1 -j jmeter-`date +'%Y%m%d%H%M%S'`.log
Solrの設定
セマンティック検索を行うための設定
Solrスキーマ定義にセマンティック検索用フィールドタイプとフィールドを追加する必要があります。
solr.DenseVectorField
クラスの768次元ベクトルのフィールドタイプと当該フィールドタイプのフィールドを二つ追加しました。
<fieldType name="knn_vector" class="solr.DenseVectorField" vectorDimension="768" similarityFunction="dot_product"/>
<field name="body_vector" type="knn_vector" indexed="true" stored="true"/>
<field name="title_vector" type="knn_vector" indexed="true" stored="true"/>
なお、クエリで使用する入力ベクトルの種類もベクトルのSIMD最適化の効果に影響するため、あらかじめ7367種類のベクトル値を計算し、それを入力ベクトルとするクエリを7367件用意しておきました。
セマンティック検索に必要なSolr設定の詳細については、弊社クラウド型検索エンジンサービスKanadaSearchのドキュメントApache Solrのセマンティック検索機能をご参照ください。
Solrのヒープサイズ
検証サーバーのスペックに合わせ、Solrのヒープサイズを以下のように設定しました。
SOLR_JAVA_MEM="-Xms512M -Xmx925M"
Solrログ設定
負荷テストで大量ログが出力されるとシステム負荷となるため、必要最小限のログサイズに変更しました。
ローテーションサイズと世代数を小さく設定します:
/var/solr/log4j2.xml
<SizeBasedTriggeringPolicy size="1 MB"/>
<DefaultRolloverStrategy max="1"/>
ログレベルをエラーに変更、リクエストログを無効にします:
/var/solr/solr.in.sh
- Change log level:
SOLR_LOG_LEVEL=ERROR
- Disable request logging:
SOLR_REQUESTLOG_ENABLED=false
- Change log level:
ベクトルインデクシングの実施とセマンティック検索の試験
インデクシングに使用するデータを準備
KandaSearch拡張機能ライブラリーで提供している Livedoorニュースコーパス(embeddings)のデータを使用します。7367件のドキュメントにはすでに768次元ベクトルが付与されています。
Livedoorニュースコーパス(embeddings)を使ったセマンティック検索の詳細については、弊社クラウド型検索エンジンサービスKanadaSearchのドキュメントKandaSearchのセマンティック検索機能をご参照ください。
Solrコレクションを作成
KandaSearch拡張機能ライブラリーで提供している Livedoorニュースコーパス(embeddings)のコンフィグ を使って、Solrコレクションを作成します。
ベクトルデータのインデクシング
ベクトル付き Livedoorニュースコーパス(embeddings)のデータ をSolrへPOSTします。
$ /opt/solr/bin/solr post -c livedoor ./data/livedoor_embeddings.json
Posting files to [base] url http://localhost:8983/solr/livedoor/update...
Entering auto mode. File endings considered are xml,json,jsonl,csv,pdf,doc,docx,ppt,pptx,xls,xlsx,odt,odp,ods,ott,otp,ots,rtf,htm,html,txt,log
POSTing file livedoor_embeddings.json (application/json) to [base]/json/docs
1 files indexed.
COMMITting Solr index changes to http://localhost:8983/solr/livedoor/update...
WARNING: URLs provided to this tool needn't include Solr's context-root (e.g. "/solr"). Such URLs are deprecated and support for them will be removed in a future release. Correcting from [http://localhost:8983/solr] to [http://localhost:8983/].
Time spent: 0:01:30.047
ベクトル付きクエリの用意
セマンティック検索クエリに用いる入力ベクトルは上述 Livedoorニュースコーパス(embeddings)のデータ の全ドキュメントから768次元ベクトルを7367件すべて抽出します(クエリ作成に数時間かかる場合があります)。
livedoor_embeddings.json
から7367件のbody_vector
値を抽出します
% for i in {0..7366}; do command="jq -r '[.[] | .body_vector] | nth($i)' ../livedoor_embeddings.json > vector-$i.json" && eval $command ;done
- 上記ベクトルをqパラメータに付与し、7367件のクエリを作成します
for i in {0..7366}; do echo "{\!knn f=body_vector topK=10}"$(cat vector-$i.json | tr -d '\n' | tr -d ' ') >> vector-query-7367.csv ;done
- 作成されたクエリのサンプルです
{!knn f=body_vector topK=10}[-0.02621307782828808,-0.06952571868896484,-0.0034800975117832422,...(768次元)]
セマンティック検索が正常に行えるか確認します
- ベクトルクエリを発行します
$ curl -g 'http://localhost:8983/solr/livedoor/query?q={!knn%20f=body_vector%20topK=10}[-0.02621307782828808,-0.06952571868896484,-0.0034800975117832422,...(768次元)]'
- Solrから以下のレスポンスが返されます
{ "responseHeader":{ "status":0, "QTime":11, "params":{ "q":"{!knn f=body_vector topK=10}[-0.02621307782828808,-0.06952571868896484,... } }, "response":{ "numFound":10, "start":0, "numFoundExact":true, "docs":[{ "id":"movie-enter-5978741.txt", "url":"http://news.livedoor.com/article/detail/5978741/", "category":"movie-enter", "title":"【DVDエンター!】... ...", "title_exact":"【DVDエンター!】... ...", "title_2g":"【DVDエンター!】......", ... ...
セマンティック検索の負荷測定を開始します
JMeterから異なる同時ユーザ数とループカウントを指定し負荷を掛けながら、QPS、ResponseTime、システムリソース使用率などパフォーマンスを測定します
Javaバージョンは、Java 11、Java 21(ベクトル最適化無効)、Java 21(ベクトル最適化有効)の三つに対しそれぞれ負荷計測を行います。
Java 21(ベクトル最適化無効)は、Solr起動コマンド
/opt/solr/bin/solr
から--add-modules jdk.incubator.vector
オプションをコメントアウトすることで実現しました# if [[ "$JAVA_VER_NUM" -ge "20" ]] && [[ "$JAVA_VER_NUM" -le "21" ]] ; then # SCRIPT_SOLR_OPTS+=("--add-modules" "jdk.incubator.vector") # echo "Java $JAVA_VER_NUM detected. Incubating Panama Vector APIs have been enabled" # fi
前回のテストケース実行が今回に影響しないように(リソース消費やSolrキャッシュなにより影響など)、新しくテストケース開始時はSolrを再起動します
テスト結果を確認
JMeterのパフォーマンス測定結果
vmstat/iostat/topコマンドのリソース測定結果
Solrログを確認
負荷テスト期間中にエラーが発生しなかったか確認します。特にメモリ不足によるOOM(OutOfMemory)エラーやプロセスkillが発生していないか確認します。
Solrログのエラー確認例です。
$ sudo grep -i err /var/solr/logs/* | grep -v " INFO "
システムログを確認
負荷テスト期間中にエラーが発生しなかったか確認します。特にメモリ不足によるOOM(OutOfMemory)エラーやプロセスkillが発生していないか確認します。
syslogのエラー確認例です。
$ sudo grep -i err /var/log/syslog
おわりに
Apache Solr 9.7.0を使った負荷テストを実施し、SIMD最適化によりセマンティック検索(ベクトル検索)のパフォーマンスが大幅に向上できたことを確認できました。 なお、セマンティック検索の負荷テストにおける検証手順や注意点、考察や気づきなども共有させていただきました、少しでも皆様のご業務の一助となれたら幸いです。