查询速度提升 98.9%,索引吞吐量提升 4 倍:系统化诊断 Elasticsearch 性能
作者:来自 Elastic Aleksandar Panov
结合使用 AutoOps、 Profile API 和 ES Rally 来发现集群热点、慢查询和索引瓶颈,并通过真实基准测试展示延迟降低 98.9%、索引吞吐量提升 4 倍。
想体验一下 Elastic Cloud 吗?订阅 AWS Marketplace 上的 Elastic Cloud 或 Microsoft Azure Marketplace 上的 Elastic Cloud,即可获得最高 1,000 美元的账单抵扣额度。
AutoOps、 Profile API 和 ES Rally 这三款 Elastic 工具能够在整个技术栈的各个层面系统化地诊断 Elasticsearch 的性能问题。在一个物流配送场景中,它们发现了导致搜索延迟飙升至 30 秒的分片不均衡、一个将 98.9% 执行时间浪费在深度分页上的查询,以及将批量写入吞吐量限制到可实现水平四分之一的索引设置。本文将介绍每种工具、它们能够发现的问题,以及如何利用这些发现来解决问题。
当有数十位用户持续连接并使用由 Elasticsearch 集群支撑的平台时,快速了解潜在的性能瓶颈以及如何解决它们至关重要。首先要回答的问题是:应该从哪里开始?下面是一个可供参考的决策路径:
该路径首先从 AutoOps 开始,用于检测集群级别的问题,例如资源压力或不合理的分片分布。如果集群状态正常,则使用 Profile API 来识别慢查询;候选查询可以来自慢查询日志或应用侧日志。接下来,使用 ES Rally 对可能限制吞吐量的索引设置进行基准测试。如果这三个工具都没有发现问题,那么瓶颈就在客户端。
如何使用 AutoOps 检测集群级别的性能问题
首先要回答的问题始终都是:问题是否出在集群本身?
AutoOps 的设计目标是提供实时集群诊断、给出针对性的建议,并帮助你快速提升集群健康状况。它默认包含在 Elastic Cloud 中,并且最近已作为一种https://www.elastic.co/blog/autoops-free。
它的设置非常简单,只需不到五分钟,你就可以看到数据开始流入 AutoOps 提供的一整套综合图表。其背后的思路是在靠近集群的位置部署一个 Elastic Agent,它会将数据上报到 AutoOps 平台,而该平台与你的 Elastic Cloud 账户相连接。下面是安装指南。
它的一个优点是能够立即提供那些难以通过现有集群监控技术发现的警告和建议。更有价值的是,它还会直接展示这些问题的解决方案。
在我们的物流配送场景中,AutoOps 发现了一项能够解释用户反馈性能缓慢的问题:集群负载严重不均衡。节点 es01 几乎承担了所有流量,而另外三个节点几乎处于空闲状态,搜索延迟飙升至 30 秒,正如我们将在下一节的图表中看到的那样。
AutoOps 如何发现节点热点和分片不均衡
AutoOps 的节点性能图表显示,es01 几乎承担了全部索引流量(5.6 docs/sec),而 es02、es03 和 es04 则基本处于空闲状态。
如下图所示,CPU 使用率集中在单个节点(es01)上,该节点承载了负载更高的索引,而另外三个节点大部分时间都处于空闲状态。
异常行为的下一个信号出现在搜索延迟图中。通过使用 AutoOps 的节点级搜索延迟视图,我们发现了一些出乎意料的结果。
当其他节点几乎没有搜索延迟时,负载最高的节点却表现出了影响搜索应用的延迟症状。它不仅承担了写入任务,还负责处理所有搜索请求。分析过程中,我们发现一个配送索引仅存储在一个节点上,而没有分布到全部四个节点。这就是问题的根本原因。通过重新索引数据(增加主分片数量),我们平衡了查询负载,并消除了这一主要问题。
本文聚焦于 AutoOps 的一个发现,以便使诊断流程更加清晰。如果你希望深入了解 AutoOps 能够发现的各种问题,可以参考关于热点( hotspotting )、CPU 使用率过高以及长时间运行查询的专题文章。
如何使用 Profile API 查找 Elasticsearch 慢查询
当集群恢复健康之后,下一个问题就是:是否存在某些开销很高的查询?第一步是找到这些候选查询。你可以通过 Elasticsearch 的慢查询日志,或者通过应用侧日志记录每次搜索请求所花费的时间来进行分析。在我们的案例中,问题出现在配送跟踪页面的无限滚动场景中 —— 随着用户不断向下滚动结果,性能逐渐变差。
Profile API 是一个用于分析长时间运行查询并查找搜索瓶颈的强大工具。使用起来非常简单:只需在任意搜索请求中将profile="true"打开,响应结果中就会包含一个 profile 部分,提供详细的耗时拆解。它能够指出哪个阶段占据了主要执行时间,例如查询阶段、聚合阶段或数据获取阶段。
下面我们来看一个深度分页的案例。在移动应用的无限滚动场景中,用户持续向下滚动,应用则每次请求获取 100 条文档。
随着用户持续滚动,搜索请求会进入越来越深的分页,而from参数也会不断增大:
GET delivery-records/_search { "profile": true, "from": 9000, "size": 100, "query": { "bool": { "filter": [ { "term": { "payment_type": "1" } }, { "range": { "tax_amount": { "gte": 5 } } } ] } }, "sort": [ { "delivery_pickup_datetime": { "order": "desc" } } ] }下面汇总的 Profile 数据(已从纳秒转换为毫秒)清楚地显示了瓶颈:大部分时间都消耗在查询阶段和获取阶段之外,由巨大的from偏移量导致的协调工作上。Elasticsearch 必须收集 9,100 个匹配文档,对它们进行排序,丢弃前 9,000 个文档,然后只返回请求的 100 个文档。
| 组件 | 阶段 | 时间 |
|---|---|---|
| ConstantScoreQuery 包含项 | 查询 | 34.4ms |
| BooleanQuery | 查询 | 31.1ms |
| QueryPhaseCollector 包含项 | 收集 | 22.1ms |
| SimpleFieldCollector(9,100 个文档优先级队列) | 收集 | 19.1ms |
| FetchPhase | 获取 | 12.1ms |
| 其他步骤,例如请求解析和反序列化、队列等待时间、响应构建 | 1,885.4ms | |
| 总计 | 2,004.2ms |
使用 search_after 修复深度分页
在这种情况下,search_after方法最多可以快两倍,因为它避免了扫描和丢弃之前的结果。相反,它会从第 90 页返回的最后一个文档继续执行,使用其delivery_pickup_datetime值(以 epoch 毫秒表示)作为游标,并且只获取接下来的 100 条记录。
GET delivery-records/_search { "profile": true, "size": 100, "track_total_hits": false, "query": { "bool": { "filter": [ { "term": { "payment_type": "1" } }, { "range": { "tax_amount": { "gte": 5 } } } ] } }, "sort": [ { "delivery_pickup_datetime": { "order": "desc" } } ], "search_after": [ 1451604674000 ] }如下方对比所示,使用search_after方法时性能有了显著提升。
| 组件 | 阶段 | 时间 |
|---|---|---|
| ConstantScoreQuery 包含项 | 查询 | 6.8ms |
| BooleanQuery | 查询 | 6.2ms |
| QueryPhaseCollector 包含项 | 收集 | 2.6ms |
| PagingFieldCollector | 收集 | 2.0ms |
| FetchPhase | 获取 | 2.2ms |
| 其他步骤,例如请求解析和反序列化、队列等待时间、响应构建 | 9.4ms | |
| 总计 | 29.2ms |
性能对比
| 组件 | 深度分页(from:9000) | search_after | 时间节省 | 百分比 已节省 |
|---|---|---|---|---|
| ConstantScoreQuery | 34.4ms | 6.8ms | 27.6ms | 80.2% |
| BooleanQuery | 31.1ms | 6.2ms | 24.9ms | 80.1% |
| QueryPhaseCollector(总计) | 22.1ms | 2.6ms | 19.5ms | 88.2% |
| FieldCollect | 19.1ms | 2.0ms | 17.1ms | 89.5% |
| FetchPhase | 12.1ms | 2.2ms | 9.9ms | 81.8% |
| 总计 | 2,004.2ms | 29.2ms | 1,975ms | 98.5% |
查询总时间从 1,004 ms 降低到 29 ms(提升 98.5%),几乎完全是因为 Elasticsearch 不再需要在每次请求中构建并丢弃一个包含 9,000 个文档的优先队列。
如何使用 ES Rally 对 Elasticsearch 索引设置进行基准测试
在集群健康并且查询优化完成后,最后的问题是:索引设置本身是否成为瓶颈?
ES Rally 是由 Elastic 制作的官方基准测试工具。它的核心优势是可复现性:你可以在相同的集群和硬件上,针对两个配置运行相同的工作负载,因此结果中的任何差异都完全取决于你更改的设置。ES Rally 能够在完全相同的条件下测量改进效果:相同的集群、硬件和数据集。
为什么默认 Elasticsearch 索引设置会限制批量索引吞吐量
在很多情况下,较慢的索引速度是由默认索引设置导致的,这些设置适合开发环境,但不适合生产环境。例如,默认的refresh_interval为 1 秒会增加资源使用,因为每次 刷新都会创建一个新的可搜索段,而副本数量为 1 会使每个索引请求的写入操作数量翻倍。
例如,我们的交付物流平台每天批量导入数千条新记录,这正是这些默认设置会造成影响的工作负载类型。将refresh_interval设置为 -1 可以在批量加载阶段禁用刷新操作,而临时将副本数量降低到 0 可以减少一半的写入操作。导入完成后,这两个设置都会恢复。
在这个演示中,我们不会重点介绍如何准备自定义数据用于基准测试,但值得一提的是,有一篇关于这方面的不错文章。
为了进行基准测试,你需要设置两个文件夹:一个包含当前设置,另一个包含候选设置。在这个案例中,示例数据集包含 100 万条记录。下面描述的所有文件都可以在这个代码仓库中找到。
两个 track 文件夹都包含一个index-settings.json文件。这些文件允许你调整映射、副本分片数量、分片设置以及字段类型;例如,将字段从double转换为scaled_float,或者从text转换为keyword。因为 ES Rally track 可以快速重新运行,所以在迭代过程中尝试不同配置并评估新的优化思路会非常容易。
下一步是运行 race 命令(用于当前配置和候选配置的 race),以收集统计数据。
esrally race \ --track-path="delivery-records-current" \ #(name of current setup folder) --target-hosts="es01:9200" \ #(location of es cluster) --pipeline=benchmark-only \ --report-format=csv \ --report-file="current.csv" \ --race-id="run-current" \ --on-error=abortesrally race \ --track-path="delivery-records-contander" \ --target-hosts="es01:9200" \ --pipeline=benchmark-only \ --report-format=csv \ --report-file="contander.csv" \ --race-id="run-contander" \ --on-error=abort快速提示:使用有意义的--race-id值(而不是自动生成的值)会让比较命令更容易运行。
在两个 race 都完成后,比较结果:
esrally compare --baseline=run-current --contender=run-contander此外,报告会生成到 csv 文件中,可以轻松进行处理以提取重要数据。
| 指标 | 当前设置 | 候选设置 | 变化 |
|---|---|---|---|
| 累积索引时间 | 0,826517 | 0,755967 | +8.54% |
| 平均吞吐量 | 11,090 docs/s | 50,921 docs/s | +359 % |
| 中位数吞吐量 | 10,821 docs/s | 51,304 docs/s | +374 % |
| 最小吞吐量 | 10,335 docs/s | 41,387 docs/s | +300 % |
| 最大吞吐量 | 13,006 docs/s | 55,342 docs/s | +326% |
候选配置仅通过在批量加载阶段调整refresh_interval和副本数量,就实现了大约 3–4 倍更快的索引吞吐量。
在实际应用中,单次比较通常是不够的。常见的后续实验包括将 double 字段转换为 float 或 scaled_float,将 text 字段更改为 keyword,调整分片数量,以及调优 refresh interval。因为 ES Rally track 可以快速重新运行,所以迭代测试这些选项非常简单。
当 Elasticsearch 不是瓶颈时应该做什么
如果 AutoOps 显示集群健康,Profile API 显示查询速度很快,并且 ES Rally 确认索引设置不是限制因素,那么瓶颈就在客户端。Elasticsearch 端已经不再是需要关注的地方。常见的应用层原因包括:
网络延迟,存在于客户端和集群之间,尤其是在跨区域、VPN 或代理环境中。
客户端反序列化大型响应负载时产生的开销。使用 _source exclude 或 fields 参数,仅返回客户端实际需要的内容。
客户端队列等待,发生在请求到达集群之前。Elasticsearch 客户端使用 HTTP 连接池。在并发负载下,请求需要排队等待可用连接。Profile API 永远看不到这种等待,因为请求还没有离开客户端。
单次搜索调用,而 _msearch 可以将独立查询批量处理为一次网络往返。
单文档索引,而 _bulk API 可以将每次请求的开销分摊到多个文档上。
AutoOps、Profile API 和 ES Rally 的诊断价值正是在于,它们可以让你在转向应用代码之前,明确排除集群、查询和索引设置的问题。当这三个工具都没有发现问题时,调查方向就会从 Elasticsearch 转移到客户端。
要在应用侧调查这些问题,Elastic APM 是自然的下一步工具:它可以追踪客户端中的请求路径,并准确显示 Profile API 无法看到的那类集群前等待时间。
结论
回到最初的问题,即交付物流平台中的搜索缓慢问题,我们从基础设施层一直追踪到了查询和设置层:
AutoOps 发现了不均衡的分片分布,将所有查询负载集中到了单个节点上,导致最终用户遇到最高达 30 秒的延迟峰值。
Profile API 显示深度分页是慢查询的来源。从 from/size 切换到
search_after消除了 98.9% 的延迟。ES Rally 确认,在批量导入期间优化索引设置,特别是
refresh_interval和副本数量,可以将吞吐量提升 3–4 倍。
每个工具都回答不同的问题。AutoOps 提供“是否存在结构性问题?”的视角。Profile API 回答“为什么这个特定查询很慢?”ES Rally 验证“这些修改是否真的能改善性能,以及改善多少?”将这些工具结合使用,并按照这个顺序执行,它们可以覆盖应用 Elasticsearch 侧完整的诊断范围。当这三个工具都没有发现问题时,下一步就是按照上一节所述检查客户端。
原文:Diagnose Elasticsearch performance with AutoOps and ES Rally - Elasticsearch Labs