ESrally单机向量检索性能测试全流程
测试方案的尝试
准备测试 ES 的向量检索性能,Vespa 方案由于下载依赖库存在网络问题无法执行成功,终止;开源工具 ann-benchamrk 是一个用于评估近似最近邻(ANN)搜索库的性能测试工具,这个本是最佳选择,但是也由于需要 pip 安装几十个依赖和 docker 构建十分麻烦,且详细的教程介绍太少,最后还是选择 esrally 进行性能测试,这个也是 ES 官方博客所使用的。
测试环境
uname -r
# 5.4.6-1.el7.elrepo.x86_64 内核版本
lscpu
# Intel(R) Xeon(R) Gold 6130T CPU @ 2.10GHz 处理器型号
# Architecture: x86_64 架构
cat /etc/redhat-release
# CentOS Linux release 7.4.1708 (Core) 操作系统版本
python3 --version
# Python 3.10.11
在线安装 esrally
pip3 install esrally
Httpx 版本冲突
由于之前安装了其他 ollama-python 0.1.2 导致一些依赖版本冲突:
ERROR: pip's dependency resolver does not currently take into account all the packages that are installed. This behaviour is the source of the following dependency conflicts. ollama-python 0.1.2 requires httpx<0.27.0,>=0.26.0, but you have httpx 0.27.0 which is incompatible. ollama-python 0.1.2 requires responses<0.25.0,>=0.24.1, but you have responses 0.18.0 which is incompatible.
处理办法:pip 3 uninstall ollama-python
安装完查看一下 esrally 版本(不能直接使用 esrally):
cd /root/python3/Python-3.10/bin
# ./esrally --version
esrally 2.10.0
./esrally --help
返回内容如下(有省略):
usage: esrally [-h] [--version] {race,list,delete,info,create-track,compare,build,download,install,start,stop,add} ...
____ ____
/ __ \____ _/ / /_ __
/ /_/ / __ `/ / / / / /
/ _, _/ /_/ / / / /_/ /
/_/ |_|\__,_/_/_/\__, /
/____/
You Know, for Benchmarking Elasticsearch.
options:
-h, --help show this help message and exit
--version show program's version number and exit
subcommands:
{race,list,delete,info,create-track,compare,build,download,install,start,stop,add}
Dense vector track 介绍
rally-tracks/dense_vector at master · elastic/rally-tracks (github.com)
本赛道(track)用于对密集向量场的索引和搜索进行基准测试。
该数据集包含 1,000 万个矢量,维度为 96。该数据集基于 Yandex DEEP 1 B 图像数据集,可在此处下载:big-ann-benchmarks.com/。
数据集由名为 learn.350M.fbin
的 "样本数据 "文件的前 1000 万个向量创建。
使用下面这个命令生成 JSON 格式数据集:
python3 _tools/parse.py data/learn.350M.fbin > documents.json
文档示例
{
"vector": [0.21529805660247803, -0.06119159981608391, 0.08770883828401566, 0.08731604367494583, -0.03312725946307182, -0.06861377507448196, 0.011172166094183922, 0.08099681884050369, 0.06873716413974762, -0.10662394016981125, -0.06803347170352936, -0.22509372234344482, 0.04775683954358101, -0.11963146924972534, -0.13713325560092926, 0.040520284324884415, 0.03633395954966545, -0.06001321226358414, 0.05640476569533348, -0.1323852241039276, 0.09493865817785263, 0.08581436425447464, 0.11651603877544403, -0.007319801487028599, 0.07001037895679474, -0.20735269784927368, 0.09725717455148697, -0.09026120603084564, 0.027974626049399376, 0.07966648787260056, -0.00902935303747654, -0.25620758533477783, -0.03905373811721802, 0.1739693433046341, -0.0062398542650043964, 0.1722472906112671, -0.005885206162929535, -0.15015317499637604, -0.13207486271858215, -0.05693387985229492, 0.1926010102033615, -0.03730269894003868, 0.06276655197143555, 0.06388836354017258, -0.07929308712482452, -0.04800641909241676, -0.04506685957312584, -0.025829171761870384, 0.11739484965801239, 0.04018357768654823, 0.03557595983147621, -0.14328059554100037, -0.009096059016883373, 0.01991777867078781, 0.19544541835784912, 0.16432076692581177, -0.17786552011966705, -0.011906086467206478, -0.022930240258574486, 0.0394594669342041, -0.1750987023115158, -0.07123833894729614, 0.03142083063721657, -0.03363385424017906, 0.042252954095602036, -0.12258686870336533, 0.0053991954773664474, -0.0580497682094574, -0.13083989918231964, 0.01648520492017269, -0.15029865503311157, 0.04681768640875816, -0.22521495819091797, -0.028500616550445557, 0.09996795654296875, 0.04748646169900894, -0.12404435873031616, 0.11154870688915253, -0.018465351313352585, -0.009930790401995182, -0.04939839243888855, -0.1522141546010971, 0.21582986414432526, -0.05126684159040451, -0.020097043365240097, -0.05058222636580467, 0.0635807067155838, 0.01816011220216751, 0.0961170494556427, 0.06972803920507431, 0.13445426523685455, -0.015163707546889782, -0.0299700740724802, 0.03558037802577019, 0.10351981967687607, -0.025422528386116028]
}
Parameters参数含义
Rally 0.8.0+ 使用 --track-params
接收下列参数:
bulk_size
(默认值: 5000)bulk_indexing_clients
(默认: 1): Number of clients that issue bulk indexing requests.index_settings
: A list of index settings. Index settings defined elsewhere (e.g.number_of_replicas
) need to be overridden explicitly.max_num_segments
(默认: 1): The number of segments to target when doing a force merge.number_of_replicas
(默认: 0)post_ingest_sleep
(默认: false): Whether to pause after ingest and prior to subsequent operations.post_ingest_sleep_duration
(默认: 30): Sleep duration in seconds.vector_index_type
(默认: "hnsw"): The index kind for storing the vectors.
性能测试准备
下载数据集
Esrally 好像没有单独下载数据的命令,直接开始测试后会自行下载所需数据。
esrally race --track=dense-vector
报错 clone 失败
esrally.exceptions.SupplyError: Could not clone from [https://github.com/elastic/rally-tracks] to [/root/.rally/benchmarks/tracks/default]
猜测是 git 时网络波动,之前也用过 esrally 测试其他项目, 解决办法:重试。
如果还不行就单独手动 clone 项目到指定的位置并改名:
cd /root/.rally/benchmarks/tracks/
git clone https://github.com/elastic/rally-tracks.git
mv rally-tracks default
可能出现 The requested URL returned error: 403,需要重试或自行下载 zip 包上传。
报错非 git 项目
esrally.exceptions.SystemSetupError: [/root/.rally/benchmarks/tracks/default] must be a git repository.
Please run:
git -C /root/.rally/benchmarks/tracks/default init
解决办法:
cd /root/.rally/benchmarks/tracks/default
git init
报错 git 版本过低
Your git version is [['git version 1.8.3.1']] but Rally requires at least git 1.9. Please update git.
提示我升级 git 到 1.9,但是我们最终是离线运行,资源已经自行下载过,理论上是不需要使用 git 的。修改源码 vim ./utils/git.py
注释掉版本报错行即可。
指定离线参数无效
./esrally race --offline --track=dense-vector --pipeline=benchmark-only
当前报错:(Could not checkout [master]. Do you have uncommitted changes? You may need to specify --offline if running without Internet connection.).
实际上 git 相关的报错非常多,也非常难缠,花费了很多时间来解决这一系列问题。
使用 --offline
只是不会下载一些内容,但是连接git的操作依然会执行,通过 --track-path=/home/deploy/.rally/benchmarks/tracks/default/...
重新指定本地的track则可以绕过git的连接,如果使用 --track=geonames
这个参数,将会一直去连接git找到这个最新的track信息。
需注意的是这样配置之后还是会报错缺少 teams 项目。
解决办法(git 项目只是一小部分,测试数据 7G 多是尚未下载好的):
- 下载数据
sh download.sh dense_vector
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
23 7333M 23 1759M 0 0 1125k 0 1:51:09 0:26:39 1:24:30 1120k
mv dense_vector dense-vector
2. 修改配置文件:vim rally.ini
[tracks]
default.url = ~/.rally/benchmarks/tracks/default
3. 测试命令修改 --track=dense-vector
成如下的内容:
--track-path=/home/deploy/.rally/benchmarks/tracks/default/<测试项名称>
- 报错找不到 teams 项目,这个也是和
rally-tracks
一样的,自行下载后拷贝到指定位置并改名为default
。
git clone https://github.com/elastic/rally-teams.git
cp -r ./rally-teams/ ~/.rally/benchmarks/teams/default
呜呼,下载工作基本结束了,准备起飞
安装启动本地 ES
我们的目的是测试自己的环境中的 ES 集群,那么这里要使用自行搭建好的 ES 集群。这里先搭建一个单节点的本地服务,供性能测试用。
su elastic
./bin/elasticsearch -d
后台启动 ES 后可以执行如下命令检查是否正常:
curl http://localhost:9200/_cluster/health?pretty
{
"cluster_name" : "es-prod",
"status" : "green",
"timed_out" : false,
"number_of_nodes" : 1,
"number_of_data_nodes" : 1,
"active_primary_shards" : 0,
"active_shards" : 0,
"relocating_shards" : 0,
"initializing_shards" : 0,
"unassigned_shards" : 0,
"delayed_unassigned_shards" : 0,
"number_of_pending_tasks" : 0,
"number_of_in_flight_fetch" : 0,
"task_max_waiting_in_queue_millis" : 0,
"active_shards_percent_as_number" : 100.0
}
执行测试
./esrally race --track-path=/root/.rally/benchmarks/tracks/default/dense_vector --target-hosts=ESIP:9200 --pipeline=benchmark-only --offline
解压数据这里耗费的时间不少。
上述冒烟测试,参数未作任何调整,后续正式的测试需要至少调大 ES 集群的内存设置、测试时的分片数(也可以测试单分片的性能)、并发数。
curl http://localhost:9200/_cat/indices?v
curl -XDELETE http://localhost:9200/vectors
设置主分片数发现当前测试并不支持,可以参看前面章节允许设置哪些参数。
我们查看一下默认的索引设置是怎么样的:
curl http://localhost:9200/vectors/_mappings?pretty
{
"vectors" : {
"mappings" : {
"dynamic" : "strict",
"_source" : {
"enabled" : false
},
"properties" : {
"vector" : {
"type" : "dense_vector",
"dims" : 96,
"index" : true,
"similarity" : "dot_product",
"index_options" : {
"type" : "hnsw",
"m" : 32,
"ef_construction" : 100
}
}
}
}
}
}
如果确实要修改主分片数,index.json
源码位置位于 /root/.rally/benchmarks/tracks/default/dense_vector
。
向量索引解释
上述的索引描述的是 Elasticsearch 中的一种索引结构,特别适用于高维向量空间的搜索优化,常用于诸如图像识别、自然语言处理、推荐系统等需要计算向量相似度的场景。
下面是对这段配置的详细解释:
"vectors"
: 这是索引名称或者类型的一个标识,并不是实际的数据类型,而是用户自定义的名称。"mappings"
: 映射定义了文档字段如何存储和索引。这是 Elasticsearch 中定义数据结构的地方。"dynamic": "strict"
: 指定了文档映射的动态处理行为。"strict"模式意味着如果文档中有映射中未定义的字段,写入时会抛出错误,确保所有字段都是预定义的。"_source": { "enabled": false }
: 表示禁用_source
字段的存储。默认情况下,Elasticsearch 会存储文档的原始 JSON(即_source 字段),以便在检索时返回完整的文档。这里禁用它可能是为了节省存储空间,尤其是在向量本身足够表示文档内容,或者原始文档存储在其他地方的情况下。"vector"
: 是一个字段名,表示这个字段是用来存储高维向量数据的。"type": "dense_vector"
: 指定该字段的数据类型为密集向量,适合存储如机器学习模型生成的固定长度的浮点型向量。"dims": 96
: 定义了向量的维度,这里是 96 维。这意味着每个文档在这个字段下都会有一个长度为 96 的浮点数数组。"index": true
: 表示该字段需要被索引,这样才能对其进行搜索和过滤操作。"similarity": "dot_product"
: 设置了相似度计算方法为点积 (dot_product)。在向量空间中,点积常用来衡量两个向量的相似度,值越接近说明两个向量方向越一致,相似度越高。"index_options"
: 这里配置了针对高维向量的特定索引选项,使用的是 HNSW (Hierarchical Navigable Small World) 算法,这是一种高效的近似最近邻搜索算法,特别适合高维空间。"type": "hnsw"
: 指定使用 HNSW 算法来构建索引。"m"
: HNSW 中的一个参数,影响索引的构建质量和搜索效率,通常是一个较小的数值,这里是 32。"ef_construction"
: 在构建 HNSW 索引时使用的临时邻接链的大小,较大的值可能会提高搜索精度但同时增加索引构建时间,这里是 100。
完整的性能测试命令:
./esrally race --track-path=/root/.rally/benchmarks/tracks/default/dense_vector --target-hosts=localhost:9200 --pipeline=benchmark-only --offline --report-file=/home/elastic/report_dv_sd_cd-30g.csv --report-format=csv --kill-running-processes
全部项目最终运行了约 2770 秒。
查看结果
结果会在指定的 csv 报告文件中输出一份,也会直接在屏幕上打印,摘取部分结果如下:
Cumulative indexing time of primary shards,,13.3031,min
Mean Throughput,index-append,4647.58,docs/s
99th percentile latency,index-append,1487.4399612145496,ms
Median Throughput,index-update-concurrent-with-searches,5808.68,docs/s
Mean Throughput,knn-search-100-1000-concurrent-with-indexing,46.24,ops/s
Mean Throughput,knn-search-10-100_multiple_segments,124.47,ops/s
Mean Throughput,knn-search-100-1000_multiple_segments,55.26,ops/s
Median Throughput,knn-search-10-100,144.09,ops/s
Median Throughput,knn-search-100-1000,61.82,ops/s
Mean Throughput,script-score-query,5.03,ops/s
下面是对每个指标含义的解释:
-
Cumulative indexing time of primary shards: 这个指标表示在测试期间,所有主分片累计花费的索引时间总和。单位为分钟(min),在这里是 13.3031 分钟。这个值可以用来评估数据写入效率。
-
Mean Throughput, index-append: 表示索引追加操作的平均吞吐量,即平均每秒成功索引的文档数量。这里的值是 4647.58 docs/s,意味着在测试过程中,平均而言每秒可以处理 4647.58 份文档的索引操作。
-
99 th percentile latency, index-append: 这是索引追加操作的 99%分位延迟,即 99%的索引请求在该时间内完成。数值为 1487.4399612145496 毫秒 (ms),意味着只有 1%的索引请求耗时超过这个值,是一个衡量延迟稳定性的指标。
-
Median Throughput, index-update-concurrent-with-searches: 表示在并发执行索引更新与搜索操作时,索引更新操作的中位数吞吐量,即每秒能处理的文档数。这里是 5808.68 docs/s,展示了在有搜索负载同时进行时,索引更新的效率。
-
Mean Throughput, knn-search-100-1000-concurrent-with-indexing: KNN(K-Nearest Neighbors,最近邻)搜索的平均吞吐量,在并发执行索引和搜索操作时,针对每秒能处理的查询次数(ops/s)。这里是 46.24 ops/s,表明在有索引压力的同时,每秒能完成 46.24 次这样的 KNN 查询。
-
Mean Throughput, knn-search-10-100_multiple_segments & knn-search-100-1000_multiple_segments: 分别代表在多段(segment)的情况下,KNN 搜索查询的平均吞吐量,针对查询向量数量不同(10-100 和 100-1000)的情况。这两个值(124.47 ops/s 和 55.26 ops/s)说明了在不同查询规模下系统的处理能力。
-
Median Throughput, knn-search-10-100 & knn-search-100-1000: 类似于上面的指标,但这里是中位数吞吐量,分别对应每秒能处理的查询次数为 144.09 和 61.82 次。中位数通常比平均值更能抵抗异常值影响,提供更稳定的性能表现参考。
-
Mean Throughput, script-score-query: 脚本评分查询的平均吞吐量,表示每秒能处理的此类查询次数。这里为 5.03 ops/s,脚本评分查询通常较为复杂,涉及脚本计算文档得分,因此吞吐量相对较低。
[!note] 备注
knn-search-10-100
的 10 指的是返回的 Top 结果数,就是 KNN 的 K 是多少,而 100 指的是num_candidates
,这个参数是 HNSW 算法所特有的,它控制着我们要考虑的近邻队列的大小(好奇者请注意:它等同于原论文中的 ef 参数)。
此外,测试使用的命令行参数有很多,可参考致谢中的官方手册,具体的使用后续再研究探讨。
关于离线测试
任何离线测试的都需要在线的操作,前述流程执行之后所需的材料已经在本地。
- Esrally 的安装位置:
Python-3.10/lib/python 3.10/site-packages/esrally
- Rally 命令位置:
Python-3.10/bin
- Rally 代码及比赛描述等:
/root/.rally/benchmarks/tracks/default
- 例如其中一个数据集的具体位置:
/root/.rally/benchmarks/data/dense-vector
那么离线测试时应该至少包括以上内容,拷贝到目标测试环境中。
致谢
本实践遭遇的部分问题解答参考了以下内容:
3. rally-tracks/dense_vector at master · elastic/rally-tracks