排查和降低 Elasticsearch JVM 内存压力

JVM 内存使用率过高会降低集群性能,并可能触发熔断器错误。为避免这些问题,如果节点的 JVM 内存使用率持续超过 85%,建议采取措施降低内存压力。

诊断 JVM 内存压力过高

检查 JVM 内存压力

Elasticsearch 的 JVM 使用 G1GC 垃圾回收器。因此,堆使用量会随时间呈现锯齿状变化,详见 理解 JVM 堆内存。报告的堆百分比是瞬时测量值,会随回收活动波动。监控时应重点关注 JVM 内存压力,它基于老年代垃圾回收的滚动平均值,更能反映节点持续的 JVM 响应能力。

使用节点统计 API 计算每个节点当前的 JVM 内存压力:

GET _nodes/stats?filter_path=nodes.*.name,nodes.*.jvm.mem.pools.old

根据输出,将 used_in_bytes 除以 max_in_bytes,即可得到内存压力。例如,将输出保存为 nodes_stats.json,再使用第三方 JQ 工具处理:

cat nodes_stats.json | jq -rc '.nodes[]|.name as $n|.jvm.mem.pools.old|{name:$n, memory_pressure:(100*.used_in_bytes/.max_in_bytes|round) }'

Elastic Cloud Hosted 和 Elastic Cloud Enterprise 的部署概览页面,也为集群中的每个节点提供 JVM 内存压力指示器。当内存压力达到 75% 时,指示器会变红。更多信息见 内存压力监控。

检查垃圾回收日志

内存使用量越高,垃圾回收通常越频繁、耗时越长。可以在 elasticsearch.log 中跟踪垃圾回收事件的频率与持续时间。例如,下面的事件表明,Elasticsearch 在最近 40 秒中有 21 秒用于垃圾回收,占比超过 50%。

[timestamp_short_interval_from_last][INFO ][o.e.m.j.JvmGcMonitorService] [node_id] [gc][number] overhead, spent [21s] collecting in the last [40s]

垃圾回收活动也可能出现在 nodes hot threads API 输出的 OTHER_CPU 类别中,相关说明见 排查高 CPU 使用率。

为了达到理想的 JVM 性能,垃圾回收应满足:

GC 类型完成时间频率
年轻代 GC小于 50 毫秒约每 10 秒一次
老年代 GC小于 1 秒每 10 分钟不超过一次

捕获 JVM 堆转储

要确定高 JVM 内存压力的确切原因,可在内存使用量高时捕获并分析 JVM 堆转储。

如果有 Elastic 订阅,可以请求 Elastic 协助分析。联系支持时,请遵循以下要求:

  • 在支持工单中书面授权 Elastic 查看上传的堆转储。
  • 转储可能包含私密信息,必须取得所需的业务审批后才能分享。文件按照 Elastic 的隐私声明处理。
  • 通过安全的 Support Portal 分享。文件过大无法上传时,可以在工单中请求安全上传 URL。
  • 同时提供覆盖相同时间段的垃圾回收日志。

监控 JVM 内存压力

使用 AutoOps 检测和解决问题:AutoOps 实时检测 Elasticsearch 集群的问题,并提供解决办法与性能优化建议。详见 AutoOps 介绍。

根据部署类型,选择以下方式持续跟踪 JVM 内存压力。

对于提供部署日志、指标与性能页面的云部署:

  • 推荐启用 AutoOps。
  • 启用日志和指标。之后可在 Kibana 的“Stack Monitoring”页面查看监控信息,也可启用 JVM 内存阈值告警,通过电子邮件获知潜在问题。
  • 在部署菜单中打开“Performance”页面,查看内存压力故障排查图表。

对于使用 Elasticsearch 监控的部署:

  • 推荐启用 AutoOps。
  • 启用 Elasticsearch 监控。日志和指标启用后,在 Kibana 的“Stack Monitoring”页面查看信息,也可以启用 JVM 内存阈值邮件告警。

降低 JVM 内存压力

下面列出一些常见的缓解建议。

常见配置问题

有些配置问题即使在没有明显负载时,也会让内存压力持续偏高,或者在性能问题出现时产生非线性反应。

禁用交换

Elasticsearch 的 JVM 自行管理可执行内容,操作系统交换可能使性能严重下降,因此建议禁用 swap。

Elasticsearch 建议在操作系统层面完全禁用交换。Elasticsearch 层面的设置只能尽力避免交换,但交换一旦发生就可能严重影响性能。通过节点统计 API 查看是否有节点正在交换:

GET _nodes/stats?filter_path=**.swap,nodes.*.name

例如,将输出保存为 nodes_stats.json,再用 JQ 处理:

cat nodes_stats.json | jq -rc '.nodes[]|{name:.name, swap_used:.os.swap.used_in_bytes}' | sort

如果在 Elasticsearch 层面尝试禁用后,仍发现节点发生交换,就需要进一步在操作系统层面禁用,以避免性能影响。

启用压缩普通对象指针

JVM 性能很大程度上依赖压缩普通对象指针(Compressed OOPs)是否启用。可以使用压缩指针的最大堆阈值取决于操作系统,通常约为 30GB。使用节点信息 API 查看:

GET _nodes?filter_path=nodes.*.name,nodes.*.jvm.using_compressed_ordinary_object_pointers

例如,将输出保存为 nodes.json,再用 JQ 处理:

cat nodes.json | jq -rc '.nodes[]|{node:.name, compressed:.jvm.using_compressed_ordinary_object_pointers}'

将堆限制在总内存的一半以内

Elasticsearch 默认自行管理 JVM 堆大小。如果手动覆盖,Xms 与 Xmx 应相等,并且不超过操作系统总 RAM 的一半。详细建议见 设置 JVM 堆大小。

通过节点信息 API 查看堆设置:

GET _nodes?filter_path=nodes.*.name,nodes.*.jvm.mem

例如,将输出保存为 nodes.json,再用 JQ 处理:

cat nodes.json | jq -rc '.nodes[]|.name as $n|.jvm.mem|{name:$n, heap_min:.heap_init, heap_max:.heap_max}'

减少分片数量

每个分片都消耗内存。通常,少量大分片比大量小分片使用更少资源。减少分片数量的建议见 规划分片大小。

常见流量问题

以下建议针对流量模式引起的 JVM 内存压力。

避免高成本搜索

高成本搜索可能消耗大量内存。启用慢日志可以更好地跟踪集群中的此类搜索。

高成本搜索可能具有很大的 size 参数、包含大量桶的聚合,或者使用昂贵的查询。可以考虑:

  • 通过索引设置 index.max_result_window 降低 size 上限。
  • 通过集群设置 search.max_buckets 减少允许的聚合桶数量。
  • 通过 search.allow_expensive_queries 禁用高成本查询。
  • 通过 search.default_search_timeout 设置默认搜索超时。
PUT _settings
{
  "index.max_result_window": 5000
}
PUT _cluster/settings
{
  "persistent": {
    "search.max_buckets": 20000,
    "search.allow_expensive_queries": false,
    "search.default_search_timeout": "1m"
  }
}

提示:高效编写 ES|QL 查询的专门建议,见 优化 ES|QL 查询性能。

防止映射膨胀

定义过多字段,或字段嵌套过深,可能导致消耗大量内存的映射膨胀。使用映射限制设置,约束字段映射数量。

还可以配置 Kibana 高级设置 data_views:fields_excluded_data_tiers,阻止 Kibana 从指定数据层获取字段数据,以改善性能。例如,要排除通常用于可搜索快照的 cold 与 frozen 层,可设置为 data_cold,data_frozen。这样有助于 Discover 更快载入字段,详见 Kibana Discover 加载的六个常见问题排查指南。

分散批量请求

批量索引和多搜索请求虽然比单独请求更高效,但过大的批量请求仍会造成高 JVM 内存压力。如果可以,提交较小的请求,并拉长请求之间的间隔。

增加节点内存

高强度索引与搜索负载可能造成高 JVM 内存压力。升级节点、增加内存容量,可以更好地承受繁重工作负载。

减少字段数据使用

字段数据与全局序数的计算可能消耗大量 CPU。默认情况下,全局序数在搜索时计算,也可以启用预加载,让它在摄入后计算。

字段数据载入 JVM 堆缓存,并根据使用频率保留。它能占用的堆内存上限,是字段数据缓存设置与字段数据熔断器限制中的较小者。熔断器错误会表现为请求被拒绝。将 indices.fielddata.cache.size 设得过低,会造成抖动与频繁驱逐。

使用 cat nodes API 查看字段数据驱逐情况,并确定字段数据是否占用了大量 JVM 内存:

GET _cat/nodes?v=true&h=name,heap.*,fielddata.*

如果输出表明字段数据是主要内存消耗来源,使用 cat fielddata API 确定哪些字段使用了字段数据,以及各节点的使用量:

GET _cat/fielddata?v=true&s=size:desc

可以使用 clear cache API 清除字段数据缓存,临时降低内存使用量。例如,仅清除 my-index-000001 中 fieldname1 与 fieldname2 的缓存:

POST my-index-000001/_cache/clear?fielddata=true&fields=fieldname1,fieldname2

清除所有索引的字段数据缓存:

POST */_cache/clear?fielddata=true&expand_wildcards=all&ignore_unavailable=true

字段数据内存使用量高的常见原因包括:

  • 在 text 字段上启用 fielddata。应改用多字段,并针对 keyword 字段搜索。
  • 对已经计算全局序数的高基数字段进行聚合或排序。例如,Kibana 为非 text 字段提供自动补全或加载可视化时可能发生。相关建议见 避免全局序数加载。

原文:High JVM memory pressure。作者/维护者:Elastic 文档团队。本文为原文的中文译文;代码保留原文内容。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容