ES 为什么是近实时而非实时

22 May 2026 – wusfe · 4 min read

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 就要:

  1. 将 Buffer 中的数据转为一个新 Segment
  2. 打开该 Segment 的文件句柄
  3. 更新 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 以提升写入吞吐。