主题
写入与索引过程
一句话: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.durability | request | 每次写请求 fsync translog 后返回,不丢已确认写 |
index.translog.durability=async | — | 按 sync_interval 批量 fsync,可能丢一个间隔的数据 |
index.translog.sync_interval | 5s | async 模式下的 fsync 间隔;不是 flush 间隔 |
index.translog.flush_threshold_size | 512mb | translog 达到该大小触发 flush |
index.translog.flush_threshold_period | 30m | 距上次 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。