Elasticsearch 底层原理系列(三):文档写入流程
2020-10-30·6 分钟阅读
前言
Elasticsearch 的写入流程是其高性能和近实时搜索能力的关键。理解写入流程中的 Buffer、Translog、Refresh、Flush 等机制,对于优化写入性能和保证数据可靠性至关重要。
本章将深入解析文档从写入到可搜索的完整流程。
技术亮点
| 技术点 | 难度 | 面试价值 | 本文覆盖 |
|---|---|---|---|
| 写入流程 | ⭐⭐⭐ | 高频考点 | ✅ |
| Refresh 机制 | ⭐⭐⭐⭐ | 高频考点 | ✅ |
| Translog 原理 | ⭐⭐⭐⭐ | 高频考点 | ✅ |
| Flush vs Refresh | ⭐⭐⭐⭐ | 常见误区 | ✅ |
面试考点
- Elasticsearch 文档写入的完整流程是什么?
- Refresh 和 Flush 有什么区别?
- Translog 的作用是什么?如何保证数据不丢失?
- 如何优化 ES 的写入性能?
写入流程概览
整体架构
┌─────────────────────────────────────────────────────────────────────────┐
│ 文档写入完整流程 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ Client │
│ │ │
│ │ 1. 发送写入请求 │
│ ▼ │
│ ┌─────────────┐ │
│ │ Coordinating│ │
│ │ Node │ ─── 路由解析、请求转发 │
│ └──────┬──────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Primary Shard │ │
│ │ │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ Memory │ │ Translog │ │ OS Cache │ │ Disk │ │ │
│ │ │ Buffer │───►│ (WAL) │───►│ Segment │───►│ Segment │ │ │
│ │ │ │ │ │ │ (Refresh)│ │ (Flush) │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ │
│ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │
│ │ 写入内存 持久化日志 1秒后可见 持久化到磁盘 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ │ 同步到副本分片 │
│ ▼ │
│ ┌─────────────┐ │
│ │ Replica │ │
│ │ Shard │ ─── 相同的写入流程 │
│ └─────────────┘ │
│ │ │
│ ▼ │
│ 返回写入成功响应 │
│ │
└─────────────────────────────────────────────────────────────────────────┘
时间线视角
┌─────────────────────────────────────────────────────────────────────────┐
│ 写入时间线 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ T0 T1 (约1秒) T2 (约5分钟) T3 (约30分钟) │
│ │ │ │ │ │
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ ┌────┐ ┌────┐ ┌────┐ ┌────┐ │
│ │写入│ │可搜│ │Segment │持久│ │
│ │完成│ │索 │ │部分合并 │化 │ │
│ └────┘ └────┘ └────┘ └────┘ │
│ │
│ • T0: 文档写入 Memory Buffer + Translog │
│ • T1: Refresh 操作,Segment 在 OS Cache 中可搜索 │
│ • T2: 小 Segment 开始合并 │
│ • T3: Flush 操作,Segment 持久化到磁盘 │
│ │
└─────────────────────────────────────────────────────────────────────────┘
详细流程解析
Step 1: 协调节点处理
┌─────────────────────────────────────────────────────────────────────────┐
│ 协调节点处理流程 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 1. 请求解析 │
│ • 解析 JSON 文档 │
│ • 验证文档结构 │
│ • 提取路由信息 │
│ │
│ 2. 路由计算 │
│ shard_num = hash(_routing) % num_primary_shards │
│ • 默认 _routing = _id │
│ • 可自定义 routing 参数 │
│ │
│ 3. 转发到目标节点 │
│ • 确定主分片所在节点 │
│ • 发送写入请求 │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ Client ──► Coordinating Node ──► Primary Shard Node │ │
│ │ (路由计算) (实际写入) │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Step 2: 写入 Memory Buffer
┌─────────────────────────────────────────────────────────────────────────┐
│ Memory Buffer 写入 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ IndexWriter (Lucene) 处理流程: │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ Document │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────┐ │ │
│ │ │ Analyzer │ ─── 分词、归一化 │ │
│ │ └──────┬──────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────┐ │ │
│ │ │ In-Memory │ ─── 构建倒排索引 │ │
│ │ │ Index Buffer│ (Term Dictionary + Postings) │ │
│ │ └──────┬──────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────┐ │ │
│ │ │ Doc Values │ ─── 构建列式存储 │ │
│ │ │ Buffer │ (用于排序聚合) │ │
│ │ └─────────────┘ │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ 此时数据: │
│ • 在内存中,尚未可搜索 │
│ • 丢失风险:节点崩溃会丢失 │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Step 3: 写入 Translog
┌─────────────────────────────────────────────────────────────────────────┐
│ Translog(事务日志) │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ Translog = Transaction Log,类似于数据库的 WAL(Write-Ahead Log) │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 写入流程: │ │
│ │ 1. 文档写入 Memory Buffer 的同时 │ │
│ │ 2. 将操作追加到 Translog 文件 │ │
│ │ 3. 根据 sync 设置决定是否 fsync │ │
│ │ │ │
│ │ Translog 内容: │ │
│ │ ┌──────────────────────────────────────────────────────┐ │ │
│ │ │ Op Type │ Doc ID │ Source JSON │ Timestamp │ ... │ │ │
│ │ ├─────────┼────────┼─────────────┼───────────┼─────────┤ │ │
│ │ │ INDEX │ doc_1 │ {...} │ T1 │ │ │ │
│ │ │ INDEX │ doc_2 │ {...} │ T2 │ │ │ │
│ │ │ DELETE │ doc_3 │ - │ T3 │ │ │ │
│ │ │ UPDATE │ doc_1 │ {...} │ T4 │ │ │ │
│ │ └──────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ 作用: │
│ • 数据可靠性保证(节点崩溃后可恢复) │
│ • 支持实时 GET 操作(从 Translog 读取) │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Step 4: Refresh(刷新)
┌─────────────────────────────────────────────────────────────────────────┐
│ Refresh 操作 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ Refresh 是实现"近实时搜索"的关键! │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ Before Refresh: │ │
│ │ ┌──────────────┐ ┌──────────────┐ │ │
│ │ │ Memory Buffer│ │ Translog │ │ │
│ │ │ [docs...] │ │ [ops...] │ │ │
│ │ └──────────────┘ └──────────────┘ │ │
│ │ │ │ │
│ │ │ Refresh (默认每秒执行) │ │
│ │ ▼ │ │
│ │ After Refresh: │ │
│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │
│ │ │ Memory Buffer│ │ Translog │ │ OS Cache │ │ │
│ │ │ (清空) │ │ (保留) │ │ New Segment │ │ │
│ │ │ │ │ │ │ (可搜索) │ │ │
│ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ 关键点: │
│ • Segment 写入 OS Cache,不是磁盘! │
│ • 操作轻量,不影响性能 │
│ • 数据立即可搜索 │
│ • Buffer 被清空,Translog 保留 │
│ │
│ 默认配置: │
│ • refresh_interval = 1s │
│ • 每秒自动执行一次 │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Refresh 配置优化
// 动态调整 refresh 间隔
PUT /my_index/_settings
{
"index": {
"refresh_interval": "30s" // 批量导入时可调大
}
}
// 或完全禁用(批量导入完成后手动 refresh)
PUT /my_index/_settings
{
"index": {
"refresh_interval": "-1" // 禁用自动 refresh
}
}
// 手动触发 refresh
POST /my_index/_refresh
Step 5: Flush(刷盘)
┌─────────────────────────────────────────────────────────────────────────┐
│ Flush 操作 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ Flush = Refresh + 持久化 + 清理 Translog │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ Flush 执行时机: │ │
│ │ 1. Translog 达到大小阈值(默认 512MB) │ │
│ │ 2. 定时触发(默认 30 分钟) │ │
│ │ 3. 手动触发 │ │
│ │ │ │
│ │ Flush 执行步骤: │ │
│ │ ┌──────────────────────────────────────────────────────────┐ │ │
│ │ │ 1. 执行 Refresh(确保所有内存数据变成 Segment) │ │ │
│ │ │ 2. 调用 Lucene commit(fsync Segment 到磁盘) │ │ │
│ │ │ 3. 清理旧的 Translog 文件 │ │ │
│ │ │ 4. 更新 commit point │ │ │
│ │ └──────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ Before Flush: │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ Segment1 │ │ Segment2 │ │ Segment3 │ │ Translog │ │ │
│ │ │ (cache) │ │ (cache) │ │ (cache) │ │ (大) │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ │
│ │ │ │
│ │ After Flush: │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ Segment1 │ │ Segment2 │ │ Segment3 │ │ Translog │ │ │
│ │ │ (disk) │ │ (disk) │ │ (disk) │ │ (新文件) │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ 重要:Flush 是重量级操作,不要频繁调用! │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Refresh vs Flush 对比
┌─────────────────────────────────────────────────────────────────────────┐
│ Refresh vs Flush │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────┬───────────────────────┐ │
│ │ Refresh │ Flush │ │
│ ├───────────────────────┼───────────────────────┤ │
│ │ 目的:使数据可搜索 │ 目的:数据持久化 │ │
│ ├───────────────────────┼───────────────────────┤ │
│ │ 频率:默认 1 秒 │ 频率:默认 30 分钟 │ │
│ ├───────────────────────┼───────────────────────┤ │
│ │ 目标:OS Cache │ 目标:磁盘 │ │
│ ├───────────────────────┼───────────────────────┤ │
│ │ 重量:轻量 │ 重量:重量级 │ │
│ ├───────────────────────┼───────────────────────┤ │
│ │ Translog:保留 │ Translog:清理 │ │
│ ├───────────────────────┼───────────────────────┤ │
│ │ Buffer:清空 │ Buffer:不涉及 │ │
│ └───────────────────────┴───────────────────────┘ │
│ │
│ 记忆技巧: │
│ • Refresh = 刷新可见性(内存 → OS Cache) │
│ • Flush = 刷盘持久化(OS Cache → Disk) │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Translog 配置详解
Translog 配置参数
PUT /my_index/_settings
{
"index": {
"translog": {
"durability": "async", // 或 "request"
"sync_interval": "5s", // async 模式下的同步间隔
"flush_threshold_size": "512mb" // 触发 flush 的阈值
}
}
}
Durability 模式
┌─────────────────────────────────────────────────────────────────────────┐
│ Translog Durability 模式 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 1. request(默认) │
│ • 每次写入请求后 fsync │
│ • 保证数据不丢失 │
│ • 性能较低 │
│ │
│ 2. async │
│ • 定期 fsync(默认 5 秒) │
│ • 可能丢失最近 5 秒的数据 │
│ • 性能更高 │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 性能 vs 可靠性权衡 │ │
│ │ │ │
│ │ request 模式: │ │
│ │ ┌────────────────────────────────────────────────────────┐ │ │
│ │ │ 写入 → Translog → fsync → 返回确认 │ │ │
│ │ │ ↑ │ │ │
│ │ │ 每次请求都同步 │ │ │
│ │ └────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ async 模式: │ │
│ │ ┌────────────────────────────────────────────────────────┐ │ │
│ │ │ 写入 → Translog → 返回确认 │ │ │
│ │ │ │ │ │ │
│ │ │ └── 后台每 5 秒 fsync 一次 │ │ │
│ │ └────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
写入性能优化
批量写入优化
┌─────────────────────────────────────────────────────────────────────────┐
│ 批量写入最佳实践 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 1. 使用 Bulk API │
│ • 批量大小:5-15MB 为佳 │
│ • 文档数量:1000-5000 个/批 │
│ │
│ 2. 调整 Refresh 间隔 │
│ • 批量导入时设置 refresh_interval = -1 │
│ • 导入完成后恢复并手动 refresh │
│ │
│ 3. 调整 Translog 配置 │
│ • 使用 async durability │
│ • 增大 flush_threshold_size │
│ │
│ 4. 调整副本数量 │
│ • 批量导入时设置 number_of_replicas = 0 │
│ • 导入完成后恢复副本 │
│ │
│ 优化配置示例: │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ │ // 导入前设置 │ │
│ │ PUT /my_index/_settings │ │
│ │ { │ │
│ │ "index": { │ │
│ │ "refresh_interval": "-1", │ │
│ │ "number_of_replicas": 0, │ │
│ │ "translog.durability": "async" │ │
│ │ } │ │
│ │ } │ │
│ │ │ │
│ │ // 执行 Bulk 导入... │ │
│ │ │ │
│ │ // 导入后恢复 │ │
│ │ POST /my_index/_refresh │ │
│ │ PUT /my_index/_settings │ │
│ │ { │ │
│ │ "index": { │ │
│ │ "refresh_interval": "1s", │ │
│ │ "number_of_replicas": 1 │ │
│ │ } │ │
│ │ } │ │
│ └────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
写入流程时序图
┌─────────────────────────────────────────────────────────────────────────┐
│ 写入流程时序图 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ Client Coord Node Primary Replica Memory Disk │
│ │ │ │ │ │ │ │
│ │──写入请求──►│ │ │ │ │ │
│ │ │──路由转发──►│ │ │ │ │
│ │ │ │──写入Buffer─►│ │ │ │
│ │ │ │──写入Translog───────────────────────►│ │
│ │ │ │──同步副本──►│ │ │ │
│ │ │ │ │──写入────►│ │ │
│ │ │◄──写入确认──│◄──确认──────│ │ │ │
│ │◄──成功响应──│ │ │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ (1秒后) │ │ │ │
│ │ │ │──Refresh──────────────►│ │ │
│ │ │ │ │ │ Segment │ │
│ │ │ │ │ │ (Cache) │ │
│ │ │ │ │ │ │ │
│ │ │ (30分钟后) │ │ │ │
│ │ │ │──Flush───────────────────────────►│ │
│ │ │ │ │ │ │ │
│ │ │ │ │ │ │Disk│
│ │
└─────────────────────────────────────────────────────────────────────────┘
数据恢复机制
节点崩溃后的恢复
┌─────────────────────────────────────────────────────────────────────────┐
│ 数据恢复流程 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 节点崩溃后重启: │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 1. 读取最后一个 Commit Point │ │
│ │ • 确定哪些 Segment 已持久化 │ │
│ │ │ │
│ │ 2. 加载已持久化的 Segment │ │
│ │ • 从磁盘读取 Segment 文件 │ │
│ │ • 这些数据是安全的 │ │
│ │ │ │
│ │ 3. 重放 Translog │ │
│ │ • 从 Commit Point 之后开始重放 │ │
│ │ • 恢复未 Flush 的数据 │ │
│ │ │ │
│ │ 4. 恢复完成 │ │
│ │ • 数据完整恢复 │ │
│ │ • 开始接受新请求 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ 重要: │
│ • Translog 保证了数据不会丢失 │
│ • 恢复时间取决于 Translog 大小 │
│ • 定期 Flush 可以减少恢复时间 │
│ │
└─────────────────────────────────────────────────────────────────────────┘
总结
本章详细解析了 ES 文档写入流程:
| 操作 | 触发时机 | 目标 | 作用 |
|---|---|---|---|
| 写入 Buffer | 实时 | 内存 | 快速接收数据 |
| 写入 Translog | 实时 | 磁盘 | 数据可靠性 |
| Refresh | 1秒 | OS Cache | 近实时搜索 |
| Flush | 30分钟 | 磁盘 | 数据持久化 |
核心要点:
- 写入即持久:数据同时写入 Buffer 和 Translog
- 近实时搜索:Refresh 实现约 1 秒可见
- 最终持久化:Flush 将数据真正写入磁盘
- 可靠性保证:Translog 支持崩溃恢复
参考资料
下一章预告
下一章将探讨 Segment 合并机制,包括:
- Merge Policy 详解
- 自动合并触发条件
- Force Merge 使用场景
- 合并对性能的影响
相关文章
Elasticsearch 底层原理系列(九):生产实践与性能调优
2020-12-31·6 分钟阅读
深入解析 Elasticsearch 生产环境最佳实践,包括硬件选型、JVM 调优、索引设计、性能监控与调优、常见问题排查。
Elasticsearch 底层原理系列(八):分布式一致性
2020-12-20·5 分钟阅读
深入解析 Elasticsearch 分布式一致性机制,包括 Master 选举、脑裂问题、故障检测与恢复、以及数据一致性保证。
Elasticsearch 底层原理系列(七):分片与路由机制
2020-12-13·5 分钟阅读
深入解析 Elasticsearch 分片路由机制,包括路由算法、自定义路由、分片数量规划、以及分片重平衡策略。