北川广海の梦

北川广海の梦

Bluge 文档写入过程

53
2023-09-13

之前分析了 zincsearch 对于一个文档写入的处理,bluge 作为其底层的索引库,其处理细节同样非常重要。本文将梳理一下 bluge 如何处理 document 写入操作。

基本概念

bluge 对外提供了一个 writer 对象,应用层可以以此对文档进行 crud 操作。

应用层可以一次性包含多个操作,多个操作会处于一个 batch 中。

一个 batch 中包含的新增或修改的文档,会被解析成 一个 segment

writer 在打开时,会启动三个 goroutine:

  • introducer:负责维护当前索引的一些状态。

  • persister:负责将 snapshot 文件或者 segment 文件保存到磁盘

  • merge:负责将已经写入磁盘的 segment 小文件进行合并,并写入磁盘

索引由 .snp 文件和 .seg 文件组成,.snp文件是当前索引状态的记录,包含了索引的 epoch 等信息,epoch 相当于索引的版本号,当索引变化时+1。.snp 中同时还记录了索引包含的 segment id list ,以及每个 segment 中被删除文档的 bitmap。seg 则是文档内容的存储。snp文件可能存在多个,但是当前生效的只会有一个,过期的 snp 和 seg 文件会由删除策略清理。

Document 写入过程

对于一个 batch,wirter 在尝试写入时,会先解析其内容,对于新增或修改文档的部分,会合并到一个 segment 对象中,对于删除的文档,则记录其id。之后会将更改通过 segmentIntroduction 对象,提交到introducer goroutine中。提交之后,当前 goroutine 会等待直到对应的操作被持久化。

introducer 接收到这个更改之后,会根据当前的 epoch+1,生成一个新的 snapshot,新的 snapshot 在生成过程中,会根据传入更改标记被删除的文档 id,同时会加入新传入的 segement。新的 snapshot 生成完毕之后,introducer 会将这个新的 snapshot 设置为当前快照,之后会广播这个 snapshot 的 epoch。

persister goroutine 内部维护了一个当前 已经持久化的 presist epoch,接收到 introducer 通知时,如果 epoch 大于 presist epoch,则说明需要处理持久化。此时会浅复制一份 当前的 snapshot,会将所有未持久化的 segment 保存到磁盘上。之后会 通知 introducer,更新当前快照,主要是更新哪些 segement 是已经持久化的以及一些统计信息。然后会将复制的 snapshot 进行持久化。所有持久化操作完毕后,广播自己的 presit epoch,通知 merge gourtine 进行处理。

值得注意的是,当 persister 每次被唤醒时,会先广播自己的 presit epoch, 如果 merge epoch 落后于 presit epoch,则会被唤醒,开始处理合并操作。之后 presister 会检查目录中文件数量。如果数量小于配置的文件数量(PersisterNapUnderNumFiles),那么会先尝试等待 merge epoch 追上 presit epoch 或者 等待一个 配置的时间 (DefaultPersisterNapTimeMSec,如果 merge epoch 已经追上,那么必定会等待这个配置的时间),默认为 0,zincsearch 将这个时间设置为 50ms。

否则,如果文件数量超过阈值, 会先尝试清理过期的文件,如果清理之后仍然超过配置的文件数量,则会等待 mrege 将当前 presit epoch 进行合并。在这个过程中,presiter goroutine 将被阻塞,直到 merge epoch 追赶上来。文件数量的默认配置为 1000,zincsearch采用的默认值。

这个阻塞过程中,上层的写入的每一个 batch 都会被作为 segment 一个正常提交到 introducer,并记录到当前快照中。 presister 恢复后,会将所有的 segment 与 当前快照持久化保存。之后再通知所有处于等待状态的上层 goroutine,并调用持久化完毕的回掉函数,在此之后,文档就被保存下来了,上层应用的写入完毕。

presister 在持久化时,如果前正在进行的的异步任务数量(numEventsBlocking)小于配置的阈值(MemoryPressurePauseThreshold),那么会直接会对内存中的 segment 进行内存中的合并,再直接写入磁盘。否则直接将所有未持久化的 segment 写入磁盘。这个配置的默认值是 int32.max, zincsearch 采用的是默认值。

merge goroutine 内部维护了一个 merge epoch,当被广播唤醒时,如果当前快照的 epoch 比 merge epoch 更新,那么 merge goroutine 会根据当前快照,读取对应磁盘中的 segment,尝试合并成新为新的一个或多个 segment,如果有需要合并的,合并完毕后的新的 segment 会被直接存储在磁盘中,之后会通知 introducer 更新当前快照中 segment 的信息。

总结

一个 document 的写入,会先被解析为一个 segment,之后交给 introducer 来更新当前的快照信息,之后 prisister 会负责将 当前的快照和 segment 文件 写入到磁盘,merge 则会尝试对磁盘中的 segment 进行合并。

其 segement 被设计为与 es 一样的不可变设计,缺点是也会带来大量的 segment 文件消耗过多文件句柄资源等问题。 bluge 的设计在一定程度上平衡了内存负载、磁盘压力、吞吐量,例如 :当 segment 文件过多时,会减慢持久化的速度,等待 segment 先被合并,或者在内存压力不高时直接先进行合并再持久化 ,用户可以根据自身应用的实际情况,调整对应的参数更好的满足实际使用场景。