深入 Elasticsearch(05):近实时、Translog 与 Refresh/Flush
上一篇解决了文本怎样变成词项——analysis pipeline 把原始文本标准化为可索引的 term。这一篇进入文档写入后怎样变成可搜索的。
一条文档经过 analysis 生成词项后,并不会立即对搜索可见。ES 的写入路径涉及内存缓冲、translog 持久化、refresh 生成新 segment、flush 提交到磁盘四个步骤。这套机制让 ES 在写入吞吐和搜索延迟之间取得平衡,也是"近实时搜索"(near-real-time, NRT)这个名字的由来。
本文只抓一个问题:ES 的近实时搜索是怎么实现的,refresh 和 flush 的区别在哪里。
写入路径的完整流程
一条文档从客户端发出到最终持久化到磁盘,经过以下步骤:
1 | |
上面的文本描述涉及 Client、Coordinating Node、Primary Shard、Replica、in-memory buffer、translog、segment、磁盘等多个实体和两条时间线(refresh / flush)。下图把这些实体和时间点放到一张图里,纵向是数据流,横向标注两个关键时间点。
flowchart TD
Client["Client"] -->|"路由"| Coord["Coordinating Node"]
Coord --> Primary["Primary Shard"]
Primary --> Buf["In-memory Buffer"]
Primary --> TL["Translog<br/>(fsync 到磁盘)"]
Primary -->|"转发"| Replica["Replica Shard"]
Buf -->|"refresh<br/>(默认 1s)"| Seg["新 Segment<br/>(内存中,可搜索)"]
Seg -->|"flush<br/>(translog 达阈值)"| Disk["Segment fsync 到磁盘<br/>+ 清空 Translog"]
style Buf fill:#fff4e0,stroke:#cc9944
style TL fill:#f0fff0,stroke:#66aa66
style Seg fill:#f0f4ff,stroke:#6688cc
style Disk fill:#e8e8e8,stroke:#888888
这个流程中有两个关键的时间点:refresh 之后文档可搜索,flush 之后文档持久化到磁盘。两者之间的间隔就是"近实时"中"近"的含义——不是真正的实时,而是有一个 refresh 间隔(默认 1 秒)。
Refresh:从不可见到可搜索
Refresh 的作用是把 in-memory buffer 中的数据转化为一个新的 Lucene segment。这个新 segment 被打开(open),立即对搜索可见——但尚未被 fsync 到磁盘。
1 | |
Refresh 前后的状态变化涉及 buffer、segment 集合、搜索可见性三个维度的同时变更,下面这张图把 refresh 前后两个快照并排放置,箭头标注数据的去向。
flowchart LR
subgraph BEFORE["refresh 前"]
direction TB
buf1["In-memory Buffer<br/>doc1, doc2, doc3<br/>(不可搜索)"]
segs1["已有 Segments<br/>seg0, seg1<br/>(可搜索)"]
end
subgraph AFTER["refresh 后"]
direction TB
buf2["In-memory Buffer<br/>(空)"]
segs2["Segments<br/>seg0, seg1, seg2<br/>(全部可搜索)"]
end
buf1 -->|"生成 seg2"| segs2
Refresh 的代价不高(相比 fsync),但也不是零成本。每次 refresh 都会产生一个新的 segment,segment 过多会增加搜索开销并触发后台 merge。
refresh_interval 控制自动 refresh 的间隔,默认 1 秒。可以根据场景调整:
| 场景 | refresh_interval | 理由 |
|---|---|---|
| 默认 | 1s |
近实时搜索,写入后约 1 秒可见 |
| 批量导入 | "-1"(禁用) |
写入完成后手动 refresh,避免频繁生成小 segment |
| 准实时要求 | 200ms |
更快可见,但 merge 压力更大 |
| 日志场景 | 30s |
日志不需要秒级可见,减少 segment 数量 |
手动触发 refresh:
1 | |
Translog:写入的持久化保障
Refresh 让文档可搜索,但新 segment 还没有 fsync 到磁盘。如果这时候节点崩溃,in-memory buffer 和未提交的 segment 都会丢失。Translog(事务日志)就是为了解决这个问题。
每次写入操作在进入 in-memory buffer 的同时,会追加到 translog。Translog 是一个顺序写的日志文件,默认在每次写入操作后 fsync 到磁盘(index.translog.durability: request)。
1 | |
Translog 的角色等同于关系型数据库中的 WAL(Write-Ahead Log)。核心思想相同:在数据结构本身持久化之前,先把操作记录到一个顺序写的日志中,用日志保证持久性。
Translog 的 durability 可以配置:
| 配置 | 行为 | 持久性 | 性能 |
|---|---|---|---|
request(默认) |
每次写入后 fsync translog | 最高 | 每次写入一次 fsync |
async |
每 sync_interval(默认 5s)fsync 一次 |
可能丢失最近 5s 的数据 | 更高吞吐 |
Flush:Lucene Commit
Flush 是把 Lucene 的状态持久化到磁盘的操作。它做三件事:
- 把所有 in-memory buffer 中的数据生成 segment(等同于一次 refresh)
- 对所有未提交的 segment 执行 fsync(Lucene commit point)
- 清空 translog(因为 segment 已经持久化,不再需要 translog 来恢复)
1 | |
Flush 的触发条件:
- Translog 大小超过阈值(
index.translog.flush_threshold_size,默认 512MB) - 距离上次 flush 时间超过阈值
- 手动调用
_flushAPI
手动触发 flush:
1 | |
Refresh vs Flush 对比
| 维度 | Refresh | Flush |
|---|---|---|
| 核心动作 | buffer → new segment(内存中) | segment fsync to disk + clear translog |
| 效果 | 文档变为可搜索 | 文档持久化到磁盘 |
| 默认间隔 | 1 秒 | translog 达到 512MB 或手动触发 |
| 成本 | 低(不涉及磁盘 fsync) | 高(fsync 所有未提交 segment) |
| 数据安全 | 无(新 segment 未持久化) | 有(segment 已在磁盘上) |
| 类比 | 打开新文件供读取 | 数据库 checkpoint |
实验:观察近实时行为
创建一个 index 并设置较长的 refresh_interval:
1 | |
写入一条文档:
1 | |
立即搜索——不会返回任何结果:
1 | |
手动 refresh:
1 | |
再次搜索——文档出现:
1 | |
恢复默认 refresh_interval:
1 | |
模式提炼:WAL + 延迟可见性
1 | |
ES 的写入路径本质是"先写日志保持久、再异步物化提供查询"的模式。这个模式的核心 trade-off 是:牺牲即时可见性(延迟 1 秒),换取更高的写入吞吐(不需要每次写入都 fsync 索引文件)。
| 系统 | WAL | 物化操作 | 可见性延迟 |
|---|---|---|---|
| Elasticsearch | Translog | Refresh(生成 segment) | 默认 1 秒 |
| MySQL (InnoDB) | Redo log | Checkpoint(写脏页) | 事务提交即可见 |
| PostgreSQL | WAL | Checkpoint | 事务提交即可见 |
| Apache Kafka | Log segment | 本身就是日志存储 | 取决于 ISR + acks |
| SQLite | WAL mode journal | Checkpoint | 写入即可见 |
ES 和传统数据库的关键区别在于:数据库的 WAL 用于崩溃恢复,可见性由事务控制,事务提交后立即可见。ES 的 translog 也用于崩溃恢复,但可见性由 refresh 控制,refresh 之前文档不可搜索。
工程迁移表
| 概念 | Elasticsearch | MySQL (InnoDB) | PostgreSQL | Kafka |
|---|---|---|---|---|
| 预写日志 | Translog | Redo log | WAL | Log segment |
| 内存缓冲 | Indexing buffer | Buffer pool | Shared buffers | Page cache |
| 可搜索时机 | Refresh 之后 | Commit 之后 | Commit 之后 | Consumer lag |
| 持久化时机 | Flush 之后 | Checkpoint 之后 | Checkpoint 之后 | fsync / flush |
| 默认持久化策略 | 每次写入 fsync translog | 每次 commit fsync redo log | 每次 commit fsync WAL | 可配 acks |
常见误解
误解一:写入返回成功就表示文档可以被搜索到。 写入返回成功只表示文档已经进入 in-memory buffer 和 translog。文档在下一次 refresh 之后才能被搜索到。如果需要写入后立即搜索,可以在写入请求中加 ?refresh=true(但会影响性能)或 ?refresh=wait_for(等待下一次自然 refresh)。
误解二:refresh 就是 flush。 refresh 只是生成新 segment 并打开供搜索,不涉及磁盘 fsync。flush 才是把 segment fsync 到磁盘。refresh 频繁执行(默认每秒),flush 较少执行(translog 达到阈值时)。
误解三:禁用 translog 可以提高写入性能。 ES 不支持完全禁用 translog。可以把 index.translog.durability 设为 async 减少 fsync 频率,但代价是节点崩溃时可能丢失最近几秒的数据。在数据可重放的场景(如从 Kafka 消费写入)中可以接受。
练习
-
创建一个
refresh_interval: -1的 index,写入 5 条文档,验证搜索不到。然后手动_refresh,验证搜索可以返回结果。 -
写入文档后,用
_segmentsAPI 观察 segment 变化。对比 refresh 前后和 flush 前后 segment 的committed状态。 -
把
index.translog.durability设为async,用批量写入测试吞吐量差异。注意:这个实验要在测试环境进行,不要在生产数据上操作。
系列导航
| 上一篇 | 下一篇 |
|---|---|
| Analysis 管道:从原始文本到可搜索词项 | 搜索执行模型:Query-Then-Fetch 的两阶段流程 |
