一、当Elasticsearch开始"喘不过气"时

最近遇到个挺有意思的案例:某电商平台的商品搜索接口,在促销活动时查询延迟从平时的50ms飙升到800ms。这就像高峰期挤地铁,明明平时很顺畅,突然就堵得水泄不通。

我们先看看这个集群的基本配置:

  • 3个数据节点(16核64GB内存)
  • 5亿条商品数据
  • 每天约2000万次查询

问题爆发时的情况:

// 查询语句示例(技术栈:Elasticsearch 7.x)
GET /products/_search
{
  "query": {
    "bool": {
      "must": [
        {"match": {"title": "智能手机"}},  // 关键词匹配
        {"range": {"price": {"gte": 1000}}}  // 价格过滤
      ],
      "filter": [
        {"term": {"status": 1}}  // 状态过滤
      ]
    }
  },
  "sort": [{"sales": "desc"}],  // 按销量排序
  "from": 0,                    // 分页起始
  "size": 20,                   // 每页条数
  "aggs": {                     // 聚合分析
    "brand_distribution": {
      "terms": {"field": "brand_id"}
    }
  }
}

二、给Elasticsearch做"体检"

2.1 先看硬件指标

通过监控发现三个典型症状:

  1. CPU使用率长期90%+
  2. JVM内存频繁GC
  3. 磁盘IO等待时间超过30ms

2.2 查询分析不容忽视

使用Profile API抓取慢查询:

GET /products/_search?profile=true
{
  "query": { /* 同上 */ }
}

// 返回结果片段:
"collector": [
  {
    "name": "CancellableCollector",
    "time": "243.753ms",  // 收集耗时
    "children": [
      {
        "name": "SimpleTopScoreDocCollector",
        "time": "235.214ms"  // 评分计算耗时
      }
    ]
  }
]

三、开出的"药方清单"

3.1 索引层面的"减肥手术"

重构商品索引的mapping:

PUT /products_new
{
  "settings": {
    "number_of_shards": 6,               // 从3个分片增加到6个
    "number_of_replicas": 1,
    "refresh_interval": "30s"            // 刷新频率从1s调整为30s
  },
  "mappings": {
    "properties": {
      "title": {
        "type": "text",
        "fields": {
          "keyword": {"type": "keyword"}  // 添加keyword子字段
        }
      },
      "brand_id": {
        "type": "keyword",               // 精确值改为keyword
        "doc_values": true               // 启用列式存储
      }
    }
  }
}

3.2 查询语句的"精简之道"

优化后的查询模板:

GET /products/_search
{
  "query": {
    "bool": {
      "filter": [                        // 把能转filter的都移过来
        {"term": {"status": 1}},
        {"match": {"title": "智能手机"}},
        {"range": {"price": {"gte": 1000}}}
      ]
    }
  },
  "sort": [
    {"_score": {"order": "desc"}},       // 先按相关性
    {"sales": {"order": "desc"}}         // 再按销量
  ],
  "track_total_hits": false,             // 不计算总命中数
  "size": 20
}

四、效果验证与深度调优

4.1 冷热数据分离方案

配置ILM策略实现自动迁移:

PUT _ilm/policy/hot_warm_policy
{
  "policy": {
    "phases": {
      "hot": {
        "min_age": "0ms",
        "actions": {
          "rollover": {
            "max_size": "50gb",          // 热节点最大50GB
            "max_age": "7d"
          }
        }
      },
      "warm": {
        "min_age": "7d",
        "actions": {
          "allocate": {
            "require": {
              "data": "warm"            // 标记为温数据
            }
          }
        }
      }
    }
  }
}

4.2 JVM参数的精细调整

修改jvm.options配置:

-Xms31g                              # 堆内存初始值
-Xmx31g                              # 堆内存最大值
-XX:+UseG1GC                         # 启用G1垃圾回收器
-XX:MaxGCPauseMillis=200             # 目标暂停时间
-XX:InitiatingHeapOccupancyPercent=35 # GC触发阈值

五、避坑指南与经验总结

5.1 必须绕过的三个大坑

  1. 避免使用通配符查询:"title": "手机"
  2. 深度分页问题:from+size超过10000时的性能悬崖
  3. 聚合查询不加限制:terms聚合的size默认只有10

5.2 持续监控的四个关键指标

# 通过API监控核心指标(技术栈:Elasticsearch+Prometheus)
curl -XGET "http://localhost:9200/_nodes/stats?filter_path=nodes.*.jvm,nodes.*.indices.search"

六、最终效果与扩展思考

优化后的性能对比:
| 指标 | 优化前 | 优化后 | |--------------|--------|--------| | 平均查询延迟 | 820ms | 68ms | | 吞吐量 | 120QPS | 850QPS | | CPU使用率 | 95% | 65% |

对于更复杂的场景,可以考虑:

  1. 引入Nginx缓存高频查询结果
  2. 使用Redis缓存过滤条件组合
  3. 对实时性要求不高的报表走异步计算