跳到正文

写入与索引过程 ​

一句话:ES 写入先落 Lucene 内存缓冲和 translog,默认 1 秒 refresh 成可搜索的 Segment,再由 flush 做 Lucene commit 持久化;「写完查不到」和「5 秒 flush」都是把 refresh、translog fsync、flush 三件事混为一谈。

适用版本:Elasticsearch 8.x。默认值以 8.x 官方文档为准。

为什么需要 ​

如果每次写入都立刻 fsync 成磁盘上的新段,写入吞吐会非常低;如果只写内存,进程一崩就丢数据。ES 用三层折中:

  • refresh:把内存缓冲变成 Segment,让数据可搜索,但不保证持久化。
  • translog:预写日志,保证进程崩溃后能重放。
  • flush:Lucene commit,把 Segment fsync 到磁盘并截断 translog。

理解这三层的触发条件和持久化语义,才能回答「写入成功了吗」「为什么查不到」「会不会丢」。

核心机制 ​

主副本写入链路 ​

  • index.write.wait_for_active_shards 默认 1,表示至少主分片活跃;它约束的是活跃分片数,不改变「主分片同步复制到 in-sync 副本」的事实。
  • 副本失败会被移出 in-sync 集合,主分片继续服务;恢复后再追平。
  • 写请求的 refresh 参数只影响可见性,不影响 translog 持久化。

refresh:1 秒可见,不等于持久化 ​

  • index.refresh_interval 默认 1s。refresh 把内存缓冲生成一个新的 Lucene Segment,打开搜索器,数据从此可被搜索。
  • refresh 后的 Segment 还在文件系统缓存里,没有 fsync;进程崩溃后要靠 translog 重放。
  • refresh_interval=-1 关闭自动 refresh,适合批量导入;0 不是合法值。
  • ?refresh=wait_for 让本次请求等待下一次 refresh,适合测试;不要对每条写入都加。

translog:崩溃恢复的 WAL ​

配置默认行为
index.translog.durabilityrequest每次写请求 fsync translog 后返回,不丢已确认写
index.translog.durability=async—按 sync_interval 批量 fsync,可能丢一个间隔的数据
index.translog.sync_interval5sasync 模式下的 fsync 间隔;不是 flush 间隔
index.translog.flush_threshold_size512mbtranslog 达到该大小触发 flush
index.translog.flush_threshold_period30m距上次 flush 超过该时间也触发

最常见的错误是把「5 秒」说成「每 5 秒 flush 一次」。5 秒是 sync_interval,只在 durability=async 时决定 translog fsync 频率;默认 request 模式下每次写都 fsync。flush 由 translog 大小/时长触发,是 Lucene commit,不是每 5 秒一次。

flush 与 merge ​

  • flush:执行 Lucene commit,把当前 Segment fsync 到磁盘,写 commit point,然后截断 translog。触发条件是 translog 达到 flush_threshold_size(默认 512MB)或超过 flush_threshold_period(默认 30m),也可以手动 POST /index/_flush。
  • merge:后台把多个小 Segment 合并成大 Segment,同时清理 .del 标记的已删除文档和旧版本。merge 是 IO/CPU 密集型,ES 自动调度;_forcemerge 只对不再写入的只读索引做。
  • update:本质是「旧文档标记删除 + 写入新文档」,不是原地修改;delete 只写删除标记,空间要等 merge 回收。

崩溃恢复 ​

如果 durability=async 且崩溃发生在两次 fsync 之间,这段时间的写入会丢;request 模式下已返回成功的写入不会因为进程崩溃而丢(但主分片故障后的数据保留还取决于副本和写关注)。

最小上手 ​

bash
# 写入并等待一次 refresh(仅验证用)
curl -X POST "localhost:9200/logs/_doc/1?refresh=wait_for" \
  -H 'Content-Type: application/json' \
  -d '{"level":"error","message":"timeout"}'

# 手动 refresh / flush / merge
curl -X POST "localhost:9200/logs/_refresh"
curl -X POST "localhost:9200/logs/_flush"
curl -X POST "localhost:9200/logs/_forcemerge?max_num_segments=1"

# 看段和 translog 相关统计
curl "localhost:9200/_cat/segments/logs?v"
curl "localhost:9200/_nodes/stats/indices/segments,translog?pretty"
json
// 调整写入相关设置(8.x)
PUT /logs/_settings
{
  "index": {
    "refresh_interval": "1s",
    "translog": {
      "durability": "request",
      "sync_interval": "5s",
      "flush_threshold_size": "512mb"
    }
  }
}

生产动作 ​

  • 批量导入:refresh_interval=-1、number_of_replicas=0,导入完成后恢复;bulk 按 5–15MB 控制,不要按条数。
  • 不要频繁 refresh:每条写入都 refresh=wait_for 会产生大量小 Segment,merge 压力暴涨。
  • translog 持久化:默认 request 不丢已确认写;改成 async 只能用于可重灌的数据,并明确接受 5 秒级丢失窗口。
  • flush 不要手动频繁触发:让大小/时长阈值工作;手动 flush 只在维护窗口做。
  • forcemerge 只对只读索引:写入型索引频繁 forcemerge 会和正常 merge 抢 IO。
  • 监控:_nodes/stats/indices/segments 的段数、refresh 耗时、translog 大小、merge 速率。

边界 ​

  • 近实时不是实时:默认最多 1 秒不可见;不要用它做「写完立即读」的一致性校验。
  • refresh 不等于持久化:refresh 后可搜索,但崩溃恢复仍靠 translog + commit。
  • async translog 会丢数据:这是用持久性换吞吐,不是「异步更快但一样安全」。
  • 频繁 update/delete 成本高:旧文档要等 merge 才回收,写多改多的场景不适合 ES。
  • merge 会吃 IO:大 merge 期间查询和写入都可能抖动;不要在业务高峰 forcemerge。

讲清楚 ​

ES 写入分三层:先写 Lucene 内存缓冲和 translog,默认 1 秒 refresh 成 Segment,数据就可搜索了;但 refresh 不 fsync,崩溃后要靠 translog 重放;flush 才是 Lucene commit,把 Segment 落盘并截断 translog,触发条件是 translog 到 512MB 或超过 30 分钟。

所以「5 秒 flush」是错的:5 秒是 async 模式下 translog 的 fsync 间隔,默认 durability=request 是每次写都 fsync。写入成功只代表主分片和 in-sync 副本写完了,不代表已经 flush。生产上批量导入关 refresh、关副本,导入后恢复;forcemerge 只对只读索引做,否则会和正常 merge 抢 IO。

把一门技术讲透到既懂也会用