ES 为什么是近实时而非实时
ES 为什么是近实时而非实时
1. 写入链路全景
一条文档写入 ES 后,并不是立刻能被搜索到。它经过以下路径:
Client 写入请求
│
▼
[Translog] ← 先写 WAL,防止内存丢数据
│
▼
[In-Memory Buffer] ← 原始 JSON 暂存内存
│
│ ◄── refresh 触发(默认 1s)
▼
[Segment] ← Buffer 刷成不可变段,此时可搜索
│
│ ◄── flush 触发(默认 30min 或 translog 满)
▼
[Disk] ← Segment 持久化到磁盘
│
│ ◄── merge 后台合并小段
▼
[Larger Segment] ← 减少段数量,提升查询效率
关键区分:
- refresh(轻量):Buffer → Segment,写入 OS Cache,可搜索
- flush(重量):执行 refresh 后,调用 fsync 落盘,清空 translog
- merge(后台):合并小 Segment 为大 Segment,减少文件句柄
2. 为什么不能每条文档都做 refresh
Lucene 的 Segment 是**不可变(Immutable)**的。每个 Segment 内部有自己的倒排索引、正排存储、DocValues 等结构。每做一次 refresh 就要:
- 将 Buffer 中的数据转为一个新 Segment
- 打开该 Segment 的文件句柄
- 更新 IndexReader,重新打开所有 Segment 获取全局视图
如果每条写入都 refresh,相当于写入 1 条产生 1 个 Segment。10 万条文档就是 10 万个 Segment——查询时要遍历 10 万个内部索引,性能直接崩盘。不夸张地说,每条 refresh 意味着 O(N) 的查询复杂度。
3. refresh_interval=1s 的由来
ES 设计定位是全文搜索引擎,而不是关系型数据库。在搜索场景中,用户修改了内容后,1 秒内被其他用户搜索到,已经能满足绝大多数需求。这个默认 1 秒是工程上的折中——在写入性能和可见性延迟之间取得平衡。
refresh_interval 调优
PUT logs/_settings
{
"index": {
"refresh_interval": "30s"
}
}
| 场景 | 建议 refresh_interval | 理由 |
|---|---|---|
| 日志/监控(写入密集) | 30s~60s | 写入优先,延迟可容忍 |
| 电商搜索 | 1s(默认) | 商品上架需要较快可见 |
| 实时大屏/告警 | 1s + 业务层补偿 | 1s 是最小合理刷新间隔 |
| 批量导入 | -1(关闭) | 导入完成后再手动刷新 |
批量导入关闭 refresh 的写法:
PUT my_index
{
"settings": {
"refresh_interval": "-1",
"number_of_replicas": 0
}
}
POST _bulk
{ "index": { "_index": "my_index" } }
{ "title": "doc1" }
{ "index": { "_index": "my_index" } }
{ "title": "doc2" }
...
PUT my_index/_settings
{
"index": {
"refresh_interval": "1s",
"number_of_replicas": 1
}
}
// 手动刷新确保可见
POST my_index/_refresh
踩坑:生产环境直接关闭 refresh 忘了恢复
某数据团队批量导入 500 万条数据后,忘记恢复 refresh_interval。前端搜索永远返回空,排查 3 小时才发现 refresh 已关闭。教训:批量导入完成后一定要在脚本里显式恢复 refresh_interval。
4. 数据可见延迟分析
从客户端视角看延迟链路:
Client 发起写入
↓ 网络 RTT(~1ms 内网)
Node 接收写入
↓ translog 顺序写(~<1ms)
Buffer 暂存
↓ 等待 refresh 间隔(最多 1s)
Segment 生成,可被搜索
↓
Client 发起搜索,查到此文档
总延迟 = 网络 RTT + translog 写入 + 等待 refresh + 再次 RTT
最坏情况:刚过 refresh 点写入 → 等满 1s 才可见 → ~1s 延迟
最好情况:临近 refresh 点写入 → ~几十 ms 延迟
也可以用 _refresh API 强制刷新,但注意这会触发所有 Segment 重建 IndexReader,影响全局查询性能:
POST my_index/_refresh
仅对特定文档强制刷新(ES 不直接支持单文档 refresh),变通方案:
GET my_index/_doc/1?realtime=true
realtime=true(默认值)会从 translog 中读取未 refresh 的文档,GET by ID 能拿到,但搜索仍搜不到。
5. 与 MySQL WAL + Buffer Pool 对比
| 维度 | ES(Lucene) | MySQL(InnoDB) |
|---|---|---|
| 先写日志 | Translog(WAL) | Redo Log(WAL) |
| 内存缓冲 | In-Memory Buffer | Buffer Pool(Page Cache) |
| 刷入介质 | refresh → Segment(OS Cache) | checkpoint → 脏页刷盘 |
| 默认可见延迟 | 1s(refresh_interval) | 0(写即提交) |
| 不可变性 | Segment 不可变 | Page 可变(原地更新) |
| 主要瓶颈 | refresh 产生大量 Segment | 随机写磁盘 |
MySQL 的 InnoDB 是写即提交,事务提交后立刻对其他事务可见(隔离级别允许下)。ES 则是有意延迟,换取更高的写入吞吐。两者设计哲学不同——一个是 OLTP 事务引擎,一个是全文搜索数据分析引擎。
6. 总结
ES 的 NRT(Near Real Time)源于 Segment 不可变的设计——每次 refresh 产生一个新 Segment,频繁 refresh 会导致"段爆炸",查询需要打开巨量文件。1 秒的默认 refresh 间隔是工程上的最佳实践。生产环境要根据业务场景调优,批量导入时关闭 refresh,导入后恢复;日志场景适当放宽到 30s~60s 以提升写入吞吐。