Milvus 是一款开源的特征向量相似度搜索引擎,在2020-03-11我们发布了版本0.7.0。在该版本中,Milvus 为存储系统添加了一个新组件— WAL(write-ahead logging,预写日志系统)。今天我们就来详细介绍一下相关背景和实现原理,以及如何能更好地使用它。

|背景

我们先回顾一下之前版本数据插入的流程:当用户往 Milvus 系统中插入数据时, Milvus 会先把数据攒在内存里,然后当内存数据达到一定规模(默认128M)或者定时器超时(默认1s)后,内存数据再排队刷入磁盘。当数据真正落盘后它们就能被访问了。这个设计借鉴了操作系统对流数据的处理方式,在写磁盘频率和数据可靠性之间做了一定的平衡。然而随着我们接触到越来越多的用户场景,以上设计渐渐不能满足需求了。首先,用户命令的原子性没得到保证,比如在系统意外退出时,用户某一次插入的数据可能部分落盘、部分丢失;其次,用户无法明确知道数据何时可见,即使在客户端等待1s也未必能保证之前插入数据全部可见;最后,当初设计时没有考虑数据删除,同步执行删除有可能要长时间阻塞用户线程。为了解决上述问题,我们在0.7.0 版本中引入了 WAL 组件。

|WAL 概要

WAL 的中文名是预写日志系统,其核心思想是把用户所有的修改操作(插入、删除)先写入日志中,然后再应用到系统状态里。一旦成功写完日志,即可通知用户操作成功。由于日志是以尾部追加方式写入,耗时较短,所以不会长时间阻塞用户线程。此外为防止意外退出导致数据丢失,系统重启时还会根据日志重做用户操作,以保证数据可靠性。

Milvus 的 WAL 设计也是如此。这里涉及了两个线程:一个是用户线程,另一个是后台线程。用户线程把修改操作写入 WAL 缓存和 WAL 文件后即可认为本次操作成功;然后由后台线程把 WAL 里的操作反映到系统状态里。当数据成功落盘后,系统会定期清理旧的 WAL 文件。整体架构和数据流参考下图。

| 数据一致性

WAL 通过 LSN(Log sequence number,日志序号)来实现数据的一致性。Milvus为用户的每次修改操作分配一个 LSN,这些 LSN 随用户的操作数严格递增。系统通过维护以下这些关键 LSN 点就能保证数据的一致性了:①用户操作的最大LSN;②已经反映到内存但还未落盘的最大LSN;③已经落盘的最大LSN。

用户的修改操作会不断更新①的位置;后台线程把用户操作反映到系统里的过程正是让②不断追上①的过程;同时定时写盘(Auto Flush)机制又让③不断追上②。所以当系统意外退出后,我们只要在下次重启期间比较①③位置就能方便地找到哪些用户操作需要重做。最后根据功能定位,那些③之前的信息已经没有意义了,它们随时可以被删除掉。下图是 WAL 内 LSN 系统的示意图(具体实现时每个集合(collection)会记录下各自集合相关的 LSN,具体逻辑一致就不再展开了)。

对于有实时读写要求的用户,Milvus 提供了同步接口:flush()。当用户调用 flush() 时,该用户线程首先会拿到当前系统的位置①并被阻塞住,直到系统内位置③与其重合才被唤醒并返回。这样待 flush() 返回时,其之前的所有操作都将可见。

flush 可以指定所有集合,也可以指定单个集合。如果用户指定单个集合,为尽可能短的阻塞用户线程,Milvus 内部还会调整执行顺序并优先执行 flush。下面例子是一种可能的调整结果,调整前后对用户效果等价,但用户线程的阻塞时间会明显减少。

|文件及缓存

通过以上介绍,WAL 可以被理解成一个任务队列,用户的修改操作会不断追加在队列尾部,而后台线程又不断从头部消费队列。整个队列需要实时写文件和逐渐清理旧文件。

可以预见,随着系统的运行这个队列有时会变空(后台线程消费完了而用户不再有新的修改操作),有时又会很大(用户一次性插入大量数据,为保证操作原子性必须等整个操作全部写入 WAL 后,后台线程才能开始消费)。而我们需要在不阻塞用户线程的情况下,让 WAL 运行在一个限定大小的内存空间里。

Milvus 在这里使用了双缓冲区。初始时读写指针指向同一片数组;当写指针写到数组尾部时,会判断读指针是否和自己共享同一片缓冲,若是则新数据写往另一片缓冲,否则当前缓冲从头写起;而读指针在追赶写指针时,发现读到缓冲尾部时需判断另一片缓冲的内容是否是当前内容的下一片,若是则从下一片缓冲头部读起,否则需要把下一片内容从磁盘加载到当前缓冲。具体分类示意图见下:

不难证明,该策略在最坏情况下每个 WAL 文件会有一次额外加载的性能损耗。而这也正是我们配置和使用中应该尽量避免的。

|配置和使用

关于 WAL,Milvus 为用户提供了以下几个配置参数:

  • enable

是否启用 WAL 组件,默认值 true。启动 WAL 会在一定程度上(取决于系统 IO)影响系统的插入性能,如果用户非常在意系统的插入性能可以选择关闭 WAL。即使关闭 WAL,flush 接口依旧有效。

  • recovery_error_ignore

系统重启恢复数据期间是否无视 WAL文件损坏,默认值 true。如果设置成 false,系统恢复数据若发现 WAL 文件损坏,将直接退出。

  • buffer_size

WAL 缓存大小,默认值256,单位 MB,合法区间 [64, 4096]按前文介绍,缓存大小会直接影响系统运行期间 WAL 文件是否有一次额外加载的性能损耗。如果用户有批量数据导入,建议把缓存值设为单批次导入数据量的2倍以上。

  • wal_path

WAL 路径,默认值 Milvus 数据路径下的wal目录用户可以使用默认路径。不过考虑到 WAL 有频繁的文件读写操作,如果硬件允许,用户可以把它指定在高速磁盘上。

|结语

WAL 是 Milvus 新版存储系统的一部分,通过它可以有效提高用户数据的可靠性。在实现上,我们让它尽可能短地阻塞用户线程以缩短系统响应时间。除此以外,Milvus 的其它功能和优化我们还在持续开发,欢迎各位继续为我们提供宝贵意见和建议。

欢迎加入 Milvus 社区

http://github.com/milvus-io/milvus| 源码

http://milvus.io| 官网

http://milvusio.slack.com| Slack 社区

http://zhihu.com/org/zilliz-11/| 知乎

http://zilliz.blog.csdn.net| CSDN 博客


Zilliz
154 声望829 粉丝

Vector database for Enterprise-grade AI