3099 字
约 10 分钟
20
S13-A11 · 搜索的降级与兜底:查不到时给什么

ES 挂了、搜索页白屏、导航栏的搜索框变成装饰——根因通常不在集群,而在搜索被当成不可失败的强依赖。降级要回答的不是"怎么让 ES 更稳",而是"它不稳时,这个页面还能给什么有用的东西"。


1. 四种失败,四套兜底

把"搜索坏了"拆开看,四种失败的处置完全不同:

失败类型 表现 兜底手段 用户感知
集群不可用 连接拒绝、全节点超时 缓存 / DB 模糊查 / 分类浏览 从"搜索"变"逛"
查询超时 部分请求超预算 截断结果 + 提示 / 走缓存 结果变少但可用
结果为空 正常返回 0 条 不一定降级,可能要改写查询 关键:可能是正确答案
结果质量差 有结果但排序离谱 换排序策略 / 规则干预 最隐蔽,最难兜

前两类是可用性问题,后两类是相关性问题。混成一个"降级开关"是最常见的错:可用性要立刻兜底,相关性要观察评估,触发条件与回滚代价都不同。


2. 降级的三层与体验落差

| 层级 | 手段 | 延迟量级 | 体验落差 | 适用 |
|---|---|---|---|
| L1 | 缓存结果(同 query 近期结果) | 毫秒 | 小,可能过期 | 热点查询、短暂抖动 |
| L2 | DB 模糊查询(LIKE/前缀) | 数十到数百毫秒 | 中:召回骤降、无分词 | 数据量小、字段少 |
| L3 | 分类/榜单浏览 | 毫秒 | 大:从"找"变"逛" | 兜底,页面不空 |

选哪层不该由代码里的 catch 决定,而由查询的业务价值决定:搜商品名,L2 的 LIKE 有意义;搜一句自然语言,LIKE 基本等于空结果,L3 更诚实。

L2 的隐藏成本:LIKE '%x%' 用不上索引,全表扫会把搜索故障放大成数据库故障。降级路径必须限流,否则兜底就是二次事故。


3. "空结果"要不要降级

这处最容易做错。空结果经常是正确答案——搜了不存在的型号、拼错词、查已下架条目。此时降级去"猜他想搜什么",会把噪声塞进相关性统计。

判断要不要处理空结果,看四个条件:

条件 判据 处置
拼错 有纠正候选且置信高 提示"是否想搜 X",不自动替换
同义未覆盖 词典有映射、索引没覆盖 修词典与索引,不是降级
范围过窄 过滤条件清空结果 提示"去掉某条件有 N 条"
确实无此内容 上述都不成立 保持空,给可浏览入口

判据:降级只该发生在"系统坏了",不该发生在"确实没有"。混在一起短期指标好看,长期是相关性数据被系统性污染。


4. 超时预算怎么分

搜索页有总预算(如用户能等的 800ms),要在"检索 + 聚合 + 渲染"之间分配,而不是每层各设一个超时:

long deadline = System.nanoTime() + budgetNanos;      // 例如 600ms
SearchResponse r = client.search(req, RequestOptions.DEFAULT.toBuilder()
    .setTimeout(TimeValue.timeValueMillis(300))       // 单次上限
    .build());
long left = deadline - System.nanoTime();
if (r.isTimedOut() && left > retryFloorNanos) { /* 剩余够才重试 */ }
预算项 典型占比 超支后果
单次查询 总预算一半左右 没余量做重试或兜底
重试 仅在剩余足够时 总延迟翻倍,用户先走了
兜底(DB/缓存) 预留固定额度 兜底比主链还慢,等于没兜
渲染与网络 固定开销 前面省的时间被序列化吃掉

重试必须有下限判据:剩余时间跑不完一次查询时,重试只把超时变成双倍超时。宁可立刻降级。


5. 熔断与慢查询保护

拖垮集群的很少是"流量大",而是少量慢查询占满队列:线程池被占住,正常查询排在后面,整体雪崩。

{
  "search.default_search_timeout": "1s",
  "search.max_concurrent_search": 200,
  "thread_pool.search.queue_size": 1000
}
机制 作用 误配的后果
请求级超时 到点返回部分结果 太长则慢查询堆积
并发上限 限制同时查询数 太松等于没保护
队列长度 排队容量 太长则延迟失控
慢查询日志 事后定位 阈值太高等于不开

保护的目标不是让所有查询成功,而是让快查询保持快:宁可拒绝一批,也不让队列一起变慢。限流按请求数,这里按执行时间。


6. 缓存结果的时效标注

缓存兜底最大的风险不是"旧",而是用户不知道它旧。命中缓存时响应必须带得出时效:

{
  "degraded": true,
  "degraded_reason": "search_cluster_unavailable",
  "result_cached_at": "2026-11-09T14:02:11Z",
  "stale_seconds": 1260
}
字段 为什么必须有
degraded + 原因 前端可提示、监控可聚合、事后可归因
缓存时间与新鲜度 判断"能否接受"的依据
提示文案 容忍度取决于用户知道它旧

没有这些字段,降级是不可观测的:监控上是 200,体验上是错答案,没人知道发生过。


7. 降级的观测:降级率与降级后点击率

指标 定义 用途
降级率 降级请求 / 总请求 兜底是否在被大量使用
分层分布 L1/L2/L3 占比 L3 高则兜底质量差
降级后点击率 降级请求中有点击的比例 核心:兜底有没有用
降级后零点击率 降级后无任何交互 兜底等于白屏
降级时长 单次持续时长 抖动还是持续故障

只看降级率会得出反向结论:降级率 100% 但点击正常说明兜底有效;降级率 5% 但零点击翻倍说明少量用户在受重伤。兜底价值要用行为指标衡量,不是可用性指标。

降级还会污染相关性分析,所以每条搜索日志都要带 degraded 标记并按它拆维度:

污染方式 表现 修正
CTR 下降 被归因为"排序变差" 按 degraded 拆维度
A/B 失真 实验组恰逢降级窗口 剔除降级样本
训练集污染 降级点击进了样本 打标,训练时排除
空结果率虚降 兜底把空结果填满 口径区分"有结果/兜底"

没有这个标记,故障周的相关性结论无法回溯剔除。


8. 开关:人工迟滞与自动防抖

方式 优点 缺点 适用
人工开关 判断准 夜里没人按,迟滞数十分钟 计划内维护
自动开关 秒级响应 抖动导致反复切换 集群不可用
半自动 兼顾 仍需人 多数生产场景

自动开关必须防抖,否则会在临界点来回切:

// 连续 N 次失败才开,连续 M 次成功且持续 T 才关
if (failStreak.incrementAndGet() >= OPEN_THRESHOLD) state = DEGRADED;
if (state == DEGRADED && okStreak.incrementAndGet() >= CLOSE_THRESHOLD
        && now - lastFailAt > QUIET_PERIOD) state = NORMAL;
参数 含义 误配后果
OPEN_THRESHOLD 连续失败几次降级 太小则抖动即降级
CLOSE_THRESHOLD 连续成功几次恢复 太小则恢复又被打回
QUIET_PERIOD 恢复前静默观察 太短则回切后再挂

防抖是用一点延迟换稳定。参数由"探测间隔 × 阈值"折算成响应时间,写进值班手册。


9. 多副本与多集群:切换不等于一致

拓扑 切换代价 一致性问题
同集群多副本 低(自动) 副本刷新延迟,读到略旧
多分片 + 副本 低 主分片不可用时短暂不可写
双集群(同城) 中,需切读路径 写入不同步则结果不一致
双集群(异地) 高 延迟差异让排序与召回都变

关键判据:切换后用户会不会觉得"换了一个搜索"。双集群写入不同步时,切换会让热门结果消失、长尾冒出来——用户看到的是相关性崩塌,不是"稍旧"。

切换前要答:两集群滞后多少、切后能否只读、恢复后怎么补数据。答不上来就别做双活,单集群 + 缓存兜底更可控。


10. 恢复后的回切:避免来回抖

回切比降级更危险,因为此时流量已经习惯兜底路径:

策略 做法 风险
全量回切 探测成功即 100% 集群未预热,二次雪崩
灰度回切 1%→10%→50%→100% 需按比例路由能力
双读比对 回切期双读并比对 成本翻倍,能提前发现差异
延迟回切 恢复后静默观察再切 兜底期延长,但最稳
curl -s "http://es:9200/_cluster/health?wait_for_status=yellow&timeout=5s"
curl -s -XPOST "http://es:9200/app/_search" -d '{"size":1,"query":{"match_all":{}}}'

只探活不探查询是常见错误:节点起来了、分片还在恢复、查询走空分片,回切后立刻又是一轮降级。


11. 排错:五种降级相关故障

现象 原因 处理
开关开了页面仍空 兜底也失败,或结构不兼容 兜底要独立演练
降级率 100% 无人发现 无降级指标,只盯 5xx 降级率纳入 SLI
反复开关抖动 阈值太低、无静默期 加连续判定与静默期
兜底比主链还慢 L2 全表扫描、未限流 限流 + 只允许前缀匹配
恢复后指标仍异常 降级样本混入相关性统计 按 degraded 拆分剔除

12. 边界:这些场景不值得投入兜底

场景 做法 理由
内部搜索、可接受白屏 直接报错 + 提示 用户能等
非核心路径(页脚搜索) 隐藏入口 兜底成本高于收益
数据量极小 客户端过滤 不需要搜索集群
精确查(订单号) 直接走数据库 搜索引擎不是唯一真相
兜底无法保证正确性 宁可不兜 错答案比没答案代价高

判据:兜底投入应与这条路径的业务权重成比例。主链搜索值得缓存 + 双集群 + 灰度回切;帮助中心的小搜索框,隐藏它比维护三层降级更合理。


13. 自测

  1. 可用性失败与相关性失败为什么要用不同的兜底策略?
  2. L2 退化查询最大的隐藏成本是什么?
  3. 什么情况下"空结果"不应该被降级填充?误判的长期代价是什么?
  4. 为什么重试必须带"剩余预算下限"这个判据?
  5. 熔断的保护目标为什么是"让快查询保持快",而不是"让所有查询成功"?
  6. 回切前只做端口探活会漏掉什么?
S13-A11 · 搜索的降级与兜底:查不到时给什么
https://www.clxhxhhr.top/posts/6215/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。