2. 慢查询拖垮整个集群(雪崩)
"我们只是想看 Top 100 万男性用户行为,怎么全站挂了?"
- 💥 现象:一条复杂聚合上线,CPU 100%, load average 飙到 200+其他搜索全部超时
rejected execution of search request: [queue capacity reached]
rejected execution of search request: [queue capacity reached]
rejected execution of search request: [queue capacity reached]......
日志狂刷存在感,重启大法也无用:新请求进来,立刻又卡死:资源耗尽型血崩
这是为什么呢?话说你们感觉今天千问是不是挂了,奶茶下不了单
ES 查询流程:
- 客户端 → 协调节点(Coordinating Node)
- 协调节点 → 广播请求到 所有相关分片
- 各分片返回结果 → 协调节点合并、排序、聚合
- 返回最终结果
咱们复现一下:
GET /user_events/_search
{
"size": 0,
"aggs": {
"all_users": {
"terms": {
"field": "user_id.keyword",//keyword也不行了
"size": 1000000 // ← 问题根源!
}
}
}
}
协调节点发生了什么?
- 每个分片返回 100 万个桶(bucket)
- 假设 6 个分片 → 协调节点要 合并 600 万个桶
- 内存中构建巨大 HashMap → 堆内存瞬间吃满
- GC 停顿从 10ms → 5s+ (结合场景,不一定都这样,你懂的:G1GC 下,Full GC 很少发生)
- 线程池打满 → 新请求被拒绝(
rejected)
💡 聚合的
size不是"返回多少",而是"每个分片返回多少" !所以量 =size × 分片数
- 🕵️so咱们根因:terms 聚合
size=1000000或深分页from=50000,协调节点内存爆炸
🛠️ 解决方案:三重防御体系
这和之前有点重复,所以我写得也有点快
关键还是那几个核心,解决了核心 差不多可以解决很多问题
1、全局熔断:老朋友防君子了,这里只是取消查询,在timeout之前 内存恐怖早就炸了
# elasticsearch.yml
search.default_search_timeout: 30s # 超过 30 秒自动 cancel
2、所以还需要内存熔断,亲朋友防小人
# elasticsearch.yml
indices.breaker.request.limit: 40% # 单请求最多用 40% 堆
indices.breaker.total.limit: 70% # 所有 breaker 总和
这样的效果就是当聚合尝试分配内存超过阈值,ES 老娘得到授权直接甩脸子不干了
{
"error": {
"type": "circuit_breaking_exception",
"reason": "[request] Data too large, data for [<agg>] would be [12GB], which is larger than the limit of [10GB]"
}
}
3、解决了这些,还没解决产生问题的body
你小子查可不能随心所欲、瞎查!用 composite 聚合
GET /user_events/_search
{
"size": 0,
"aggs": {
"users": {
"composite": {
"size": 1000, // 每次只取 1000 个
"sources": [{ "user_id": { "terms": { "field": "user_id.keyword" } } }]
}
}
}
}
- 支持 分页遍历 (通过
after参数) 内存恒定,永不爆炸 - 官方推荐替代大
terms聚合
2)预计算 + 存结果索引
- 用 Spark/Flink 每天跑 Top N 用户
- 结果存入
daily_top_users索引 - ES 只负责查这个小索引
**实战对比:10 亿文档,**6 分片 用事实说话,效果杠杠
| 查询方式 | 协调节点内存峰值 | 是否成功 | 耗时 |
|---|---|---|---|
terms size=1000000 |
28 GB → OOM | ❌ 失败 | - |
terms size=10000 |
4.2 GB | ✅ 成功 | 18s |
composite size=1000 |
0.8 GB | ✅ 成功 | 2.1s/批 |
😅 "一条查询,团灭集群 ------ 这不是 bug,是你写的'核弹' ,把整个集群拖进了 GC 地狱 :" 哈哈 可惜场景不对;建议没事多去岛上转转。
下面咱们看看,生成环境的工具吧
Python 高风险查询检测脚本
连接 ES,分析 慢查询日志 或 当前运行任务,找出可能引发雪崩的查询
要想用,先按插件:pip install elasticsearch requests
#!/usr/bin/env python3
"""
检测 Elasticsearch 中的高风险查询:
- terms 聚合 size > 10000
- 深分页 from > 10000
- 无 timeout 的大查询
适用于 ES 7.x / 8.x
"""
import json
import argparse
from elasticsearch import Elasticsearch, RequestsHttpConnection
def is_risky_aggregation(aggs):
"""递归检查聚合是否高风险"""
if not aggs:
return False, ""
for name, agg in aggs.items():
# 检查 terms 聚合
if "terms" in agg:
size = agg["terms"].get("size", 10) # 默认 size=10
field = agg["terms"].get("field", "unknown")
if size > 10000:
return True, f"terms aggregation on '{field}' with size={size} (>10000)"
# 检查 composite(通常安全,但可设阈值)
if "composite" in agg:
size = agg["composite"].get("size", 1000)
if size > 5000:
return True, f"large composite aggregation with size={size}"
# 递归子聚合
for sub_key in ["aggs", "aggregations"]:
if sub_key in agg:
risky, reason = is_risky_aggregation(agg[sub_key])
if risky:
return True, reason
return False, ""
def is_risky_query(query_body):
"""检查查询整体是否高风险"""
risky, reason = False, ""
# 1. 检查深分页
from_val = query_body.get("from", 0)
size_val = query_body.get("size", 10)
if from_val > 10000:
risky, reason = True, f"deep pagination: from={from_val} (>10000)"
# 2. 检查聚合
if not risky and "aggs" in query_body:
risky, reason = is_risky_aggregation(query_body["aggs"])
# 3. 检查是否无超时(需结合上下文,此处仅提示)
if not risky and "timeout" not in str(query_body):
# 注意:timeout 可能在 URL 参数中,此处不严格判断
pass
return risky, reason
def scan_active_tasks(es_client):
"""扫描当前正在运行的搜索任务"""
print(" 扫描当前活跃搜索任务...")
try:
resp = es_client.tasks.list(actions="*search")
tasks = resp.get("nodes", {})
for node_id, node_info in tasks.items():
for task_id, task in node_info.get("tasks", {}).items():
if "search" in task.get("action", ""):
desc = task.get("description", "")
try:
query_body = json.loads(desc.split("] ")[-1]) # 粗略提取
risky, reason = is_risky_query(query_body)
if risky:
print(f"! [ACTIVE TASK] {reason}")
print(f" Task ID: {task_id}")
print(f" Query: {json.dumps(query_body, indent=2)[:200]}...\n")
except Exception as e:
continue # 解析失败跳过
except Exception as e:
print(f"获取活跃任务失败: {e}")
def scan_slowlog(es_client, index_pattern="*"):
"""扫描慢查询日志(需提前开启 slowlog)"""
print(f" 扫描索引 '{index_pattern}' 的慢查询日志...")
try:
# 查询 .tasks 或应用日志中的慢查询(简化版:假设你有 slowlog 索引)
# 实际中,slowlog 默认输出到 ES 日志文件,或可通过 Filebeat 收集到索引
# 此处演示从自定义 slowlog 索引读取(如 filebeat-*)
pass # 留空,因 slowlog 通常不在 ES 内部索引
except Exception as e:
print(f" 慢查询日志需通过日志系统分析(如 Kibana Logs)")
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--host", default="http://localhost:9200")
parser.add_argument("--username", default="")
parser.add_argument("--password", default="")
args
基础使用 python detect_risky_queries.py --host http://es-prod:9200
带认证 python detect_risky_queries.py --host https://es-prod:9200 --username admin --password secret
Kibana 告警规则模板(基于 Elastic Observability)
1、确保慢查询日志已收集 elasticsearch.yml中:此处可以红杏出墙想想mysql
index.search.slowlog.threshold.query.warn: 5s
index.search.slowlog.threshold.query.info: 10s
大前提:咱们并通过 Filebeat → Elasticsearch 收集日志
2、在 Kibana 创建告警规则
规则类型:Log threshold alert
- Index pattern :
filebeat-*(或你的 ES 日志索引)
#Criteria kql
message:"size=" AND message:"terms" AND
(message:*10000* OR message:*20000* OR message:*50000* OR message:*100000*)
或者:日志化结构
event.dataset: "elasticsearch.slowlog" AND
elasticsearch.slowlog.aggregation.size > 10000
- Threshold: ≥ 1 次/5分钟
- Actions: 发送钉钉/企业微信/Webhook/邮箱
**3、高级菜:**监控线程池拒绝数
- Rule type: Metric threshold
- Index :
.monitoring-es-* - Metric :
max by (node.name) (thread_pool_search_rejected) - Threshold: > 0
- 意义:一旦出现拒绝,说明已有查询压垮线程池!
💡 最佳实践组合:
- 用 Python 脚本 定期巡检(如 cron 每小时跑一次)
- 用 Kibana 告警 实时捕获线上慢查询
- 双保险,防雪崩于未然