ES排障两件套:慢查询日志阈值配置 + tcpdump 抓包看真实请求
排查 Elasticsearch 查询变慢,通常分两个层次:先用慢查询日志锁定"哪些查询慢",如果日志信息还不够(比如想确认客户端到底发了什么原始请求),再用 tcpdump 直接抓包看线上真实流量。
同时,慢日志还有一些容易被忽视的陷阱——比如它按分片记录、日志量可能看起来被放大 N 倍;写入也有独立的慢日志配置;以及 8.x 默认开启 HTTP 层 TLS 后,tcpdump 可能抓到的是密文。
以下记录完整的配置方法、常见坑与避坑要点。
版本说明
本文原写于 2023-02,2026-09 补充。慢日志阈值设置项在 7.x/8.x 通用;8.x 默认开启 HTTP 层 TLS,本文的 tcpdump 明文抓包方式在 8.x 默认配置下不可用。
# 1. 慢查询日志:按阈值分级记录
ES 的慢日志按 warn/info/debug/trace 四级阈值记录,而不是简单的"超过 N 秒就记"——这样可以在日志量和信息粒度之间取平衡:查询稍慢记一条 info,非常慢的才升级到 warn,方便后续按级别过滤。
按合理阈值记录(生产环境常态配置):
PUT /_all/_settings
{
"index.search.slowlog.threshold.query.warn": "5s",
"index.search.slowlog.threshold.query.info": "2s",
"index.search.slowlog.threshold.query.debug": "1s",
"index.search.slowlog.threshold.query.trace": "400ms",
"index.search.slowlog.threshold.fetch.warn": "1s",
"index.search.slowlog.threshold.fetch.info": "800ms",
"index.search.slowlog.threshold.fetch.debug": "500ms",
"index.search.slowlog.threshold.fetch.trace": "200ms"
}
2
3
4
5
6
7
8
9
10
11
query 阶段对应查询解析和执行,fetch 阶段对应从各分片取回文档内容——两者耗时特征不同,所以分开设阈值。query 慢通常是查询本身复杂(聚合、脚本),fetch 慢通常是命中文档过多或 _source 太大。
临时排查时,把阈值全部降到 0,记录所有语句(跟 MySQL 的 long_query_time=0 是同一个思路):
PUT /_all/_settings
{
"index.search.slowlog.threshold.query.warn": "0s",
"index.search.slowlog.threshold.query.info": "0s",
"index.search.slowlog.threshold.query.debug": "0s",
"index.search.slowlog.threshold.query.trace": "0s",
"index.search.slowlog.threshold.fetch.warn": "0s",
"index.search.slowlog.threshold.fetch.info": "0s",
"index.search.slowlog.threshold.fetch.debug": "0s",
"index.search.slowlog.threshold.fetch.trace": "0s",
"index.indexing.slowlog.threshold.index.warn": "0s",
"index.indexing.slowlog.threshold.index.info": "0s",
"index.indexing.slowlog.threshold.index.debug": "0s",
"index.indexing.slowlog.threshold.index.trace": "0s"
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
坑:跟 MySQL 全量慢日志一样,这是临时手段——全量记录会让日志量随 QPS 线性增长,排查完必须把阈值改回生产值,否则磁盘会被日志占满。
验证:跑几条查询后检查 logs/<cluster-name>_index_search_slowlog.log(路径见 path.logs 配置),应该能看到刚才的请求被记录,含耗时和查询体。
# 1.1 慢日志是按分片记录的
一条查询会在它命中的每个分片上各记一条慢日志。这意味着:
- 一条聚合查询命中 5 个主分片 → 5 条慢日志
- 10 个副本分片也参与了查询 → 可能更多
直接数日志行数得出的 "QPS" 会被严重高估。读慢日志时要按 took 和查询体聚合去重,不要把分片日志当成查询日志。
# 1.2 写入慢日志(indexing.slowlog)
上述配置中的 index.indexing.slowlog.* 控制的是写入(索引)的慢日志:
index.indexing.slowlog.threshold.index.*:写入耗时阈值index.indexing.slowlog.source:控制记录多少字节的原始文档- 默认 1000 字节
- 设
false关闭 - 设
true全量(文档体积大时会显著放大日志量)
# 1.3 慢日志级别与输出位置
慢日志走独立的 log4j2 appender,文件名形如 <cluster>_index_search_slowlog.log,与主日志分离。
- 阈值设为
-1表示关闭该级别 - 这些是 index-level dynamic setting,可以只对某个索引开启,而不是
/_all
排查单索引问题时,只对该索引设阈值,避免全集群日志暴涨:
PUT /my-problem-index/_settings
{
"index.search.slowlog.threshold.query.info": "1s"
}
2
3
4
# 2. 慢日志不够用时:tcpdump 直接抓 HTTP 请求体
慢日志记录的是 ES 解析后的查询信息,如果你怀疑问题出在客户端拼装的原始请求(比如某个 SDK 生成了意料之外的 DSL),或者想在不改集群配置的情况下临时观察流量,直接抓包更直接:
tcpdump -A -nn -s 0 'tcp dst port 9200 and (((ip[2:2] - ((ip[0]&0xf)<<2)) - ((tcp[12]&0xf0)>>2)) != 0)' -i lo
拆解这条命令:
-A:以 ASCII 显示包内容,这样能直接读出 HTTP 请求体里的 JSON,而不是十六进制-s 0:不截断包,抓取完整长度(默认snaplen可能会把长查询体截断)tcp dst port 9200:只抓发往 ES HTTP 端口的包(((ip[2:2] - ((ip[0]&0xf)<<2)) - ((tcp[12]&0xf0)>>2)) != 0):这是经典的"只保留有 payload 的包"过滤表达式(IP 总长度减去 IP/TCP 头长度 ≠ 0),排除掉纯 ACK 这类空包,避免抓包结果被大量噪音淹没-i lo:抓本机回环网卡;如果 ES 客户端和服务端不在同一台机器,改成对应的物理网卡名
验证:另开一个终端跑一条 curl -XGET localhost:9200/_search ...,tcpdump 的终端应该实时打印出这条请求的 HTTP 头和 JSON body。
坑:-A 模式下二进制/gzip 压缩的请求体会显示成乱码——如果客户端启用了 Content-Encoding: gzip,先临时关掉压缩再抓包,否则抓到的内容不可读。
# 2.1 tcpdump 的 8.x TLS 硬边界
Elasticsearch 8.x 默认开启 HTTP 层 TLS(xpack.security.http.ssl.enabled: true)。此时:
- tcpdump 抓到的是加密后的 TLS 记录层数据
-A显示的是不可读的密文,看不到 JSON body
此时的替代手段:
- 改用慢日志 trace 级别(上面 1.2 节的配置),捕捉解析后的查询
- 在客户端侧开 SDK 的请求日志(如 Python elasticsearch 的
debug级别 HTTP trace) - 在测试环境关闭 TLS 复现(仅限非生产环境验证)
这是排查时必须知道的一条硬边界——8.x 上 tcpdump 不能作为首选手段了。
# 2.2 落盘后用 Wireshark 分析
长会话或复杂查询的抓包结果,直接 -A 看滚动太快了。建议落盘后用 GUI 工具分析:
tcpdump -w capture.pcap -s 0 'tcp dst port 9200 and (((ip[2:2] - ((ip[0]&0xf)<<2)) - ((tcp[12]&0xf0)>>2)) != 0)' -i lo
-w capture.pcap:把抓包结果写入文件(二进制格式)- 用 Wireshark 打开后:
- 右键 Decode As → HTTP
- 或用 Follow → HTTP Stream 查看完整请求响应
这对 "批量 bulk 请求" 尤其有用,HTTP Stream 会把几十条小请求合并成一次 TCP 传输,能还原实际操作序列。
# 3. 两件套之外的第三件:_tasks 与 _nodes/hot_threads
慢日志告诉你"哪条查询慢",但集群卡顿时还有另外两件工具:
| 工具 | 命令 | 告诉你什么 | 适用时机 |
|---|---|---|---|
| Slowlog | 监控日志文件 | 哪条查询超过阈值、耗时多久 | 已知慢的查询 |
| hot_threads | GET /_nodes/hot_threads | 各节点此刻卡在什么调用栈上 | 集群整体变慢,怀疑 CPU 或 IO 瓶颈 |
| _tasks | GET /_tasks?actions=*search&detailed | 正在执行的搜索任务列表 | 想看当前有哪些查询在跑、跑了多久 |
hot_threads 示例:
GET /_nodes/hot_threads
输出是类似 Java stack trace 的格式,能看到各节点的最"热"线程在做什么——比如卡在 IndexSearcher.search() 是查询阶段,卡在 BytesReference 相关方法是取文档阶段。
_tasks 示例:
GET /_tasks?actions=*search&detailed=true
这个会列出所有正在执行的搜索任务,包括:
_id:任务唯一标识,可用于 cancel_source:原始查询 DSLrunning_time_in_nanos:已运行时长parent_task_id:如果这是子查询,它的父任务是谁
适用时机对照:
- 慢日志有阈值,它只能告诉你「已经过去了的慢查询」
- 如果只想知道「此刻卡在哪」,看 hot_threads
- 如果想 kill 掉某个正在跑的慢查询,先用 _tasks 找到 task_id,再 DELETE /_tasks/{task_id}
# 4. 可复用要点
- 慢查询定位优先用慢日志(结构化、按阈值分级),只有怀疑"客户端发的东西不对"时才上 tcpdump。
- 全量记录(阈值设 0 /
long_query_time=0)是排障期间的临时手段,排查完必须改回去。 - tcpdump 过滤表达式的核心技巧是"排除空 payload 包",否则会被 TCP 层的 ACK 噪音淹没。
- 8.x 默认开启 HTTP 层 TLS,tcpdump 抓到的是密文——此时优先用慢日志 trace 级别或客户端 SDK 日志。
- 总是按
took和查询体聚合慢日志去重,不要把分片日志当成查询日志。 - 单索引问题只对该索引开慢日志,不要全集群都来。
- 集群卡顿但慢日志空白时,用 hot_threads 看进程状态,用 _tasks 看当前正在跑的任务。
Agent 可直接解析的元数据块
{
"_meta": {
"version": "1.0",
"article_id": "es-slowlog-tcpdump-troubleshooting",
"scope": "Elasticsearch 6.x+",
"last_verified": "2023-02"
},
"quick_start": [
"PUT /_all/_settings 配置 search.slowlog.threshold",
"检查 logs/<cluster>_index_search_slowlog.log",
"tcpdump -A -nn -s 0 'tcp dst port 9200 and payload!=0' -i lo",
"GET /_nodes/hot_threads 排查当前卡顿",
"GET /_tasks?actions=*search&detailed 查看正在执行的任务"
],
"safety_rules": [
"阈值设 0 是临时手段,排查完必须改回",
"按 took 聚合去重,不要把分片日志当查询日志",
"8.x 默认 TLS 启用,tcpdump 看不到明文",
"单索引问题只对该索引开慢日志"
],
"verification": [
"跑 curl 测试后检查 slowlog 文件",
"tcpdump -A 确认能看到 HTTP body",
"hot_threads 输出包含 IndexSearcher.search 等关键词"
]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26