Elasticsearch线上问题之慢查询

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 查询流程:

  1. 客户端 → 协调节点(Coordinating Node)
  2. 协调节点 → 广播请求到 所有相关分片
  3. 各分片返回结果 → 协调节点合并、排序、聚合
  4. 返回最终结果

咱们复现一下:

复制代码
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 告警 实时捕获线上慢查询
  • 双保险,防雪崩于未然
相关推荐
南极星10059 小时前
我的创作纪念日--128天
java·python·opencv·职场和发展
前端小菜袅9 小时前
PC端原样显示移动端页面方案
开发语言·前端·javascript·postcss·px-to-viewport·移动端适配pc端
Highcharts.js9 小时前
如何使用Highcharts SVG渲染器?
开发语言·javascript·python·svg·highcharts·渲染器
郝学胜-神的一滴9 小时前
超越Spring的Summer(一): PackageScanner 类实现原理详解
java·服务器·开发语言·后端·spring·软件构建
摇滚侠9 小时前
Java,举例说明,函数式接口,函数式接口实现类,通过匿名内部类实现函数式接口,通过 Lambda 表达式实现函数式接口,演变的过程
java·开发语言·python
阿里嘎多学长9 小时前
2026-02-03 GitHub 热点项目精选
开发语言·程序员·github·代码托管
Tony Bai9 小时前
“Go 2,请不要发生!”:如果 Go 变成了“缝合怪”,你还会爱它吗?
开发语言·后端·golang
打工的小王9 小时前
java并发编程(七)ReentrantReadWriteLock
java·开发语言
lang201509289 小时前
Java并发革命:JSR-133深度解析
java·开发语言