ElasticSearch(二):写入流程、近实时与段管理
ElasticSearch(二):写入流程、近实时与段管理
导语:ES 的写入链路是面试的必考重头戏。本篇讲清「一条文档从客户端到可被搜索」的完整路径,理清 buffer / translog / segment / filesystem cache 四者的关系与 refresh、flush、merge 的分工,并修正「写一致性」「副本同步」这两处网上流传最广的错误说法,共 14 题。
一、写入流程与近实时
1. 为什么说 ES 是「近实时(NRT)」搜索?
答: 因为文档写入后默认要等约 1 秒(index.refresh_interval)才能被搜到。
根本原因:新写入的文档先进内存索引缓冲区(in-memory buffer),此时还没有形成可被检索的数据结构(倒排索引必须建立在 segment 里)。只有执行 refresh 时,缓冲区中的数据才会被生成一个新的 segment,此时文档才开始对外可见。
写入 → in-memory buffer(不可搜索)
↓ refresh(默认每 1s)
→ 新 segment(在 filesystem cache 中,【可搜索】)
↓ flush(translog 达阈值/定时)
→ segment fsync 落盘(持久化)围绕这 1 秒的实操要点:
| 场景 | 做法 |
|---|---|
| 要求「写入后立刻能查到」 | 写入时带 refresh=wait_for(等到下次 refresh 完成再返回,不额外触发 refresh)或 refresh=true(立即触发一次 refresh,代价高) |
| 批量导入(追求吞吐) | index.refresh_interval: -1(不自动 refresh)或设为 30s,导入完成后再手动 _refresh |
| 手工触发 | POST /index/_refresh |
| 只读历史索引 | refresh_interval: -1 彻底关闭,减少无意义的段生成 |
易错点:「近实时」的瓶颈是 refresh,不是 flush。flush 只负责落盘持久化,不改变可见性——所以「ES 要等落盘才能查到」是错的。
2. 详细描述一条文档的写入流程
答: 分同步路径(客户端等待的部分)和异步路径(后台完成的部分)。
同步路径:客户端 → 主分片 → 副本 → 返回
① 客户端请求任意节点 -> 该节点成为【协调节点】
② 协调节点按 shard = hash(_routing) % number_of_shards 定位【主分片】,转发请求
③ 主分片:
· 做写入前校验(含 wait_for_active_shards 门槛检查,见第 9 题)
· 文档写入【In-memory Buffer】(此时不可搜索)
· 同时追加到【Translog】(默认每个请求都 fsync,见第 3 题)
· 把请求【转发给所有活跃副本分片】
④ 副本分片:同样写 Buffer + Translog,成功后【ACK 主分片】
⑤ 主分片收到【所有活跃副本的 ACK 后】,才向协调节点返回成功
⑥ 协调节点返回客户端(此时数据已持久化,且副本也已写入)异步路径(后台线程完成,不影响写入响应):
⑦ refresh(默认每秒):Buffer 内容生成【新 segment】写入 filesystem cache
-> 文档【变为可搜索】
⑧ flush(translog 达阈值或定时):segment 真正 fsync 落盘,并清空 translog
⑨ merge(后台):小 segment 合并为大 segment,物理清理已删除文档三处必须纠正的常见错误(原文即踩了坑):
| ❌ 常见错答 | ✅ 正确 |
|---|---|
「ES 写请求有 replication=async/sync 参数控制」 | 那是 ES 1.x 的老参数,早已移除。现代 ES 的写门槛由 wait_for_active_shards 控制 |
| 「先 refresh 再同步副本」 | 顺序反了。副本同步发生在主分片写 Buffer 之后、refresh 之前,是写入同步路径的一部分 |
| 「协调节点收到主分片成功就返回」 | 主分片是收到所有活跃副本 ACK 之后才返回成功的,因此返回时副本已写入 |
还有一个常被忽略的细节:写请求的「成功」不代表"可搜索"——返回成功后文档已经持久化(translog 已 fsync),但要等 refresh 才能被查询到,这正是 NRT 的语义。
3. Translog 的作用是什么?ES 怎么保证数据不丢?
答: Translog(事务日志)是 Lucene 之上的「写前日志(WAL)」,用来弥补「segment 落盘太慢」这个缺口。
为什么需要它:refresh 后的数据还在 filesystem cache 里(没落盘)。如果此时进程崩溃或机器断电,这部分数据就丢了。Translog 在写入时同步记录每条操作,恢复时重放即可补齐。
恢复流程(面试常问):
节点启动
→ 从【最后一次成功 flush 的已提交段】恢复基础数据
→ 重放【translog】中记录的所有变更(补齐未提交部分)
→ 数据恢复到崩溃前的状态关键参数(决定"能丢多少数据"):
| 参数 | 默认值 | 含义 |
|---|---|---|
index.translog.durability | request | 每个写请求都 fsync translog 后才返回 → 默认不丢数据 |
index.translog.durability | async | 按 sync_interval 批量 fsync,性能更好,但宕机最多丢 sync_interval 的数据 |
index.translog.sync_interval | 5s | async 模式下的 fsync 间隔 |
index.translog.flush_threshold_size | 512mb | translog 超过该大小触发 flush(生成新 translog generation 并清空旧的) |
高频结论:ES 默认是"不丢数据"的(
durability: request),代价是每次写都要 fsync。很多人以为"ES 会丢数据",其实那是把 durability 改成async或者把副本数设为 0 之后的场景。性能与持久性的取舍开关就是durability。
4. refresh、flush、merge 三者有什么区别?
答: 这是 ES 最经典的一组混淆概念,用一张表说清:
| 维度 | refresh | flush | merge |
|---|---|---|---|
| 做什么 | Buffer → 新 segment 写入 filesystem cache | 把 filesystem cache 中的 segment fsync 落盘,并清空 translog | 把多个小 segment 合并成大 segment |
| 目的 | 让数据可被搜索(可见性) | 持久化(防进程崩溃丢数据) | 控制段数量、物理删除已标记删除的文档 |
| 是否落盘 | 否 | 是 | 是(新段落盘后删除旧段) |
| 触发方式 | 默认每 1s;_refresh API;写入时 refresh=true/wait_for | translog 超过 flush_threshold_size(默认 512MB)、定时、_flush API | 后台自动(TieredMergePolicy)、_forcemerge |
| 对性能影响 | refresh 越频繁 → 小段越多 → merge 压力越大,反过来拖慢写入 | 消耗磁盘 IO,会有短时抖动 | 最重:大量磁盘 IO + CPU + 临时额外空间 |
| 能否手动控制 | refresh_interval(-1 关闭自动刷新) | 一般不需要手动 | _forcemerge(见第 7 题) |
三条容易答错的关联关系:
- refresh ≠ flush:refresh 只让数据"可搜索",flush 才让它"不丢"。「ES 每秒落盘」是错的,每秒只是 refresh;
- flush ≠ 数据可见:flush 不影响可见性;
- merge ≠ 删数据:merge 是把本来"逻辑已删除"(
.del标记)的文档物理清理掉,所以磁盘空间只有在 merge 之后才会真正释放。
二、文档的增删改与段管理
5. 文档的更新和删除是怎么实现的?
答: 根本原因是 Lucene 的 segment 不可变(immutable)——文档不能原地修改,所以只能"标记 + 重写"。
删除:
- 在 segment 的
.del(live docs)文件中把该 docId 标记为已删除; - 查询时这些文档仍可能被倒排索引匹配到,但结果会被过滤掉(所以查询结果正确);
- 磁盘空间不会立即释放——真正删除发生在段合并时(见第 7 题)。
更新(同 id 重新写入):
PUT /index/_doc/1实际上是两步:把旧版本文档标记为删除 + 索引一份新文档;- 新旧版本各自存在于不同的段中,查询时旧版本被
.del过滤掉; - 这也是为什么频繁更新文档会导致段膨胀、磁盘占用上涨(旧版本在 merge 前都还占着空间)。
部分更新 _update(重点:它不是原地更新):
_update 的实质是「read-modify-write」:
① 从 _source 取出原文档
② 应用修改(局部修改 doc,或执行 painless 脚本)
③ 把修改后的【完整文档】重新索引一遍(同样触发"标记删除旧版 + 写新版")由此推出三个重要事实:
_update不是原子操作:并发修改同一个文档可能互相覆盖(用if_seq_no/if_primary_term,或加retry_on_conflict=N让 ES 自动重试,见第 11 题);_update比_index更"重":多了一次读取_source的开销;- 文档不存在时
_update会报document_missing_exception,需要用upsert或doc_as_upsert。
批量修改要小心:_update_by_query 内部是「scroll 扫出文档 → 逐个 read-modify-write → _bulk 写回」,数据量大时非常慢(可加 slices 并行分片执行、wait_for_completion=false 异步跑)。
一句话总结:ES 里没有"更新",只有"标记删除 + 重新写入"。理解这一点,就能解释「为什么删除后磁盘没变小」「为什么频繁更新会拖慢查询」。
6. 什么是 Lucene 的段(segment)?为什么段不可变?
答: 一个 segment 就是一个完整的迷你倒排索引,包含自己的词典、倒排表、正排(doc_values)、删除标记等文件。一个索引分片(Lucene Index)由多个 segment + 一个 commit point 组成。
为什么设计成不可变(这是"优点"而非妥协):
| 好处 | 说明 |
|---|---|
| 无需加锁 | 数据只追加不修改,读取天然无锁、可并发(写读互不阻塞) |
| 写入快 | 只需顺序追加 + 产生新段,不必查找并修改已有结构 |
| 缓存友好 | 段级别缓存(OS filesystem cache、query cache)不会因修改而失效 |
| 崩溃恢复简单 | 段一旦写好就是自洽的,出问题只需丢弃未提交的段并重放 translog |
| 易于压缩存储 | 段文件写完后可一次性做最优压缩编码 |
代价(也就是面试要接着说的部分):
- 更新/删除只能"标记"(见第 5 题),空间不立即释放;
- 段数量会持续增长,而每次查询都要遍历所有段并合并结果 → 段越多,查询越慢、内存与文件句柄开销越大;
- 因此必须有段合并(merge)来收拾局面。
一句话:不可变换来了"无锁 + 快写 + 易恢复",代价交给后台的 merge 来还。
7. 段合并(merge)是怎么做的?_forcemerge 有什么风险?
答: merge 是后台把多个小 segment 合并为大 segment 的过程(ES 默认用 TieredMergePolicy:按段大小分层,优先合并体量相近的小段,大段不常参与)。
merge 做三件事:
- 把若干小段的数据重新写入一个新的大段(顺带重新压缩,编码更优);
- 物理删除被
.del标记的文档(空间此刻才真正回收); - 用新段替换旧段,段数量下降 → 查询更快。
代价(必须知道):
- 消耗大量磁盘 IO 与 CPU,严重时引发「merge 风暴」,查询与写入一起变慢;
- 合并期间新旧段共存,需要额外的临时磁盘空间(磁盘紧张时可能直接失败);
- 合并大段可能持续很久(小时级)。
_forcemerge(手动强制合并):
POST /index/_forcemerge?max_num_segments=1正确用途:只读索引(历史冷数据、归档日志、日志滚动后的旧索引)——合并成 1 个段能显著提升查询性能并回收被删除文档占用的空间。
风险与禁忌(高频追问):
| 风险 | 说明 |
|---|---|
| ❌ 不要在仍在写入的索引上 force_merge | 合并完成后新写入又会产生新段,白做且白白消耗 IO;正在写就永远合不到 1 个段 |
| ⚠️ 临时空间 | 合并大索引需要接近索引大小的临时空间,磁盘水位高时可能触发只读(见第 14 题) |
| ⚠️ 耗时极长 | 应在低峰执行,用 wait_for_completion=false 异步提交并监控 |
| ⚠️ 不保证达到目标 | max_num_segments=1 是"尽量",超大索引可能合不到 1 段(ES 8.x 对超大段也有保护) |
| ⚠️ 不要反复执行 | force_merge 后如果又有大量删除,会产生新的删除标记,反复合并得不偿失 |
实践建议:用 ILM 在索引 rollover 之后、转入 warm/cold 阶段时自动执行
forcemerge,而不是人工定期对所有索引跑一遍。
8. 文档是怎么被路由到某个分片的?
答: 路由公式是分片模型的核心(也解释了第 2 题为什么主分片数不能改):
shard = hash(_routing) % number_of_shards # _routing 默认取 _id路由的三个实践要点:
1)自动生成 _id 更均匀也更高效
- 自动生成的
_id是随机字符串,散列均匀,分片分布天然均衡; - 手动指定
_id时,ES 需要先确认该文档是否存在(用于版本控制),写入更慢; - 坑:若手动
_id有规律(如递增时间戳、按天编号),hash后仍可能集中到少数分片,形成热点分片(某个分片写入压力远大于其他)。
2)自定义 routing 可以把「相关数据」聚到一个分片
PUT /orders/_doc/1001?routing=user_42 # 写入
GET /orders/_search?routing=user_42 # 查询时也带 routing- 把同一用户/租户的数据路由到同一分片,查询时带上同样的
routing,协调节点就能只查一个分片,大幅减少扇出、提升性能; - 代价:
routing分布不均直接导致数据倾斜(大客户会把某个分片撑爆)。选择 routing key 时要评估基数与均匀性。
3)不带 routing 的查询必须广播
match_all、日期范围查询等无法确定目标分片的查询,协调节点会把请求广播到所有分片(每分片取主或副本之一),再合并结果;- 所以「查询扇出 = 分片数」,这也是「分片不要太多」的原因(见《ElasticSearch(一)》第 3 题)。
补充:客户端连哪个节点? 客户端(Java 的 RestClient / 8.x 的 Java API Client)配置一组节点地址,请求打到任意一个可达节点,该节点自动成为协调节点,负责路由、广播与结果合并。
版本提醒:
TransportClient在 7.0 被废弃、8.0 已移除,现代客户端都走 HTTP 层的 RestClient / Java API Client。答「用 TransportClient」会被认为知识陈旧。
三、写一致性与并发控制
9. ES 的写一致性是怎么保证的?
答: 现代 ES 用 wait_for_active_shards 做写入前的"门槛检查"。这里有一个流传极广的过时答案需要纠正。
❌ 过时答案(1.x / 2.x 时代,已废弃):写请求可带 consistency: one|quorum|all,其中 quorum = int((primary + replicas) / 2) + 1。
✅ 现代答案(2.0 起沿用至今):改用 wait_for_active_shards,含义是「写入前至少要有多少个活跃分片副本(含主分片),否则等待/报错」:
| 取值 | 含义 |
|---|---|
1(默认) | 只要主分片活跃就允许写 |
N(数字) | 需要至少 N 个活跃分片副本(含主分片) |
all | 需要主分片 + 所有副本都活跃 |
- 可作为请求参数临时指定,或作为索引设置
index.write.wait_for_active_shards长期生效; - 不满足时请求会等待(默认 1 分钟)直到满足,超时则报错;
- 它不是"写后复制保证":这只决定「写请求要不要放行」,写入之后如何复制到副本由主分片主导(见第 10 题)。
真正决定"写不丢"的三个因素(面试要串起来):
- 副本数:副本足够才能在节点故障时顶上;
wait_for_active_shards:保证写入时确实有足够副本在接数据;index.translog.durability: request:保证写入落盘(见第 3 题)。
一句话:ES 的写一致性 =
wait_for_active_shards(写入前门槛)+ 主分片主导的同步复制(写入后)+ translog(持久化)。
10. 主分片写成功后副本同步失败怎么办?
答: 先说清 ES 的写语义:主分片把请求转发给该分片组内所有活跃副本,等它们全部 ACK 后才向协调节点返回成功——即 ES 采用的是主分片主导的同步复制。
重要纠正:很多资料(包括原稿)会说「如果副本在 ISR 中……」——
ISR(In-Sync Replicas)是 Kafka 的概念,ES 没有这个机制。ES 的对应概念是「活跃副本 / in-sync shard copies(分片分配状态)」。把 Kafka 的术语套到 ES 上是典型的"串台",面试官一听就知道你只是背了八股。
副本写失败后的处理链路:
① 主分片向副本转发写请求,等待 ACK
② 副本写失败或超时
③ 主分片向 Master 上报该分片副本失败(shard failed)
④ Master 把该副本从活跃副本集合中移除,并触发【重新分配】
-> 在另一个健康节点上新建副本分片,从主分片完整复制数据
⑤ 期间该索引可能短暂变为 yellow(副本未分配)
⑥ 复制完成后集群恢复 green数据会不会丢?
- 不会丢已确认的写入:主分片本身是完好的,而且写入要等副本 ACK 才返回,所以「已返回成功」的数据在所有活跃副本上都有;
- 可能丢未确认的写入:如果主分片所在节点整体掉线,Master 会把某个副本提升为新的主分片;由于此前有部分写入可能还没复制到位,这部分未被确认的写入会丢(这也是为什么说 ES 是"准实时 + 最终一致");
- 如果副本数为 0:主分片一挂,该分片数据就无可恢复(只能从快照恢复),这也是「生产不要长期把副本设为 0」的原因。
写入被拒绝的常见情形(与副本相关的报错):
unavailable_shards_exception/no_shard_available_action_exception:活跃分片数不满足wait_for_active_shards;cluster_block_exception: index read-only:磁盘达洪泛水位,索引被置为只读(见第 14 题);es_rejected_execution_exception:写入线程池队列满,属于"写入太快"而不是副本问题(见第 14 题)。
11. 什么是乐观并发控制(_seq_no 与 _primary_term)?
答: ES 的文档级并发控制采用乐观锁:不预先加锁,而是在提交时校验「你读到的那份数据是否已被别人改过」。
两个关键字段:
| 字段 | 含义 | 特点 |
|---|---|---|
_seq_no | 分片级单调递增的操作序号 | 每写一次(含更新、删除)就 +1;是分片维度而非索引维度 |
_primary_term | 主分片任期号 | 主分片发生切换(重新分配)时会递增,用于防止"旧主分片上的 seq_no"被误用 |
使用方式:
# 带上 if_seq_no / if_primary_term 做 CAS 更新
POST /index/_update/1?if_seq_no=5&if_primary_term=1
{ "doc": { "status": "paid" } }
# 若文档已被别人改过 -> 返回 409 version_conflict_engine_exception- 冲突返回 409,由应用层决定是否重试(读最新、重算、再提交);
_update可带retry_on_conflict=N,让 ES 在冲突时自动重试 N 次(适合"自增/追加"这类可重放的更新);- 批量写时可用
conflicts=proceed(_bulk、_update_by_query支持)让冲突项跳过而不是整个失败。
与旧版 _version 的区别(版本考点):
旧版 _version | 现代 _seq_no + _primary_term | |
|---|---|---|
| 作用域 | 文档级版本号 | 分片级序号 + 主分片任期 |
| 并发控制 | version + version_type=external | if_seq_no + if_primary_term |
| 现状 | 7.x 起不再推荐用于并发控制(字段仍保留) | 推荐方式,能与副本/主分片切换正确协作 |
为什么需要 _primary_term(高频追问):只看 _seq_no 会有漏洞——主分片发生故障切换后,新主分片上的 seq_no 可能"变小"(因为它是从落后的副本提升的)。_primary_term 相当于"第几届主分片",把 (primary_term, seq_no) 组合起来才能唯一标识某个写入位置,避免把旧主分片上的状态误认为最新。
四、写入性能与排障
12. 如何优化写入性能?
答: 按「收益从大到小」排列,前三条通常是决定性的:
1)批量 + 并发(收益最大)
- 用
_bulk批量提交,单批 5~15MB(约 1000~5000 条文档)是比较稳的区间; - 客户端多线程并发写(一般 2~8 个线程即可,过多反而触发拒绝);
- 经验:「适度批量 + 适度并发」优于「超大单批」——超大单批会导致请求超时、内存暴涨与长尾延迟。
2)降低 refresh 频率
- 导入期间设置
index.refresh_interval: -1(或30s),避免每秒生成大量小段 ——小段越多,merge 压力越大,写入越慢; - 导入完成后恢复为
1s或按业务需要设置,并手动_refresh一次让数据可见。
3)导入期间把副本数设为 0
- 副本意味着每个文档都要在网络与磁盘上多写一份;
- 做法:创建索引时
number_of_replicas: 0→ 导入 → 改回 1(或更多),此时会触发副本复制(会消耗 IO,建议低峰)。
4)放宽持久化保证(按需)
index.translog.durability: async+sync_interval: 30s:牺牲「最多丢几十秒数据」换写入吞吐,仅适合可重建的数据(如日志、可重新导入的数据)。
5)使用自动生成的 _id
- 手动指定
_id时 ES 需要先检查文档是否存在(版本控制需要),写入更慢; - 若必须自定义 ID,注意避免 ID 有规律导致的热点分片(见第 8 题)。
6)Mapping 与存储优化
| 优化 | 收益 |
|---|---|
不需要检索的字段 index: false | 不建倒排索引,写入更快、更省空间 |
不需要排序/聚合的字段 doc_values: false | 不建正排,省磁盘与写入开销 |
整个对象 enabled: false | 只存 _source 便于备查,完全不索引 |
不需要评分/排序的字段用 keyword 而非 text | 避免分词开销 |
控制字段数量(index.mapping.total_fields.limit 默认 1000) | 避免字段爆炸拖慢集群状态与写入 |
7)硬件与系统层
- SSD(写入对磁盘 IO 极敏感)、足够的 IOPS 与磁盘带宽;
- 内存留给 filesystem cache(Lucene 读段文件靠它);
- 批量导入期间可临时放宽 merge 限流(
index.merge.scheduler.max_thread_count)与恢复限流(indices.recovery.max_bytes_per_sec); - 单节点写入上限受单分片限制,合理设置主分片数让写入并行(但不能太多)。
13. _bulk 批量写入要注意什么?
答: _bulk 是写入性能的关键,但用错会"批量失败"或"打挂集群"。
1)格式:NDJSON,且最后一行必须有换行
POST /_bulk
{"index":{"_index":"orders","_id":"1"}}
{"user":"u1","amount":100}
{"index":{"_index":"orders","_id":"2"}}
{"user":"u2","amount":200}
← 文件/请求体末尾必须有换行符(\n)- 每两行一组:action 行 + 文档行(
delete只需一行); - 常见报错
The bulk request must be terminated by a newline就是漏了最后的换行。
2)_bulk 不是事务:部分成功是常态
- 单个请求内文档之间互相独立,某个文档失败不影响其他文档;
- 返回码 200 不代表全部成功:必须遍历响应中的
items[],检查每一项的status与error; - 失败项要收集并重试,重试要有指数退避(尤其遇到 429/503)。
3)批量大小的取舍
| 批过大 | 批过小 |
|---|---|
请求体占内存、易超时、长尾延迟高、容易触发 es_rejected_execution_exception | 网络往返多、吞吐上不去 |
| 建议 5~15MB / 1000~5000 条 | 结合并发线程数调整 |
- 不要用单一维度调优:先固定并发线程数(如 4),再逐步增大批大小观察吞吐与延迟;或反之以批大小 5MB 为基准调并发;
- ES 官方压测脚本(
esrally)就是用这套思路找最优组合的。
4)其他实践
- 用
filter_path精简响应(只回items.*.status),避免大响应体反向拖慢客户端; - 写入时带
refresh=false(默认)即可,不要在批量导入时带refresh=true; - 做全量重建时配合别名切换实现零停机(见《ElasticSearch(四)》);
- 并发写同一文档时应设
conflicts=proceed或用if_seq_no控制,避免 409 失败。
14. 常见的写入报错有哪些?怎么排查?
答: 把报错按「被拒绝 / 被阻断 / 结构不对」三类记,比死背错误串有用得多。
| 报错 | 归因 | 原因与处理 |
|---|---|---|
es_rejected_execution_exception(bulk/write queue full) | 被拒绝 | write/bulk 线程池队列满(thread_pool.write.queue_size 默认 10000)。降并发 + 增加节点才是根因解法;调大队列只是把问题延后(还可能 OOM) |
cluster_block_exception: index [...] is read-only | 被阻断 | 磁盘达到 flood_stage(95%),ES 把所有相关索引置为只读。清理/扩容后再解除(PUT /index/_settings {"index.blocks.read_only_allow_delete": null}) |
unavailable_shards_exception / no_shard_available_action_exception | 被阻断 | 活跃分片不满足 wait_for_active_shards(节点掉线、分片正在恢复) |
mapper_parsing_exception | 结构不对 | 数据与 Mapping 类型不匹配(如给 date 字段传了非法日期、给 long 传了字符串) |
document_missing_exception | 结构不对 | _update 的文档不存在 → 用 upsert / doc_as_upsert |
illegal_argument_exception: Limit of total fields [1000] exceeded | 结构不对 | 动态字段数超 index.mapping.total_fields.limit(默认 1000)→ 别把大 JSON 直接塞进来,用 dynamic: strict 兜住 |
illegal_argument_exception: The bulk request must be terminated by a newline | 结构不对 | NDJSON 少末尾换行 |
version_conflict_engine_exception(409) | 并发 | if_seq_no 校验失败;按需重试或加 retry_on_conflict / conflicts=proceed |
排查顺序(实战链路):
- 看错误类型(是拒绝、阻断还是结构问题)——这一步就决定了后续方向;
GET _cluster/health?level=indices看集群状态与未分配分片;GET _cat/thread_pool/write,bulk?v看队列是否积压、rejected计数是否增长;GET _cat/allocation?v/GET _cat/nodes?v(关注disk.avail、heap.percent、cpu)看磁盘水位与资源;- 看 ES 服务端日志(
server/index日志)里的真实异常栈; - 客户端侧确认
_bulk响应里的items明细,别只看 HTTP 200。
一句话经验:「写不进去」先看磁盘水位,再看线程池队列,最后才怀疑 ES 本身有 bug。
下一篇:《ElasticSearch(三)》讲查询——Query Then Fetch 两阶段、query 与 filter 的本质区别、
term/match系列查询的选型陷阱、深翻页、聚合与熔断,以及查询性能调优与打分调试。
