在上一章中,我们介绍了如何将 Elasticsearch 与机器学习(ML)模型集成。本章将聚焦于优化 Elasticsearch 中向量搜索的性能。
本章涵盖的主题包括:
- ML 模型部署调优技术
- 估算 Elasticsearch 节点的向量容量
- 使用 Elastic 的性能测试工具 Rally 进行负载测试
- 排查 k 近邻(kNN)搜索响应时间慢的问题
通过本章学习,您将了解如何估算您的用例所需的 Elasticsearch 节点数,如何利用性能基准测试确保估算符合需求,以及识别可能导致 kNN 搜索响应缓慢的原因。
下面我们先从部署一个自然语言处理(NLP)模型开始。
部署 NLP 模型
部署 NLP 模型需要选择一种能够处理生产高峰期推理负载的方法,同时保证高效和安全。测试阶段如果模型体积较小,通常可以在本地运行,但生产环境需要专业的部署策略。模型可以部署在虚拟机上、云服务商提供的平台、专用模型托管服务(如 Hugging Face),或在可能的情况下,直接部署在使用向量的服务中,比如 Elasticsearch。每种方式在效率、延迟、扩展性和管理上各有利弊。
与外部部署方法交互通常通过 API 发送文本输入,接收向量输出。不论向量如何生成,都会被发送到数据存储,用于存储,之后可以通过精确匹配或近似最近邻搜索进行检索。查询时生成的向量用于搜索之前存储的向量。
调优已部署模型以实现最佳性能和效率,可能涉及硬件资源、软件配置和模型参数的调整,但具体调优方法超出本书范围。
当在 Elasticsearch 中部署嵌入模型时,模型运行在集群内专用的 ML 节点上,实现与 Elasticsearch 搜索和索引能力的无缝集成。接下来,我们将介绍一些部署调优的设置。
将模型加载到 Elasticsearch
Elasticsearch 8.0 及以上版本原生支持 libtorch(PyTorch 的 C++ 库),可以运行基于 PyTorch 的模型,并在系统内生成嵌入。
加载模型时,我们将使用上一章介绍的 Eland Python 库。Eland 支持从本地磁盘加载模型或支持的架构,但最简单的方法是直接从 Hugging Face 加载。支持的模型类型详见文档:www.elastic.co/guide/en/ma…。
示例中,我们将使用 Eland 从 Hugging Face 直接加载 sentence-transformers/msmarco-MiniLM-L-12-v3 模型(huggingface.co/sentence-tr…)。Eland]() 会自动下载并分块处理模型,以便加载到 Elasticsearch。
上一章中展示了如何在 Jupyter Notebook 使用 Python 加载 NLP 模型。代码示例可见本书 GitHub 仓库第3章文件夹:github.com/PacktPublis…。
使用 Docker 运行 Eland
如果仅需使用 Eland 来加载模型,也可以通过 Docker 运行。Docker 是一个开源平台,用于在轻量级且可移植的容器内自动化部署应用,确保软件在不同环境间可靠运行。它简化了开发流程,让开发者将应用及其依赖打包成标准化单元。
Eland 的 Docker 镜像可从其 GitHub 仓库获取:github.com/elastic/ela…。
用 Docker 安装 Hugging Face 模型的示例命令:
docker run -it --rm --network host \
elastic/eland \
eland_import_hub_model \
--url http://host.docker.internal:9200/ \
--hub-model-id sentence-transformers/msmarco-MiniLM-L-12-v3 \
--task-type ner
模型部署配置
模型加载到 Elasticsearch 后,需要部署或启动。模型在 ML 节点类型的节点上运行,Elastic Cloud 部署即是如此。
NLP 模型(如嵌入模型)可通过配置分配数和每个分配的线程数进行横向扩展,以满足生产推理负载:
- 分配数(Allocations) :启动模型时增加分配数,会提升所有可用 ML 节点上的模型实例数量。分配数增加通常能提升整体吞吐量。启动后也可动态调整。
- 每个分配的线程数(Threads per allocation) :影响模型实例所用的 CPU 线程数。线程数增加通常缩短每个请求的推理时间。该值只能在模型部署时调整。
分配数与线程数的总和不得超过所有 ML 节点的可用线程数。增加线程数和分配数超过某个阈值后会出现收益递减。建议结合具体负载测试,找到最优配置。
加载模型后,我们就可以开始进行性能测试,为正式投产做好准备。接下来,我们来看一下可以使用的性能测试工具。
负载测试
在 Elasticsearch 中,对数据的写入和查询进行基准测试,对优化搜索和分析操作性能至关重要。鉴于 Elasticsearch 常被用于处理大规模数据,必须确保写入和查询性能的高效性。为此,需要识别系统中的瓶颈和改进点,通过全面的基准测试和性能测试,用户可以在问题成为关键瓶颈前发现并解决它们。
Rally
Rally 是 Elastic 提供的免费基准测试工具,用户可用它来衡量和优化 Elasticsearch 性能。Rally 支持对各种操作(如索引、搜索、聚合)进行基准测试,适用范围广泛,适合确保集群运行在最佳状态。
本章中,我们重点使用 Rally 对 Elasticsearch 中向量搜索性能进行基准测试,这是许多高级搜索用例的核心。
Rally 提供了多种“track”和数据集,涵盖日志、指标、安全和向量等不同类型数据。用户可使用这些 track 测试集群性能,并根据配置变化或版本升级进行性能评估。
Rally 框架的三个主要组成部分:
- Track(轨道) :定义一系列基准测试和工作负载,模拟 Elasticsearch 的不同操作(如索引或查询)。
- Race(竞赛) :一次 Track 的执行,测量 Elasticsearch 在特定条件下的性能表现。
- Challenge(挑战) :Track 内部定义的一组具体操作或任务,用于测试不同配置和负载下的性能。
Track 定义待测试操作,Challenge 指定执行条件和任务,Race 则是实际运行并测量性能。
用户最好花时间使用自己的数据创建定制 Track,并让 Rally 使用与生产环境相同结构的查询,这样测试结果更具参考价值。Rally 的一个重要优势是能够在数据变更时重复运行测试,特别适合集群架构设计阶段测试分片数、副本数及节点文档量对索引和查询性能的影响。
请根据官方文档(esrally.readthedocs.io/en/stable/i…)完成环境安装。
本章将使用 Rally 中专门针对密集向量字段的高级搜索设计的 StackOverflow 向量 Track。该 Track 来自 StackOverflow 帖子数据集,包含多种字段,包括密集向量,支持带过滤的近似最近邻(ANN)和混合搜索。利用该 Track,我们能测量和优化 Elasticsearch 中向量搜索性能,并了解高维向量数据集群管理的最佳实践。
注意
Rally 默认会将大量指标输出到日志文件,但 Elasticsearch 集群可配置为收集这些结果,并通过 Kibana 仪表盘搜索和可视化,方便对比测试结果。
接下来,我们学习如何运行 Rally。
配置 Python 环境运行 Rally
以下为示例代码和说明,您可以复制并执行以跟进基准测试流程:
sudo apt-get install python3-venv
python3 -m venv rally_testing
source rally_testing/bin/activate
pip install --upgrade pip
安装依赖:
sudo apt-get install gcc libffi-dev python3-dev
pip install elasticsearch
安装 Rally 和 StackOverflow Track:
pip install esrally
esrally --version
esrally list tracks
配置 Rally
我们将配置 Rally 将测试指标发送到 Elasticsearch 集群。推荐将指标发送到与被测集群不同的独立监控集群。
可用的报告配置选项详见文档:esrally.readthedocs.io/en/stable/c…。
编辑配置文件 ~/.rally/rally.ini 中的 [reporting] 部分,例如连接到 Elastic Cloud 的专用监控集群:
[reporting]
datastore.type = elasticsearch
datastore.host = my_monitoring_cluster.es.us-central1.gcp.cloud.es.io
datastore.port = 9243
datastore.secure = true
datastore.user = rally
datastore.password = rally_password
配置完成后,我们准备基准测试 Elasticsearch 集群。
在现有集群上运行竞赛(Race)
我们将基准测试已有的 Elasticsearch 测试集群,确保其配置满足测试需求。命令行中传入 track 信息和目标集群信息。
警告
请勿在生产集群上进行基准测试!基准测试会压榨性能极限,导致正常操作变慢。这是测试预期,但在生产环境可能影响关键业务性能。
更多信息请参考 Rally 官方文档:esrally.readthedocs.io/en/stable/r…。
esrally 命令参数说明
race:表示执行竞赛--track:指定使用的 track--target-hosts:指定目标集群主机--pipeline=benchmark-only:表示不创建新集群,使用现有集群--client-options:安全认证相关配置
执行示例
esrally race --track=so_vector --target-hosts=deployment_name.es.us-central1.gcp.cloud.es.io:9243 --pipeline=benchmark-only --track-params ingest_percentage:100 --client-options="use_ssl:true,verify_certs:true,basic_auth_user:'elastic',basic_auth_password:'elastic_password'"
请注意,so_vector track 数据大小超过 32GB。
Rally 会开始运行,并自动下载 track 数据(若尚未下载)。
内存估算
为了保证 kNN 搜索的高性能,向量需要“装得下”数据节点的堆外内存(off-heap RAM)。更多性能调优细节请参见官方文档:www.elastic.co/guide/en/el…。
从 Elasticsearch 8.7 版本开始,向量所需的粗略内存估算公式如下:
num_vectors * 4 * (num_dimensions + 12)
解释如下:
- num_vectors:向量字段的数量。例如有 900 万文档,每文档一个向量,则为 900 万;如果设置了 1 个副本,则总数翻倍为 1800 万。
- num_dimensions:向量维度数,即嵌入模型输出的维度。
注意该公式只适用于浮点型向量。
举例:900 万个向量,维度为 768,则计算如下:
9,000,000 * 4 * (768 + 12) ≈ 26.2 GB RAM
这意味着数据节点总共大约需要 26GB 堆外内存来保证 kNN 搜索性能。每增加一个副本,内存需求相应翻倍。一般 Elasticsearch 节点分配一半内存给 JVM,因此在标准 64GB Elastic Cloud 节点上,预计可以缓存 900 万个 768 维向量。但仍需测试确认。
性能基准测试
如前所述,Rally 是一个功能强大的性能基准测试工具,功能丰富且支持复杂配置,详见官方文档:esrally.readthedocs.io。
使用 Rally 测试向量搜索(尤其是 kNN 搜索)步骤如下:
-
选择单节点,内存 60GB 或 64GB VM。Elasticsearch JVM 默认分配一半内存,也可试验配置 16GB JVM 堆大小。
-
配置测试索引,设置单主分片,无副本。
-
按前述内存估算,将估算向量数的 60% 索引入单节点。
-
运行 Rally,配置自定义 track,仅测试搜索查询。
-
分析结果,关注服务时间和延迟:
- 服务时间:Elasticsearch 执行查询所需时间
- 延迟:从 Rally 提交查询到任务完成的总时间,通常希望越低越好
-
向集群再加载 10% 文档,重复 Rally 测试并分析。
-
若时间保持稳定,继续逐步增加文档量并重复测试,直到响应时间大幅上升。
-
响应时间突增时,上一次“良好”测试对应的文档数即为该节点缓存向量的近似上限。
-
可通过运行
GET /_nodes/hot_threads查看热点线程,辅助确认性能瓶颈。
该方法能帮助您确定单节点堆外内存能承载的最大向量数量。需要注意,目前单分片查询仍是单线程,官方推荐单分片大小控制在 10-50GB 之间。更多分片可提升索引速度(通过多节点分散文档),但对快速响应的搜索场景通常建议靠较小分片提升性能。
结果评估示例
以下测试采用:
- Rally 的 cohere_vector track,默认设置(<github.com/elastic/rally-tracks/tree/master/cohere_vector>)
- 768 维 float32 向量
- 64GB 内存,Elastic Cloud DATAHOT.N2.68X10X45 配置单热节点
- 粗略估算:1100 万向量约占 32GB RAM(基于 Elasticsearch 8.10.3)
实际业务场景的响应时间需求因数据使用方式(外部用户、内部程序查询)、查询规模和预算不同而异。
本测试目标响应时间 < 300 毫秒,帮助理解单节点性能极限,后续可扩展到多节点集群。
7.2百万向量测试,响应时间良好:
| 百分位数 | 测试名称 | 时间(ms) |
|---|---|---|
| 50th 百分位服务时间 | standalone-knn-search-100-1000-single-client | 63.21 |
| 90th 百分位服务时间 | standalone-knn-search-100-1000-single-client | 70.95 |
| 100th 百分位服务时间 | standalone-knn-search-100-1000-single-client | 204.18 |
900万向量测试,响应时间仍然可接受:
| 百分位数 | 测试名称 | 时间(ms) |
|---|---|---|
| 50th 百分位服务时间 | standalone-knn-search-100-1000-single-client | 63.37 |
| 90th 百分位服务时间 | standalone-knn-search-100-1000-single-client | 71.76 |
| 100th 百分位服务时间 | standalone-knn-search-100-1000-single-client | 303.93 |
1380万向量测试,响应时间超出可接受范围,服务时间明显上升,可能因向量总量超过堆外内存导致 kNN 搜索响应变慢:
| 百分位数 | 测试名称 | 时间(ms) |
|---|---|---|
| 50th 百分位服务时间 | standalone-knn-search-100-1000-single-client | 763.32 |
| 90th 百分位服务时间 | standalone-knn-search-100-1000-single-client | 1045.13 |
| 100th 百分位服务时间 | standalone-knn-search-100-1000-single-client | 1655.94 |
综上所述,单节点的密集向量(kNN 搜索)容量大约为 1000 万左右。根据此结果,您可以开始规划集群规模,增加节点数和副本数以满足需求。
故障排查 — 性能变慢
我们已经确定了单节点缓存和运行向量的近似容量,但查看其他指标有助于诊断或确认性能变慢的原因。
kNN 搜索性能下降通常与堆外内存(off-heap RAM)不足有关。接下来介绍一些辅助诊断的方法。
Hot Threads API
Elasticsearch 的 Hot Threads API 可以帮助识别性能瓶颈,它会显示集群中所有节点或特定节点上最繁忙线程的信息。分析该 API 的输出有助于定位导致性能变慢的原因。
调用接口获取最繁忙线程详情:
GET /_nodes/hot_threads
也可以指定特定节点或自定义返回参数,如线程数、采样间隔等:
GET /_nodes/{nodeId}/hot_threads?threads=3&interval=500ms&snapshots=5
输出内容是繁忙线程的堆栈跟踪。一般通过观察堆栈中的模式或反复出现的关键点,来定位性能问题的根源。常见问题包括:频繁垃圾回收、慢查询或复杂查询、分片重平衡、资源密集型聚合等。
针对 kNN 搜索的排查
重点关注与 HNSW(分层可导航小世界图)相关的线程。示例线程堆栈(部分):
100.0% [cpu=4.7%, other=95.3%] (500ms out of 500ms) cpu usage by thread 'elasticsearch[instance-0000000020][search][T#8]'
2/10 snapshots sharing following 33 elements
app//org.apache.lucene.codecs.lucene91.Lucene91HnswVectorsReader$OffHeapVectorValues.vectorValue(Lucene91HnswVectorsReader.java:499)
app//org.apache.lucene.util.hnsw.HnswGraphSearcher.searchLevel(HnswGraphSearcher.java:182)
从中我们观察到:
- 大部分时间耗费在“other”(95.3%),CPU 使用仅为 4.7%;
- 运行代码主要为 Lucene91HnswVectorsReader$OffHeapVectorValues。
这强烈表明性能瓶颈部分来自 Lucene 需要从磁盘读取向量数据,而非直接从页缓存中读取。低 CPU 利用率和高“other”百分比通常意味着线程处于等待磁盘 I/O 的状态。
不过,如果 CPU 占比很高而“other”占比较低,那通常是预期表现。
重要指标 — Major Page Faults(主页面错误)
监控主页面错误指标有助于判断 Elasticsearch 是否频繁从磁盘读取向量数据。
在 Linux 系统中,主页面错误发生于程序请求访问的内存页尚未加载到系统 RAM,需要从磁盘调入时。相比次要页面错误(数据已在内存但未映射),主页面错误耗时更长,且可能显著影响程序性能。
监控工具 — Metricbeat
Elastic 的 Metricbeat 是一款轻量级数据采集器,可安装在服务器上定期采集操作系统和服务的指标数据。其 system 模块能采集主页面错误数值,详情见:www.elastic.co/guide/en/be…。
这些指标可以在 Kibana 中可视化。kNN 搜索时若观察到主页面错误增加,说明部分向量未能存入页缓存,频繁从磁盘加载。
结合 Hot Threads 指标一起监控,可以最好地判断是否需要增加堆外内存,以保障 kNN 搜索性能。
聚焦最大峰值,我们可以看到实例3的页面错误在4个小时内超过了4592次。这远远高于我们通常期望看到的水平:
热点线程(Hot Threads)和主要页面错误(Major Page Faults)是诊断搜索性能下降的有效工具。在索引阶段,有一些事项需要注意。
索引注意事项
以下是一些你应当考虑的方面:
- 支持并发搜索,但大量索引操作会导致搜索变慢,因为索引会占用搜索的计算资源。
- 大量索引还会产生许多小段(segments),这对搜索性能不利。
- 一般建议尽量将活跃的索引/更新操作与搜索分开执行。因此,如果需要重新索引以更新向量嵌入,最好在单独的索引(甚至更好的是单独的集群)中进行,而不是在原有索引上直接修改。或者,可以选择在业务低峰期(比如夜间)进行重新索引(前提是存在低峰期)。
- 优先选择较少的 _bulk 请求,但每个请求包含更多向量。
- 尽量减少对服务器的请求次数。
- 初始批量大小可设置为 10MB,然后根据情况进行调整。
总结
本章深入探讨了在 Elasticsearch 中优化向量搜索性能的细节。我们介绍了多种调优技术,涵盖了机器学习模型部署、节点扩展以及配置调优的复杂性。介绍了 Rally 等工具来辅助特定用例的负载测试。此外,本章还聚焦于故障排查,借助集群监控指标和热点线程 API,为你提供了有效解决查询延迟问题的技能。
下一章,我们将从以文本为主的语义搜索,转向以图像为主的语义搜索,探索相关模型的历史与实际应用,进一步拓展我们的向量搜索能力。