慢查询、容量与搜索降级:搜索不可用时业务怎样保底
搜索故障可能表现为查询越来越慢,也可能是接口迅速返回错误,或者 HTTP 成功却少了一部分分片结果。三种现象需要不同处理:排队问题要减轻并发,输入错误要修查询,缺失结果则要决定还能否用于当前业务。
商品搜索可以短暂展示符合条件的旧列表。库存扣减、权限判断、订单确认仍应访问各自的业务存储;搜索失效时,这些操作不应改用一份旧搜索结果继续作决定。
一次搜索的时间花在哪里
分开测量端到端与服务端耗时
应用延迟包括连接池等待、DNS/TCP/TLS、请求传输、服务端排队和执行、响应传输及 JSON 处理。OpenSearch 返回的 took 只覆盖它所报告的服务端搜索过程;客户端整体很慢而 took 较低时,应继续检查入口、网络和应用自身等待。
服务端还有查询与取回两类工作。query 阶段查倒排、过滤、评分、排序及聚合;fetch 阶段读取 source、处理高亮等结果。大 source、高亮很多字段,也可能让已经找到候选的请求迟迟无法结束。
常用观察项如下:
| 层次 | 观察内容 | 能帮助区分的问题 |
|---|---|---|
| 应用接口 | 请求数、P50/P95/P99、错误分类、超时、降级比例 | 用户实际受影响程度 |
| HTTP 客户端 | 连接池等待、在途数、传输异常、响应体大小 | 是否尚未进入搜索执行 |
| 搜索响应 | took、timed_out、_shards.failed、失败原因 | 超时、部分执行和查询错误 |
| 节点 | CPU、堆、GC、线程池 active/queue/rejected | 计算、内存或排队压力 |
| 索引与磁盘 | shard/segment 数、merge、存储量、剩余空间 | 分片碎片化、写入竞争、磁盘风险 |
| 数据投影 | 最旧未处理变更、失败项、可搜索延迟 | 搜得快却搜到旧状态 |
累计 rejected 和 GC 时间应转换为时间窗口内增量,节点重启后要处理计数器归零。平均延迟也会掩盖少数长请求,应结合尾延迟和超时数量查看。Nodes Stats API提供这些节点级统计。
健康颜色描述分片分配
green 表示主、副分片均按配置分配;yellow 表示有副本未分配;red 表示有主分片未分配。单节点、配置一个副本时通常会出现 yellow,因为副本不能与其主分片放在同一节点。Cluster Health API解释了这些状态。
健康颜色之外,查询仍可能因为字段错误、线程池饱和、磁盘延迟或很大的聚合而失败。一个索引的主分片缺失,也不意味着所有其他索引都无法工作;诊断时应定位业务实际访问的索引和分片。
慢日志与 Profile 各看一部分
请求级慢日志按整个搜索请求的服务端耗时触发;分片慢日志分别记录单分片的 query、fetch 等耗时。多分片查询里,一条慢分片日志可能只是整个请求的一部分。日志阈值和路径见 OpenSearch Logs。
Profile 可以说明哪个查询执行器或聚合器耗时明显,但会增加额外开销,也不完整包含网络、排队和所有协调等待。用于少量代表性请求,不宜对线上所有搜索长期开启。Profile API列出了它的覆盖范围。
记录慢请求时,保存脱敏的查询形状、目标索引、页大小、排序和时间范围即可满足很多诊断需要。身份证号、访问令牌、完整客户名称等不应直接进入公共日志。
容量与查询限制怎样配合
文档数只是容量的一部分
存储量受 source 大小、词项数量、doc values、nested 内部文档、复制份数和删除合并状态影响。两组数量相同的文档,索引体积与查询成本可能完全不同。
增加分片可以提高部分并行能力,也会增加每次请求触达的分片数、segment、文件句柄和协调工作。分片太小且很多时,维护开销会占据资源;分片太大则影响恢复和迁移。应依据真实数据分布、查询方式及故障恢复时间决定组织方式,而不是按文档条数套一个固定公式。
磁盘还要给 merge、快照、恢复和重建新索引留空间。磁盘水位触发时可能限制分片分配或写入;应先检查空间来源、增长趋势和节点分布。反复解除写入保护却不释放空间,会很快再次触发限制。相关设置见 Cluster settings。
写入也会影响搜索。过小批次、每条强制 refresh、并行回填与大量 merge,都会争用资源。Indexing speed提供了索引写入与刷新调整的适用条件。
把高放大请求挡在入口
商品列表可以限定页大小、最深浅分页位置和允许的排序字段;统计接口可以限制时间跨度、分组字段和桶层级;导出放到独立队列,采用 PIT 连续读取和单独并发额度。具体分页和聚合机制见Query、分页、PIT 与聚合。
通配符、正则、模糊扩展、脚本和高基数多层分桶尤其需要限制。对受控后台确有价值的查询,可以单独授权和限流;不要让普通请求直接提供任意 DSL、目标索引或脚本。
限制触发后,接口要返回可操作的信息,例如允许的最大时间范围或可用的异步导出入口。单纯返回空列表会让用户误以为“没有数据”。
预算、排队与取消
一次搜索应同时设置应用总预算和服务端搜索限制。连接池租借等待也算在总预算内;上游请求已经超时后,继续排队几十秒的搜索没有业务价值。
服务端 timeout 或取消是执行过程中的协作式限制,不能保证在精确毫秒边界停止所有工作。客户端停止等待同样不代表远端立即停工。超时后追加完整重试,会叠加原请求可能尚未结束的消耗。端到端预算可结合超时预算设计。
OpenSearch 的 search backpressure 会观察压力与高资源请求,再依据配置选择是否取消。monitor_only 只记录,enforced 才执行相应控制;默认值与具体参数要按运行版本确认。Search backpressure说明了模式、统计和取消规则。
入口限流、客户端并发限制和服务端压力保护各处于不同位置。它们需要相互配合,不能依靠一个很长的线程池队列吸收无限流量。
降级结果还能回答什么
为具体业务选择回退
| 接口 | 可接受的回退例子 | 需要保留的限制 |
|---|---|---|
| 商品关键词列表 | 同查询的短期旧列表,或明确标注的推荐列表 | 结果来源、新鲜度、租户与权限 |
| 精确商品详情 | 按业务 ID 查询已有索引支持的主存储 | 独立并发预算,当前可见性与字段权限 |
| 热门分类 | 预先计算的静态分类或缓存 | 说明更新时间,不冒充实时统计 |
| 全量报表、金额汇总 | 暂缓任务或明确失败 | 不静默采用部分分片或旧汇总 |
| 下单与库存操作 | 维持既定业务数据库校验 | 不用搜索缓存代替事务判断 |
如果数据库没有承接关键词搜索的合适索引,把失败请求统一转换成 LIKE '%keyword%' 全表扫描,可能把搜索故障扩散到主库。回源路径应在故障前就有查询计划、容量预算和访问限制。
推荐列表与关键词匹配结果的含义不同。可将接口响应标成 source=recommendation 并调整页面说明,不能保留“搜索结果”标题却悄悄换成任意热门商品。
缓存键要覆盖查询与访问范围
通常需要纳入缓存键的内容包括租户、权限集合或其版本、查询条件、排序、分页位置、区域语言及投影代际。只用关键词作键,会把租户 a 的结果返回给租户 b。
有效期也有两层:
- 索引数据本身距业务提交的滞后。
- 查询结果自缓存后经过的时间。
缓存 age_ms 只表示第二层。即使刚刚写入缓存,索引也可能已经落后很久。对时效性敏感的接口,还需报告或控制投影新鲜度。
缓存中存在记录也不代表当前用户仍然有权读取。权限撤销、商品下架和敏感状态变化,应触发失效或在返回前再次检查。高风险访问宁可明确失败,也不要为了维持成功率放宽权限。
只对合适的失败回退
可以对受控的连接故障、服务暂不可用、资源拒绝采用有限回退。格式错误、认证失败、权限错误和缺失索引通常需要修输入或配置,不能一律隐藏在成功缓存后面。
响应的部分失败还要单独检查。在允许 partial results 的查询中,HTTP 200 可能包含 _shards.failed > 0。此时统计只覆盖成功分片。allow_partial_search_results=false 可要求完整执行,但应用仍应检查 timed_out 和业务所需的完整性字段。参数见 Search API。
一个降级响应可以明确表达:
{
"degraded": true,
"source": "cache",
"age_ms": 120,
"hits": []
}这里的空数组只是响应结构示意。实际缓存内容必须来自同范围的成功查询;没有合格缓存时返回可识别的服务不可用,不能临时制造“成功但没有命中”的结果。
实测失败、恢复与排查命令
启动专用服务并读取资源状态
下载并解压OpenSearch 实验工程,进入 opensearch-lab。使用 Linux Bash、Docker/Compose、curl、jq;运行账号需有 Docker 权限。OpenSearch 3.8.0 限制 2 GiB、512 MiB 堆,端口仅绑定回环 19222。安全插件关闭,只用于本机隔离实验;正式部署配置见 Docker 安装说明。
set -euo pipefail
command -v docker curl jq
docker compose -p search22 up -d --wait --wait-timeout 180
SEARCH_URL=http://127.0.0.1:19222
curl -q --noproxy '*' --fail-with-body --silent --show-error \
"$SEARCH_URL/" | jq -e '.version.number == "3.8.0"'
curl -q --noproxy '*' --fail-with-body --silent --show-error \
"$SEARCH_URL/_cluster/health" \
| jq '{status,number_of_nodes,active_primary_shards,unassigned_shards}'
curl -q --noproxy '*' --fail-with-body --silent --show-error \
"$SEARCH_URL/_nodes/stats/jvm,thread_pool,fs,breaker" \
| jq '.nodes[] | {name,heap_percent:.jvm.mem.heap_used_percent,
gc:.jvm.gc.collectors,search:.thread_pool.search,
available_bytes:.fs.total.available_in_bytes,breakers:.breakers}'不同时间得到的堆占用、计数器和磁盘空间会变化,应保留自己的两次观测并计算差值,不与某个固定数字比较。Docker 容器的文件系统统计也可能反映宿主或虚拟磁盘共享空间,并非该容器独占的容量。
服务未就绪时先检查:
docker compose -p search22 ps
docker compose -p search22 logs --tail 100 opensearch
docker stats --no-stream search22-opensearch-1看到 OOM、端口冲突、bootstrap 检查失败或权限错误时,按具体原因处理。不要删除命名卷来掩盖启动失败。
制造真实 TCP 故障并验证缓存
工程的 FailureTest 启动一个只监听回环的测试应用入口,再经故障代理访问真实 OpenSearch。代理可在请求进入后关闭 TCP,正常模式则转发真实服务响应。
应用使用固定测试令牌 fixture-a、fixture-b 映射可信租户;这是隔离实验身份,不是生产认证方案。测试缓存只有少量文档,键由租户与关键词组成,索引目标在该实例中固定;生产实现还需有界容量、更多查询维度和权限变化处理。
执行过程如下:
- 令牌 a 查询 blue,从 OpenSearch 得到 p1 并保存成功结果。
- 关闭代理连接。同一租户、同一查询返回 p1,标记 degraded=true、source=cache。
- 租户 b 查询 blue,以及租户 a 查询 red,都没有匹配缓存,返回 503。
- 等待超过实验的 2 秒缓存有效期,原查询也返回 503。
- 恢复正常 TCP 转发,原查询重新返回 source=opensearch、degraded=false。
断连发生在 TCP 传输层,客户端得到连接异常;应用入口随后返回带来源标记的缓存 p1,或明确的 503。恢复转发后,命中内容重新来自 OpenSearch 查询。
缺失索引和部分分片不能被掩盖
第二项实验先建立成功缓存,再删除测试独立创建的目标索引。应用仍返回真实 index_not_found_exception 和 HTTP 404,而不是使用旧缓存隐藏配置故障。
第三项实验同时查询两个索引:一个 price 是 long,另一个 price 是 keyword。对两者执行 price >= "not-a-number",数值分片发生查询错误,字符串分片能完成比较:
partial allowed: HTTP 200, failed shards=1
strict: HTTP 400, query_shard_exception
repaired numeric query: one hit, actual profile returned随后仅对数值索引使用合法数值条件,返回 1 条记录和真实 Profile 数据。负例由真实 Mapping 不兼容触发,明确展示了“部分命中仍可返回 HTTP 200”的行为。
运行全部三项:
mkdir -p .m2
docker run --rm --memory 2g --entrypoint mvn \
--user "$(id -u):$(id -g)" --network search22_default \
-e MAVEN_CONFIG=/tmp/maven -e SEARCH_URL=http://opensearch:9200 \
-v "$PWD":/work -v "$PWD/.m2":/m2 -w /work \
maven:3.9.12-eclipse-temurin-17 \
-B -Duser.home=/tmp -Dmaven.repo.local=/m2 -Dtest=FailureTest clean verify预期 Tests run: 3, Failures: 0, Errors: 0。测试结束会关闭自己的入口和代理,删除自己创建的索引;Java 25 可以换用 maven:3.9.12-eclipse-temurin-25 复跑。执行失败时查看 target/surefire-reports 中是准备失败、真实状态不符合预期,还是清理失败,不把任意异常视为故障演练成功。
恢复流量时继续看数据与负载
服务恢复后,先用少量代表性查询验证实际索引、过滤条件和返回完整性,再逐步退出降级。保持短期成功率和尾延迟观察,避免一次放出全部缓存失效请求。
同一热点键可合并正在进行的回源,过期时间适当错开,后台刷新设置独立限额。回填和重试积压也应按预算恢复,别与用户查询一起无上限抢占资源。
端点恢复后,还要补齐故障期间未写入的商品变化。检查投影积压、失败队列和删除事件,必要时执行重建索引与增量对账。
本机实验结束可停止专用服务,命名卷保留:
docker compose -p search22 down权威资料与规范地址
运行状态与查询失败诊断
- 节点统计:https://docs.opensearch.org/latest/api-reference/nodes-apis/nodes-stats/
- 集群健康:https://docs.opensearch.org/latest/api-reference/cluster-api/cluster-health/
- 日志与慢日志:https://docs.opensearch.org/latest/install-and-configure/configuring-opensearch/logs/
- Profile:https://docs.opensearch.org/latest/api-reference/search-apis/profile/
- 搜索返回与失败参数:https://docs.opensearch.org/latest/api-reference/search-apis/search/
部署配置与负载控制
- 集群资源设置:https://docs.opensearch.org/latest/install-and-configure/configuring-opensearch/cluster-settings/
- 写入性能:https://docs.opensearch.org/latest/tuning-your-cluster/performance/
- 搜索反压:https://docs.opensearch.org/latest/tuning-your-cluster/availability-and-recovery/search-backpressure/
- Docker 安装:https://docs.opensearch.org/latest/install-and-configure/install-opensearch/docker/
