P99 飙了十五倍
搜索产品上线两周,监控突然报警:搜索接口 P99 从 200ms 飙到 3 秒。老王第一反应是加机器,被同事拦住:先定位再扩容,不然新机器只是帮凶。查询调优有个固定流程:慢查询日志抓现场,profile 拆手术,改写手法动刀。逐步走。
第一步:打开慢查询日志
先让慢查询自己现形,索引级配置两行:
PUT /product/_settings
{
"index.search.slowlog.threshold.query.warn": "1s",
"index.search.slowlog.threshold.fetch.warn": "200ms"
}query 阶段超 1 秒、fetch 阶段超 200ms 的查询都会进日志,带完整的查询体和触发时间。这和 MySQL 系列里 long_query_time 的思路一模一样:先抓样本再诊断。老王抓到日志一看,慢的都是同一个模式的查询——运营新上的「按商品编码模糊搜索」。
第二步:profile 拆开看
把嫌疑查询放进 profile API,ES 会返回每个分片上每个查询组件的耗时树:
GET /product/_search
{
"profile": true,
"query": {
"wildcard": { "code.keyword": "*A2024*" }
}
}
返回树里能直接看到:
wildcard 耗时占九成,遍历了全部词条profile 只建议诊断时开,别挂在线上——它本身有开销。顺着耗时树走,凶手是谁一目了然。
五类高频凶手
| 凶手 | 案发现场 | 改法 |
|---|---|---|
| 通配符前缀查询 | wildcard 星号打头 | ngram 分词或改前缀匹配 |
| 深分页 | from 过百 | search_after 或限页(第 10 篇) |
| 巨型聚合 | terms size 上千 | 砍 size 或分层聚合 |
| 脚本排序 | script_score 满天飞 | 写进字段或 function_score |
| 大 _source 返回 | 详情几千字整包回传 | _source 裁剪只取列表字段 |
第一位要重点点名:星号开头的通配符查询是 ES 版的 LIKE 百分号全表扫,倒排对它完全失效,只能遍历词典。第 1 篇赶走的 LIKE,换个马甲又回来了。模糊搜索的正确解法是 ngram 分词:把「A2024」在索引时切成 A2、20、02、24 等短片段进倒排,查询照走倒排,代价是索引膨胀,按需给指定字段开。
改写手法:rescore 两段式
排序越精细越贵。rescore 提供「先粗排后精排」的两段式:第一段用便宜的 match 快速召回前一千条,第二段只对这一小撮跑昂贵的打分逻辑(短语匹配、脚本、业务规则),精确度上去了,成本只花在刀刃上:
"query": { "match": { "name": "燕麦拿铁" } },
"rescore": {
"window_size": 100,
"query": {
"rescore_query": { "match_phrase": { "name": "燕麦拿铁" } }
}
}顺手再记两个小旋钮:terminate_after 命中即止,适合只问「有没有」的校验类查询;_source 裁剪只返回列表页需要的字段,网络和序列化开销立减一半以上。另外,把纯筛选条件都塞进 filter 上下文(第 7 篇),缓存的复用也是调优的一部分——很多时候最快的查询是根本不用重算的查询。
小结
调优三步走:慢日志抓现场,profile 定凶手,改写手法动刀子。五类凶手里通配符前缀最猖狂,它是 LIKE 的转世灵童;rescore 是精排贵的特效药。扩容是最后的手段,不是第一反应。
调优手法齐了,老王把 20 篇的知识按场景重新排列——电商搜索、日志平台、数据看板三套配方。下一篇讲场景集:把零件组装成三个可上线的方案。
评论 (0)