pinterest 插画网站pinterest( 二 )


在服务重启的情况下,可以通过重播来自 Kafka 的消息来重建各个实时段 。
索引刷新索引刷新是将内存中的数据从一个实时段持久存储到一个压缩索引文件中的过程 。当一个实时段被密封时将自动触发一次刷新,并且还可以使用调试命令手动触发刷新 。

索引刷新是一种有益的运算符,可确保数据持久性,这样我们就无需在重新启动期间从头开始重建内存中的段 。此外,通过压缩的不可变索引,刷新减少了一个段的内存占用,并提高了服务效率 。
索引压缩随着时间的流逝,生成的多个小段会影响服务性能 。为了克服这个问题,我们引入了一个后台压缩线程来将这些小段合并为更大的段 。由于删除运算符只是将文档标记为已删除,而不是物理删除它们,因此压缩线程还会保留这些已删除/过期的文档 。

在每个刷新和压缩运算符之后,将生成一个由所有静态段组成的新索引清单 。一些 Kafka 偏移量(用作检查点)也被添加到每个清单中 。根据这些检查点,服务就能知道重新启动后在哪里消费消息 。
设计细节在本节中,我们将更具体地介绍几个关键领域 。我们从最有趣的部分开始,即并发模型 。
并发模型如前所述,实时段是我们需要同时处理读取和写入操作的唯一可变组件 。不幸的是,那些开源项目采用的近实时方法无法满足我们的业务需求 。相比之下,我们选择了另一种方法,使我们能够在添加到索引后立即提交文档,而无需等待索引刷新 。为了提升性能,我们针对数据结构采用了一个无锁技术,以适应我们的使用状况 。现在来深入到细节吧!
实时段每个实时段都包含一个倒排索引和一个正排索引 。倒排索引在逻辑上是从 term 到发布列表(用于检索的文档 ID 列表)的映射 。同时,正排索引存储一个用于完整评分和数据提取的任意二进制 Blob 。我们只关注实时倒排索引部分,与正排索引相比,它更有趣且更具挑战性 。

从高层次上讲,实时段和静态段之间的主要区别是可变性 。对于实时倒排索引,从 term 到发布列表的映射必须是并发的 。folly 的并发哈希图等开源项目为此提供了很好的支持 。我们更关心的是发布列表的内部表示,它可以有效地支持我们的并发模型 。
仅附加向量一般来说,单写入者/多读取者模型效率更高,推理起来也更容易 。我们选择了与 HDFS 类似的数据模型,它具有仅附加的无锁数据结构 。我们来仔细研究一下读取者和写入者之间的互动方式 。

pinterest 插画网站pinterest

文章插图
写入者将文档 ID 附加到向量中,然后提交大小(size)以使读取者可以访问它读取者在访问数据之前获取一个快照(最大到提交的大小)
pinterest 插画网站pinterest

文章插图
为了避免随着发布列表的增长而产生的内存复制开销,我们在内部将数据作为一个存储桶列表来管理 。当我们的容量用完时,只需添加一个新的存储桶即可,无需接触旧的存储桶 。另外,通常搜索引擎使用跳过列表来加快跳过运算符的速度 。由于采用了这种格式,我们可以方便地支持一个单级跳过列表,这对于实时倒排索引已经足够了,因为它的大小通常很小 。
文档原子性现在有了仅追加的向量,我们就可以实现单个发布列表的原子性 。但是,文档可以包含一个 term 列表,并且我们最终可能会返回带有部分更新索引的意外文档 。为了解决这个潜在的问题,我们引入了一个文档级别提交,以保证文档的原子性 。在服务管道中使用了一个额外的过滤器来确保仅返回已提交的文档 。

说到文档原子性,文档更新是这里值得一提的另一种情况 。对于每次文档更新,我们特意将其转换为两个运算符:添加新文档,然后从索引中删除旧文档 。尽管每个运算符都是原子的,但加在一起我们就不能保证原子性了 。我们认为可以在很短的时间窗口内返回旧版本或新版本,但尽管如此,我们还是在服务管道中添加了重复数据删除逻辑,以在同时返回新旧版本时过滤掉旧版本 。
写缩放一个自然而然的问题是,如果你的数据结构仅支持一次写入和多次读取并发模型,那么如果单个线程不能及时处理所有写入操作该怎么办?盲目添加更多分片只是为了扩展写入吞吐量,这似乎不是一个好主意 。虽说这是一个确实存在的担忧,但在我们的设计中已经考虑到了这一点 。