Administrator
发布于 2022-08-25 / 1676 阅读
19

Elasticsearch 查询性能优化实战

一个 ES 查询,把节点 CPU 打到了 95%

运营后台有个"订单检索"接口,平时挺快,某天加了"按创建时间排序 + 关键词模糊"后,单查询 CPU 占用飙升,集群一个节点到了 95%,其他查询全被拖慢。我用 profile API 抓了执行计划,发现罪魁是把本该放 filter 的条件写进了 query,还用了一段脚本排序。这次把 ES 查询优化的几个要点理了一遍。

filter 与 query:要不要算分,差别巨大

ES 里 query 上下文会计算相关性得分(_score),而 filter 上下文只判断"是否命中",不算分。更关键的是:filter 的结果会被缓存成 bitset,相同 filter 再次执行直接复用,几乎零成本。

我们那个检索接口,把"状态=已支付""渠道=APP"这种精确等值条件写在了 query 里:

# 改造前:等值条件放在 query,既算分又没享受到缓存
GET /orders/_search
{
  "query": {
    "bool": {
      "must": [
        { "match": { "status": "PAID" } },
        { "match": { "channel": "APP" } },
        { "match": { "buyer": "张" } }
      ]
    }
  }
}

# 改造后:等值条件移到 filter,算分只留给真正需要相关的关键词
GET /orders/_search
{
  "query": {
    "bool": {
      "must":   [ { "match": { "buyer": "张" } } ],
      "filter": [
        { "term": { "status": "PAID" } },
        { "term": { "channel": "APP" } }
      ]
    }
  }
}

改完用 profile 看,filter 阶段的耗时从平均 38 ms 降到 2 ms(bitset 命中缓存),整体查询 P95 从 410 ms 降到 120 ms。

doc_values:排序和聚合的底层支撑

ES 对text字段默认建倒排索引用于搜索,但排序、聚合、脚本访问需要的是"某文档的某个字段值",这靠 doc_values(磁盘上的列式存储)提供。对需要排序/聚合的数值、keyword、日期字段,要确保 doc_values 开启(默认开);而 text 字段默认没有 doc_values,不能用于排序(要用 keyword 子字段)。

PUT /orders
{
  "mappings": {
    "properties": {
      "status":  { "type": "keyword" },        // 有 doc_values,可聚合
      "amount":  { "type": "scaled_float", "scaling_factor": 100 },
      "buyer":   { "type": "text",
                   "fields": { "kw": { "type": "keyword" } } }  // 排序用 buyer.kw
    }
  }
}

深分页与脚本:两个性能黑洞

运营喜欢"翻到第 500 页"。from + size 的深分页在 ES 里很贵:要取到第 500 页,协调节点得从各分片拉回前 500×size 条再丢弃,越深越慢,且默认有 10000 上限。改用 search_after 基于上一页最后一条的排序值游标翻页:

GET /orders/_search
{
  "size": 20,
  "sort": [ { "create_time": "desc" }, { "_id": "asc" } ],
  "search_after": ["2022-08-20T10:00:00", "ord_9981"]
}

脚本(script 字段)则是另一个黑洞——它绕过索引、逐文档执行,之前那个"按折扣后金额排序"就用了 painless 脚本,CPU 直接爆。改成写入时冗余一个 final_amount 字段,排序走字段而非脚本,CPU 占用掉了 70%。

下篇预告

这篇先把《Elasticsearch 查询性能优化实战》里的坑列了,下一篇写我们当时是怎么在线上工程里真正落地的——包括那次让领导拍桌的故障复盘。

参考