Query、分页、PIT 与聚合:查询语义和资源成本怎样权衡
为什么一个只返回二十条记录的深分页会拖慢整个集群? 这不是某个 DSL 参数没调好,而是数据身份、索引结构、分布式执行与资源治理没有被放进同一张设计图。查询成本取决于被触达分片数、每片候选数、评分与聚合结构,而不是最终响应条数。分页协议必须同时定义顺序、快照、一致性窗口和资源预算。
先守住搜索系统的三个事实
第二,可搜索不等于已提交。每个 shard 先收集局部 top-k,协调节点再全局归并。from/size 迫使每片保留 from+size 个候选;search_after 只携带上一页 sort values,PIT 固定索引视图并保留相关 segment。filter context 不计算相关度,适合权限与结构化约束。 因而需要分别观测“事实源已提交”“投影事件已消费”“索引已接收”“refresh 后可见”四个水位。只报一个写入成功率,不能证明用户已经能搜到新状态。
第三,分片不是免费的并行度。查询会在命中的 shard 上分别创建候选集合,再由协调节点归并。写入会按 routing 进入特定主分片并复制到副本。分片数增加能够提高某些并行能力,也会增加 fan-out、segment、文件句柄、cluster state 和恢复成本。
一条记录怎样跨过事实、投影和查询边界
在线列表限制 shallow page,深遍历采用 PIT+search_after;排序键必须包含唯一 tie-breaker。聚合接口设置 bucket、分片、超时和并发上限,导出任务与在线检索拆资源池。
非唯一排序会造成跨页重复或遗漏;PIT keep_alive 无上界会延迟 segment 回收;高基数 terms、过深嵌套 bucket 与脚本评分会把一次请求放大为 heap 和 CPU 事故;权限过滤遗漏则是数据泄露。
这类错误的危险之处在于,正常样本往往完全正确。只有在动态字段持续增长、事件乱序、深页访问、某个 shard 变慢或迁移追平时,隐藏的复杂度才突然显现。架构验证因此必须制造反向实验,而不是只跑 happy path。
运行链不是黑盒:从入口到 Segment
每个 shard 先收集局部 top-k,协调节点再全局归并。from/size 迫使每片保留 from+size 个候选;search_after 只携带上一页 sort values,PIT 固定索引视图并保留相关 segment。filter context 不计算相关度,适合权限与结构化约束。
[ Disk_{peak} ge Source imes IndexRatio imes (Primary+Replica) imes GenerationOverlap + MergeReserve ]
查询放大要在请求进入集群之前计算
查询成本不能用返回条数近似。一个请求的粗略成本向量至少包括:命中 shard 数、每片候选数、是否计算 score、排序字段、聚合 bucket 上限、脚本执行、highlight、_source 大小和并发度。对深分页,候选放大可近似为:
[ CandidateAmplification = rac{Shards imes (from + size)}{size} ]
这不是精确耗时公式,却能在 API 网关或查询服务中提前识别危险形状。对聚合还要加入 shard_size 与 bucket 层级;对模糊、前缀或脚本查询,要加入实际命中词项和执行次数。预算超限时应拒绝、降级或转异步,而不是把未知成本直接交给集群线程池。
权限过滤也是成本与正确性的共同约束。tenantId、organizationId、visibility 和 resource ACL 必须由服务端构造,不能信任客户端传入。权限字段要选择适合精确过滤的数据结构,并进入所有搜索、suggest、聚合、导出和缓存键。漏掉任何旁路,都会产生跨租户数据泄露。
两层失败必须分开处理
可运行模型一:把放大或局部失败变成数字
运行 DeepPaginationCostDemo.java:
javac --release 17 -Xlint:all -Werror DeepPaginationCostDemo.java
java DeepPaginationCostDemo预期输出:
shards=8 from=10000 size=20 candidatesPerShard=10020 totalCandidates=80160 returned=20 amplification=4008可运行模型二:证明乱序、重放或切换仍收敛
javac --release 17 -Xlint:all -Werror PitCursorDemo.java
java PitCursorDemo预期输出:
documents=6 pageSize=2 pages=3 uniqueSeen=6 duplicates=0 concurrentInsertVisible=false stable=true第二个模型故意制造反向条件:重复、乱序、并发插入、漏投影或引擎误路由。只要 invariant 依赖“事件总会顺序到达”或“迁移期间不会写入”,它就不是工程约束。可交付的机制必须在反向输入下拒绝旧版本、保持稳定视图、补齐差异或明确降级。
新鲜度不是一个平均延迟
观测必须能回答“哪个边界坏了”
应用侧记录 queryShape、触达索引、generation、shard 数、took、timed_out、失败 shard、命中数关系、返回字节和降级路径;写入侧记录 batchBytes、itemCount、success/retryable/permanent、重试年龄、死信量和版本拒绝量。不要记录完整查询文本、用户原文或敏感 _source,必要时对字段名与值分别白名单化。
安全、删除与审计不能交给查询约定
多租户隔离至少有索引级、routing 级与字段过滤级三种策略。索引级隔离清晰但 cluster state 成本高;共享索引节省资源但所有查询路径必须注入租户过滤;routing 能减少 fan-out,却可能形成大租户热点。选择依据是租户规模分布、合规边界、迁移成本与故障爆炸半径。
脚本查询、正则、通配符、runtime field 和高亮都属于受控能力。它们应按接口白名单开放,设置超时、复杂度和并发预算。让外部调用者提交任意 Query DSL,相当于把集群 CPU、heap 与数据访问边界一起交给客户端。
Schema 演进必须以 generation 为单位
回滚也不能只把 alias 指回旧索引。如果切换后新写入只进入新 generation,旧索引已经落后。可回滚方案要么保持旧代增量同步,要么能根据切换水位重放缺口。所谓“旧索引还在”不等于“旧索引可安全接流”。
架构决策记录应该留下什么
一份可复审的记录至少包含:主查询形状、数据事实源、文档身份、版本规则、字段职责矩阵、分片与 routing 依据、峰值容量测量、查询预算、失败分类、降级语义、迁移状态机、回滚窗口和数据删除路径。每个数字标明样本、压测方法或监控来源。
查询成本取决于被触达分片数、每片候选数、评分与聚合结构,而不是最终响应条数。分页协议必须同时定义顺序、快照、一致性窗口和资源预算。 对这个主题,最终必须守住的条件是:分页顺序稳定且查询放大有预算,新增文档不能改变既有 PIT 的遍历结果。如果团队无法通过运行模型、指标和核对账本证明它,就还没有完成架构设计。
查询限制覆盖 shard fan-out、深分页、bucket、脚本、超时、并发和响应体。所有查询、聚合、suggest、导出和缓存路径执行同一权限约束。指标能区分事实水位、消费水位、索引接收与 refresh 可见水位。
新旧索引以版本、缺失主键和摘要核对,不只比较文档数量。降级响应标明来源与新鲜度,不把派生结果冒充权威事实。旧 generation 保留到回滚窗口结束,删除与隐私擦除覆盖全部副本。
两个 Java 17 模型通过严格编译并产生与文中一致的输出。
以下资料用于核对 API 语义和引擎数据结构;架构结论仍需结合实际版本、数据分布与压测结果验证:
Elasticsearch Paginate search results。Elasticsearch Point in time
