面试考察点
- 是否理解文档路由到主分片再复制的写入路径。
- 能否描述 Query Phase 和 Fetch Phase。
- 是否知道协调节点会承担扇出、归并和内存压力。
核心答案
写入请求由协调节点根据 routing 计算目标主分片,转发给主分片校验并执行,再并行复制到副本,最后按确认条件响应。搜索则由协调节点向相关分片发起 scatter,分片返回候选文档及排序信息,协调节点全局归并后再 fetch 所需文档内容。
自定义 routing 可减少查询扇出,但路由选择不均会制造热点,且所有读写必须遵守同一规则。
写入细节
文档先进入内存缓冲并记录 translog,refresh 后生成可搜索的 Lucene segment;flush、merge 和 translog 共同影响可见性、恢复与磁盘成本。Bulk 能显著减少请求开销,但批次要有界。
单文档写入路径
协调节点解析 index/id/routing
↓
目标主分片写 translog + 内存 buffer
↓
主分片分配 seq_no,并复制给副本
↓
按 refresh 周期打开新 segment
↓
客户端得到成功响应(具体确认受 durability/replica 等配置影响)
主分片决定操作顺序,副本按顺序应用;更新文档并非原地修改 Lucene segment,而是写入新版本并标记旧版本删除,后台 merge 才会回收空间。高频 update 和 delete 会制造删除标记与 merge 压力,批量重建或 append-only 设计有时更适合。
Bulk 的正确边界
Bulk 减少 HTTP 请求与协调开销,但单批太大时会占用协调节点内存、增加失败重试粒度和长尾。按字节、文档数和耗时设置批次,逐条检查响应中的失败项;HTTP 200 不代表每个 action 都成功。
发生 429、连接超时或节点负载高时用指数退避和有限重试,避免所有客户端同步重试形成风暴。写入顺序敏感时,重试和并发 bulk 还要携带版本或业务幂等键。
搜索两阶段
Query Phase 中协调节点向目标分片发起查询,各分片只返回局部 Top N 的 doc ID、score 和排序值;协调节点归并得到全局候选。Fetch Phase 再向持有文档的分片读取 _source 和 stored fields。from + size 越大,每个分片需保留的候选越多,协调节点内存和 CPU 都会放大。
聚合也通常先在分片做局部 bucket,再由协调节点合并。高基数 terms、全局排序、script 和跨大量分片的查询会把协调节点变成瓶颈。
Routing 如何影响性能
自定义 routing 可让同一租户的数据落到固定分片,租户查询只需扇出一个或少数分片;代价是路由不均、漏写 routing 导致查不到数据,以及超级租户热点。路由是索引级契约,写入和读取必须一致,迁移和 reindex 时也要保留。
近实时与实时 GET
写入成功后按 ID GET 通常可从 translog/主分片读取到最新值,但普通 search 要等 refresh 才建立可搜索 segment。refresh=wait_for 会等待下一次刷新,refresh=true 强制刷新;两者都增加资源和延迟,不应对每条写入默认开启。
性能诊断顺序
先看协调节点耗时与分片扇出,再看分片 query/fetch、segment 数、refresh/merge、磁盘、heap 和 GC。写入慢还要检查 mapping 动态扩张、analyzer、bulk 批次和副本复制;搜索慢要拆分查询解析、候选归并、fetch 大 _source 和客户端网络。
查询细节
Query Phase 每个分片产生局部 Top N,协调节点归并全局结果;Fetch Phase 再向持有目标文档的分片取 _source。深分页会让每个分片保留更多候选,成本随 from + size 放大。
常见误区
副本分片可以服务查询,但主分片和副本不是固定只写或只读角色。协调节点不是免费代理,大结果集、高扇出和复杂聚合可能让其堆内存成为瓶颈。
高频追问与参考回答
追问:写入成功后为什么立即搜索不到?
普通搜索依赖 refresh 后的新 segment,Elasticsearch 是近实时搜索;按 ID 的实时读取和搜索可见性机制不同。
追问:协调节点一定要单独部署吗?
小集群可混合角色;大集群或查询扇出、聚合明显时可增加专用协调节点,但它仍需要足够 heap、网络和监控,不能把瓶颈简单转移。
追问:更新文档为什么会增加磁盘?
Lucene segment 近似不可变,更新会写新版本并标记旧版本删除,后台 merge 才回收空间。高更新率需要观察 merge 和磁盘临时空间。
追问:Bulk 返回 200 但业务仍丢数据吗?
Bulk HTTP 请求成功只代表整体请求被接收,响应 items 中每个 action 可能独立失败。必须逐项检查、记录失败原因并按幂等策略重试。
总结
写入围绕路由、主副本和可见性,搜索围绕分片扇出、候选归并和按需取文档,性能问题也应沿路径定位。
机制全景图
下面把「Elasticsearch 写入和搜索流程是什么?」从输入到结果压缩成一条可复述的主链路。面试时先用图建立全局坐标,再进入局部实现,能避免只背零散结论。
flowchart LR
A["协调节点解析请求"]
A --> B["路由到主分片并复制"]
B --> C["refresh 后建立 Searcher"]
C --> D["query phase 收集候选"]
D --> E["fetch phase 拉取文档并合并"]
完整链路:从输入到结果
沿着「协调节点解析请求 → 路由到主分片并复制 → refresh 后建立 Searcher → query phase 收集候选 → fetch phase 拉取文档并合并」观察输入、状态与输出,下面每个阶段都对应一个可以在源码、日志或系统表中验证的位置。
1. 协调节点解析请求
任意节点可做协调节点,写请求依据 routing 定位一个主分片,搜索则可能扇出多个分片。
2. 路由到主分片并复制
主分片执行写入并复制,wait_for_active_shards 只约束活跃副本数量,不是跨集群事务。
3. refresh 后建立 Searcher
refresh 切换可搜索 segment,旧 Searcher 仍服务进行中查询,形成 point-in-time 视图。
4. query phase 收集候选
Query phase 每分片返回本地 top N 的 doc ID 与分数,协调节点合并全局候选。
5. fetch phase 拉取文档并合并
Fetch phase 再向命中分片拉取 _source/fields;深分页会让每片都保留大量候选。
源码与实现定位
| 入口 | 阅读重点 |
|---|---|
| TransportShardBulkAction | 主分片写复制 |
| QueryPhase/FetchPhase | 候选合并与取文档 |
源码或系统表应按上表顺序追踪:先确认入口实际走到哪条路径,再用运行时数据验证,而不是仅凭类名或配置推测。
参数配置与可复现实验
GET orders/_search
{"size":50,"_source":["id","status"],"query":{"term":{"tenant_id":"7"}}}
逐级增加 size/分片/_source 字段,记录 query/fetch 和协调内存。
验证步骤与预期结果
1. 固定输入和基线
先在没有故障注入的环境执行上述配置,固定数据规模、并发度、运行时版本和预热时间。以「协调节点内存」为主基线,记录值应满足「记录稳态基线」;同时保存 query/fetch phase 时间、协调节点内存,使后续变化能够回到同一时间轴比较。
2. 从实现入口确认路径
在「TransportShardBulkAction」确认请求确实进入「主分片写复制」对应的实现,再沿「QueryPhase/FetchPhase」观察「候选合并与取文档」。如果入口路径都未命中,就不应继续调整下游参数,而应先检查调用条件、版本或路由是否与假设一致。
3. 注入本文特有的失败模式
优先复现「忽略协调节点资源」,并把单一变量逐级放大,直到「协调节点内存」越过「超过基线2倍」。随后再分别验证「写成功后立即 search 却没处理 refresh」和「fetch 返回完整 _source 放大网络」,三类故障分开执行,避免多个变量同时变化而无法归因。
4. 执行止损和根因修复
第一轮只应用「限制 size 与返回字段」,确认它能控制影响范围;第二轮应用「减少无关分片」,验证核心链路恢复;最后落实「深读用 PIT+search_after」,消除同类问题再次出现的条件。每一步都保留变更前后数据,不用“感觉变快了”替代测量。
5. 通过退出条件
实验只有同时满足三项才算通过:「协调节点内存」回到「记录稳态基线」、「P99」回到「小于业务预算」、「结果差异」回到「0」,并且业务结果差异为零。若性能恢复但结果不一致,仍应视为失败;若指标恢复后很快再次越线,则说明只完成了临时止损,没有消除根因。
量化基线
| 指标 | 样例基线/口径 | 风险线 | 结论 |
|---|---|---|---|
| 协调节点内存 | 记录稳态基线 | 超过基线2倍 | 按实现入口定位 |
| P99 | 小于业务预算 | 突破预算 | 停止扩量 |
| 结果差异 | 0 | 任意非零 | 回滚并重建 |
这些数值是实验口径或示例告警线,不是可复制到所有系统的固定答案;上线阈值应由本系统稳态、峰值和故障演练共同确定。
事故复盘:大 size 查询让协调节点 OOM
请求 size=100000,20 个分片各返回大量候选,协调节点建立巨大优先队列并 fetch 大量 _source。改用 search_after + PIT 流式翻页并限制最大窗口后消除峰值。
| 失败模式 | 首要证据 | 第一处置动作 |
|---|---|---|
| 忽略协调节点资源 | query/fetch phase 时间 | 限制 size 与返回字段 |
| 写成功后立即 search 却没处理 refresh | 协调节点内存 | 减少无关分片 |
| fetch 返回完整 _source 放大网络 | 分片扇出 | 深读用 PIT+search_after |
发布与回滚检查点
- 发布前:确认「TransportShardBulkAction」对应实现和上述配置在目标版本仍然有效,并保存「协调节点内存」基线。
- 灰度中:同时观察 query/fetch phase 时间、协调节点内存、分片扇出;任一指标越过表中风险线,就停止继续扩量。
- 回滚时:先执行「限制 size 与返回字段」控制影响,再回退代码或参数;涉及持久状态时必须额外核对结果差异。
- 发布后:至少覆盖一个完整峰值周期,确认「忽略协调节点资源」没有再次出现,才关闭变更观察窗口。
方案对比与选型
| 方案 | 更适合的场景 | 主要收益 | 代价与边界 |
|---|---|---|---|
| 普通 from/size | 浅页交互搜索 | 简单、支持跳页 | 深页协调内存线性增长 |
| search_after + PIT | 稳定深度遍历 | 每页成本稳定、视图一致 | 需保存排序游标,不能随机跳页 |
| scroll | 离线批量读取旧接口 | 快照式批量拉取 | 占用搜索上下文,不适合用户分页 |
选型至少带上 文档规模、分片数、字段基数、查询并发和写入速率,并用上面的量化基线验证;未知数据应明确为待测假设。
设计边界与工程取舍
搜索成本不仅在匹配文档,还在每分片候选合并和 fetch;限制 size、返回字段与分片数通常比单纯扩节点有效。
工程落地遵循:先设计 Mapping 与分片,再优化查询;任何调优都要控制扫描与内存放大。回答时直接引用「TransportShardBulkAction」、配置实验和事故数据,比复述固定模板更有说服力。