慢查询、容量与搜索降级:搜索不可用时业务怎样保底
搜索集群过载时,为什么超时和重试常常让故障更严重? 这不是某个 DSL 参数没调好,而是数据身份、索引结构、分布式执行与资源治理没有被放进同一张设计图。搜索是派生能力,降级首先要保护事实权威与核心写路径。可靠性设计要在入口拒绝高放大查询,并明确缓存、数据库和静态结果各自能回答什么。
先守住搜索系统的三个事实
第一,业务事实、搜索投影与查询结果是三个不同层次。订单、商品或日志在事实库中拥有业务主键和权威版本;搜索文档只是某一代 schema 下的派生表示;hit、score、sort values 和 bucket 又只是一次查询在特定索引视图上的计算结果。把三者混为一谈,删除索引会变成数据灾难,搜索命中也会被误解为强一致事实。
第二,可搜索不等于已提交。一次查询的成本可近似分解为 fan-out shard、候选文档、评分、bucket、脚本与响应体。线程池队列、heap、GC、segment 数、磁盘水位和查询延迟共同形成容量信号,单一 CPU 指标无法解释过载。 因而需要分别观测“事实源已提交”“投影事件已消费”“索引已接收”“refresh 后可见”四个水位。只报一个写入成功率,不能证明用户已经能搜到新状态。
一条记录怎样跨过事实、投影和查询边界
入口先按查询形状估算预算,昂贵请求转异步或拒绝;分层降级采用短时缓存、受限精确查询、静态热门结果,并显式返回 degraded 与 dataFreshness。恢复采用逐级放量,避免瞬时回灌。
客户端超时不等于服务端停止执行;无预算重试会继续消耗 shard;缓存旧结果若不标记新鲜度会冒充权威状态;把数据库 LIKE 当通用回退可能把搜索事故扩散成主库事故。
这类错误的危险之处在于,正常样本往往完全正确。只有在动态字段持续增长、事件乱序、深页访问、某个 shard 变慢或迁移追平时,隐藏的复杂度才突然显现。架构验证因此必须制造反向实验,而不是只跑 happy path。
运行链不是黑盒:从入口到 Segment
一次查询的成本可近似分解为 fan-out shard、候选文档、评分、bucket、脚本与响应体。线程池队列、heap、GC、segment 数、磁盘水位和查询延迟共同形成容量信号,单一 CPU 指标无法解释过载。
segment 一旦生成基本不可变,删除通常先写 tombstone,合并时才真正回收空间。更新在底层接近“删除旧文档并新增文档”。这使更新频率、文档大小、refresh_interval 和 merge 压力互相关联:更短的 refresh 提升新鲜度,却制造更多小 segment;高频大文档更新会放大 IO、合并和网络复制;副本数提高读取与容灾能力,也同步增加写放大。
[ Disk_{peak} ge Source imes IndexRatio imes (Primary+Replica) imes GenerationOverlap + MergeReserve ]
查询放大要在请求进入集群之前计算
[ CandidateAmplification = rac{Shards imes (from + size)}{size} ]
两层失败必须分开处理
重试策略必须携带原操作身份,不得重新生成业务版本。可重试错误采用有限次数、指数退避、随机抖动和总截止时间;永久错误进入隔离队列并带上脱敏后的原因、目标 generation 与 owner。重试队列也要有容量,超过容量时入口背压或落持久账本,不能无限堆在 JVM heap。
可运行模型一:把放大或局部失败变成数字
javac --release 17 -Xlint:all -Werror QueryBudgetDemo.java
java QueryBudgetDemo预期输出:
shards=24 candidatesPerShard=500 buckets=2000 estimatedUnits=14000 budget=10000 admitted=false reason=QUERY_BUDGET可运行模型二:证明乱序、重放或切换仍收敛
javac --release 17 -Xlint:all -Werror SearchFallbackDemo.java
java SearchFallbackDemo预期输出:
search=UNAVAILABLE cacheAgeSeconds=45 maxStaleSeconds=60 fallback=CACHE responseDegraded=true authorityPreserved=true第二个模型故意制造反向条件:重复、乱序、并发插入、漏投影或引擎误路由。只要 invariant 依赖“事件总会顺序到达”或“迁移期间不会写入”,它就不是工程约束。可交付的机制必须在反向输入下拒绝旧版本、保持稳定视图、补齐差异或明确降级。
新鲜度不是一个平均延迟
搜索新鲜度应以端到端 lag 描述:事实版本产生时间、事件可消费时间、投影器处理时间、索引接收时间和 refresh 可见时间。建议同时记录 event_lag、projection_lag、refresh_visibility_lag 与 oldest_unprojected_age。P50 很好看而 oldest age 持续增加,通常意味着少量 poison event、热点 key 或某个分区已经卡死。
对读接口,响应可携带 indexGeneration、projectionWatermark、dataFreshness 或 degraded 标志。它们不是给终端用户展示内部细节,而是让上层业务知道当前结果能否用于金额、权限、库存等权威判断。搜索适合发现候选,关键决策应按 businessId 回到事实服务确认。
观测必须能回答“哪个边界坏了”
集群侧把 heap、GC、CPU、磁盘水位、segment 数、merge、refresh、search/write 线程池 active/queue/rejected、fielddata/request breaker 和恢复流量放在同一时间轴。单个 rejected 指标不能说明根因:它可能来自深分页、聚合爆炸、热点 routing、segment 过碎或磁盘合并受阻。
安全、删除与审计不能交给查询约定
脚本查询、正则、通配符、runtime field 和高亮都属于受控能力。它们应按接口白名单开放,设置超时、复杂度和并发预算。让外部调用者提交任意 Query DSL,相当于把集群 CPU、heap 与数据访问边界一起交给客户端。
Schema 演进必须以 generation 为单位
核对不能只比 count。至少按业务分桶比较最大 sourceVersion、缺失主键、额外主键、关键字段摘要和删除 tombstone;热点或高价值对象进行逐项核对。切换后继续双读抽样,并监控零结果率、结果交集、排序差异、查询延迟和错误率,直到观察窗口结束。
回滚也不能只把 alias 指回旧索引。如果切换后新写入只进入新 generation,旧索引已经落后。可回滚方案要么保持旧代增量同步,要么能根据切换水位重放缺口。所谓“旧索引还在”不等于“旧索引可安全接流”。
架构决策记录应该留下什么
一份可复审的记录至少包含:主查询形状、数据事实源、文档身份、版本规则、字段职责矩阵、分片与 routing 依据、峰值容量测量、查询预算、失败分类、降级语义、迁移状态机、回滚窗口和数据删除路径。每个数字标明样本、压测方法或监控来源。
搜索是派生能力,降级首先要保护事实权威与核心写路径。可靠性设计要在入口拒绝高放大查询,并明确缓存、数据库和静态结果各自能回答什么。 对这个主题,最终必须守住的条件是:任何降级结果都标明来源和新鲜度,搜索故障不能拖垮事实库或覆盖权威状态。如果团队无法通过运行模型、指标和核对账本证明它,就还没有完成架构设计。
查询限制覆盖 shard fan-out、深分页、bucket、脚本、超时、并发和响应体。所有查询、聚合、suggest、导出和缓存路径执行同一权限约束。指标能区分事实水位、消费水位、索引接收与 refresh 可见水位。
新旧索引以版本、缺失主键和摘要核对,不只比较文档数量。降级响应标明来源与新鲜度,不把派生结果冒充权威事实。旧 generation 保留到回滚窗口结束,删除与隐私擦除覆盖全部副本。
两个 Java 17 模型通过严格编译并产生与文中一致的输出。
以下资料用于核对 API 语义和引擎数据结构;架构结论仍需结合实际版本、数据分布与压测结果验证:
Elasticsearch Search shard routing。Elasticsearch Task management
