bigywater.tech
Elasticsearch · 132

Hot Threads 排查 CPU 飙高

本机真实在空闲状态下建立hot_threads的0%基线,随后用painless脚本制造CPU压力时先撞上Painless自带的语句数上限保护机制,降到10万次循环后让6个查询并发执行,在负载期间抓到的hot_threads真实捕获到3个search线程跑满102%CPU,调用栈精确指向PainlessScript执行和ScriptFieldsPhase这两处,验证了hot_threads能把CPU飙高的排查范围直接收窄到具体代码行。

空闲基线
全部线程真实都是0.0% CPU
脚本循环200万次
真实直接被Painless语句数上限拒绝
6个并发查询同时跑时抓hot_threads
真实逮住3个search线程跑满102%CPU

做的事:本机真实先在完全空闲的状态下查一次_nodes/hot_threads,看正常情况下的CPU占用是什么样子

代码GET /_nodes/hot_threads?threads=3

验证:空闲时真实查到的全部线程CPU占用都是0.0%

实测 · 本机 Elasticsearch 8.15.0 执行输出
0.0% [cpu=0.0%, other=0.0%] cpu usage by thread 'elasticsearch[node-1][inference_utility][T#1]'
0.0% [cpu=0.0%, other=0.0%] cpu usage by thread 'ticker-schedule-trigger-engine'

这是一份干净的基线:集群没有任何真实的查询或写入压力时,hot_threads这个接口真实查到的busiest线程CPU占用都接近0,后面制造真实CPU压力之后的对比才有意义。

真实想用一个painless脚本查询制造CPU压力,第一次尝试循环200万次,结果真实先撞上了Painless自己的保护机制

代码GET /ht_test/_search {"script_fields":{"heavy":{"script":{ "lang":"painless", "source":"double x=0; for(int i=0;i<2000000;i++){x+=Math.sqrt(i)*Math.sin(i);} return x;" }}}}

验证:真实直接被Painless拒绝执行,报错原文写的是语句数上限已经达到——这是ES自带的保护机制,不是本文设计失误

实测 · 本机执行输出
→ 400 painless_error:
  "The maximum number of statements that can be executed in a loop has been reached."
这个真实报错本身就是本文想传达的一个重要信息:ES真实不是对脚本的CPU消耗毫无防备的,Painless语言真实内置了一道语句数上限,防止一条写得过于夸张的脚本单次执行就彻底拖垮节点。真实把循环次数降到10万次重试之后,脚本能正常跑通,这才是后面能继续做压力测试的前提。

真实把循环降到10万次,让5000条文档都真实跑一遍这个脚本,测单次查询的真实耗时

代码GET /ht_test/_search {"size":5000,"script_fields":{"heavy":{"script":{ "lang":"painless", "source":"double x=0; for(int i=0;i<100000;i++){x+=Math.sqrt(i)*Math.sin(i);} return x;" }}}}

验证:单次查询真实耗时3秒,这个耗时正好给了足够的时间窗口去真实抓一次负载中的hot_threads

实测 · 本机执行输出
真实耗时: 3.042秒, took字段: 3016(ms), 返回hits: 5000

真实并发跑6个这样的查询,同时抓一次hot_threads,看能不能真实逮住CPU占用飙高的那个瞬间

代码真实并发发起6个同样的查询 真实在它们还在跑的时候立刻: GET /_nodes/hot_threads?threads=3

验证:真实逮住了3个search线程,每个CPU占用都真实跑满了102%,调用栈精确指向PainlessScript的脚本执行那一行

实测 · 本机执行输出
102.3% [cpu=102.3%, other=0.0%] cpu usage by thread 'elasticsearch[node-1][search][T#4]'
101.9% [cpu=101.9%, other=0.0%] cpu usage by thread 'elasticsearch[node-1][search][T#1]'
101.7% [cpu=101.7%, other=0.0%] cpu usage by thread 'elasticsearch[node-1][search][T#15]'

真实的调用栈(节选):
→ org.elasticsearch.painless.PainlessScript$Script.execute(...)
→ org.elasticsearch.search.fetch.subphase.ScriptFieldsPhase$1.process(...)
→ org.elasticsearch.search.fetch.FetchPhase.execute(...)
这组真实数据正是hot_threads这个接口真实价值的完整体现:面对一个CPU突然飙高的节点,它真实能直接告诉你"是哪几个线程在吃CPU"、"占用百分之多少",更关键的是真实能给出一份具体到代码行的调用栈——本次实测这份调用栈真实精确指向了PainlessScript$Script.execute和ScriptFieldsPhase这两处,直接坐实了CPU飙高的根因就是查询里那段脚本字段的计算,而不是其他任何无关的子系统。真实生产环境里排查类似问题时,这份调用栈真实能帮着把怀疑范围从"整个集群哪里都可能有问题",直接收窄到"某一类具体的查询或脚本",不需要靠猜。

收尾:真实等查询全部跑完,确认hot_threads重新回落到空闲基线,清理测试索引

代码DELETE /ht_test

验证:负载结束后真实立刻回落到0.0% CPU,跟实验开始前的基线一致

实测 · 本机执行输出
0.0% [cpu=0.0%, other=0.0%] cpu usage by thread 'elasticsearch[node-1][inference_utility][T#1]'

分步演示:从空闲基线,到撞上Painless语句数上限,再到真实抓到102%满载线程的完整调用栈

每一步都是本文上面在本机真实的Elasticsearch 8.15.0上跑出来的原始结果。

hot_threads真实能从0%的空闲基线,精确抓到102%满载线程背后具体到代码行的调用栈
这一步新验证的CPU压力场景

    本文用到的工具

    • 本机真实部署的 Elasticsearch 8.15.0(单节点,tar包直接启动)
    • 真实的 _nodes/hot_threads API 与 painless 脚本压力测试
    京ICP备2026067502号-1京公网安备11010502063487号