写入与存储机制

写入与存储机制 #

当你向 Easysearch 索引一个文档时,数据要经历多个阶段才能最终安全地持久化到磁盘。理解这个过程,有助于你在性能调优和故障恢复时做出正确决策。

写入的全景图 #

一个文档从写入请求到可被搜索再到持久化落盘,大致经历以下阶段:

客户端请求
    ↓
协调节点路由到主分片
    ↓
主分片写入:
  1. 追加到 translog(事务日志)
  2. 写入内存索引缓冲区(in-memory buffer)
    ↓
refresh(默认每秒):
  - 缓冲区内容写入新的段(segment)到文件系统缓存
  - 新段可被搜索(近实时)
  - 缓冲区清空,translog 保留
    ↓
flush(translog 超过 512MB 阈值时触发):
  - 段从文件系统缓存 fsync 到磁盘
  - 写入提交点(commit point)
  - translog 清空
    ↓
段合并(后台持续):
  - 小段合并为大段
  - 清理已删除文档
  - 减少段数量,提升搜索性能

下面按阶段逐一展开。

近实时搜索:为什么不是"实时" #

Easysearch 被称为 近实时(Near Real-Time, NRT) 搜索引擎——文档写入后并非立刻可搜索,但通常在一秒内就能被检索到。

背后的原因在于:磁盘 I/O 是瓶颈。提交(commit)一个新的段到磁盘需要 fsync,开销非常大。如果每索引一个文档就执行一次 fsync,性能将无法接受。

Easysearch 的做法是:将新段先写入文件系统缓存(OS page cache)——这一步代价很低;稍后再 fsync 到磁盘。只要新段进入了文件系统缓存,就可以像其他文件一样被打开和读取,从而对搜索可见。

这就是"近实时"的由来:文档写入后最多延迟一个 refresh 周期(默认 1 秒)即可被搜索到。

Translog:写入安全网 #

每次写入操作首先追加到 translog(事务日志)。即使在 refresh 和 flush 之间发生崩溃,重启后 Easysearch 会重放 translog 恢复数据。

  • 默认每次写请求完成后 fsync translog 到磁盘
  • 在主分片和副本分片上都会写入 translog
  • 客户端收到 200 OK 时,translog 已经持久化

translog 还提供实时的 CRUD:当你通过 ID 查询、更新、删除一个文档时,Easysearch 会先检查 translog 中是否有最近的变更,再去段里检索。这意味着它总是能获取到文档的最新版本。

内存缓冲区:等待刷新 #

文档同时被写入内存索引缓冲区(in-memory buffer)。此时文档还不可搜索,需要等待下一次 refresh。

Refresh:让文档可搜索 #

在 Easysearch 中,写入和打开一个新段的轻量过程叫做 refresh。默认情况下每个分片每秒自动刷新一次。

执行流程:

  1. 将缓冲区内容写入新的段到文件系统缓存(不是磁盘)
  2. 新段被打开,文档变为可搜索
  3. 缓冲区清空,translog 保留

refresh API #

可以手动触发 refresh:

POST /_refresh          // 刷新所有索引
POST /blogs/_refresh    // 只刷新 blogs 索引

手动 refresh 在写测试时很有用,但不要在生产环境每次索引文档后都手动 refresh。你的应用需要理解并接受 Easysearch 的近实时特性。

调整刷新频率 #

并非所有场景都需要每秒刷新。批量导入或日志场景可以降低刷新频率以提升写入性能:

PUT /my_logs
{
  "settings": {
    "refresh_interval": "30s"
  }
}

refresh_interval 支持动态更新。在大批量导入时,可以先关闭自动刷新,导入完成后再恢复:

PUT /my_logs/_settings
{ "refresh_interval": -1 }      // 关闭自动刷新

PUT /my_logs/_settings
{ "refresh_interval": "1s" }    // 恢复每秒刷新

⚠️ refresh_interval 的值需要带时间单位,例如 1s(1 秒)或 2m(2 分钟)。绝对值 1 表示 1 毫秒——会让集群陷入瘫痪。

Flush:真正的持久化 #

Flush 将段从文件系统缓存 fsync 到物理磁盘,执行一次完整的提交(commit):

  1. 所有在内存缓冲区的文档被写入一个新的段
  2. 缓冲区被清空
  3. 提交点(commit point)被写入磁盘,记录当前所有活跃的段
  4. 文件系统缓存通过 fsync 被刷新到磁盘
  5. 旧的 translog 被删除,创建新的 translog

Flush 默认在 translog 大小超过 512MBindex.translog.flush_threshold_size)时自动触发。Easysearch 没有基于时间的周期性 flush 定时器——flush 完全由 translog 大小驱动。

flush API #

POST /blogs/_flush
POST /_flush?wait_for_ongoing

通常不需要手动 flush。但在重启节点或关闭索引之前执行 flush 可以加速恢复——因为 translog 越短,恢复越快。

Translog 安全性 #

默认情况下 translog 在每次写请求完成之后执行 fsync(在主分片和副本分片都会发生)。对于大容量且偶尔丢失几秒数据也不严重的集群,可以使用异步 fsync:

PUT /my_index/_settings
{
    "index.translog.durability": "async",
    "index.translog.sync_interval": "5s"
}

⚠️ 异步 translog 意味着在发生 crash 时,可能丢失 sync_interval 时间段的数据。如果不确定后果,请使用默认的 "index.translog.durability": "request"

段合并:后台优化 #

由于每次 refresh 都会创建一个新的段,段数量会快速增长。而段数目太多会带来问题:每个段都消耗文件句柄、内存和 CPU 资源,更重要的是每次搜索都要轮流检查每个段,段越多搜索越慢。

Easysearch 通过在后台进行**段合并(merge)**来解决这个问题:

  • 小段被合并到大段,大段再被合并到更大的段
  • 已标记删除的文档在合并时被真正清理
  • 合并过程中不会中断索引和搜索
  • Easysearch 默认对合并流程进行资源限制,保证搜索性能不受影响

强制合并(Force Merge) #

对于不再更新的只读索引(例如按天滚动的历史日志索引),可以使用强制合并将每个分片合并为一个单独的段:

POST /logstash-2024-10/_forcemerge?max_num_segments=1

⚠️ 不要在活跃更新的索引上执行强制合并——后台合并流程已经能很好地完成工作。强制合并不受资源限制,可能消耗大量 I/O。

写入操作类型 #

操作说明内部行为
PUT /index/_doc/id索引(创建或覆盖)如果文档已存在,旧版本标记删除,新版本写入新段
POST /index/_doc自动生成 ID 的索引与上面相同,ID 由 Easysearch 生成
PUT /index/_doc/id/_create仅创建(不覆盖)如果 ID 已存在,返回 409 Conflict
POST /index/_update/id部分更新内部:读取 → 合并 → 重新索引整个文档
DELETE /index/_doc/id删除标记删除,段合并时真正清理
POST /index/_bulk批量操作按分片拆分并行执行,换行符分隔格式

文档的"不可变"本质 #

Easysearch 中有一个关键事实:

文档一旦写入段,就不会被修改。更新 = 标记旧版本删除 + 写入新版本。删除 = 标记删除 + 段合并时真正清理。

这种不可变性带来了并发安全、缓存友好和压缩优势,但也意味着频繁更新会产生大量需要合并的小段。

性能调优要点 #

场景建议
批量导入关闭 refresh(-1),增大 bulk 批次(5-15MB),导入完成后恢复
日志场景增大 refresh_interval(如 30s),减少段创建频率
高可靠性保持 translog 默认同步模式(request
容忍少量丢失可用异步 translog(async,5s 间隔)
只读索引执行 _forcemerge?max_num_segments=1 合并为单段

小结 #

阶段作用默认频率
Translog写入安全保障,崩溃恢复每次写请求
内存缓冲暂存文档,等待 refresh
Refresh让文档可搜索(近实时)每秒
Flush将段持久化到磁盘translog 超过 512MB 时
段合并减少段数量,清理删除后台持续
  • 写入先走 translog 保证安全,再走内存缓冲等待 refresh
  • Refresh 使文档可搜索(近实时),flush 使数据持久化到磁盘
  • 更新和删除不是原地修改,而是"标记删除 + 写新版本"
  • 段合并在后台自动清理删除标记、减少段数量

下一步可以继续阅读:

最佳实践 #