AICon 深圳站聚焦 Agent 技术、应用与生态,大咖分享实战干货 了解详情
写点什么

Pinterest 搜索系统实时化的挑战和建设实践

  • 2021-03-30
  • 本文字数:4826 字

    阅读完需:约 16 分钟

Pinterest搜索系统实时化的挑战和建设实践

Pinterest 的内部搜索引擎 Manas 是一个通用的信息检索平台。正如我们在上一篇文章中讨论的那样,Manas 被设计为兼具高性能、可用性和可伸缩性的搜索框架。如今,Manas 支持大多数 Pinterest 产品的搜索功能,包括广告、搜索、Homefeed、Related Pins、Visual 和 Shopping。

 

搜索系统的关键指标之一是索引延迟,也就是更新搜索索引以反映更改所花费的时间。随着我们系统的功能不断增加,新的用例持续引入,即时索引新文档的能力变得越来越重要。Manas 之前已经支持了增量索引,能够提供数十分钟数量级的索引延迟。不幸的是,这还不能满足我们来自广告和 following feeds 持续增长的业务需求。我们决定在 Manas 中构建一个新模块,以进一步将索引延迟减少到几分之一秒的水平。

 

在这篇博客文章中,我们描述了这一系统的架构及其主要挑战,并介绍了我们所做权衡的细节内容。


本文由 Michael Mi 发表在 medium.com,经授权由 InfoQ 中文站翻译并分享

挑战

新的需求伴随着新的挑战。以下是我们面临的几个主要挑战。

索引延迟

对于 Lucene、Vespa 等开源项目来说,小批(tiny batch)方法(又称近实时)是最受欢迎的选择。使用这种方法,只有在调用索引提交时才可以搜索新编写的文档。结果,你需要在索引延迟和吞吐量之间进行权衡。不幸的是,我们无法利用这种方法将索引延迟减少到几秒钟级别。

索引刷新能力

实时服务的缺点之一是缺乏索引刷新敏捷性。对于一个批处理管道来说,重新运行索引作业以立即获取所有模式更改是很简单。但当涉及到实时服务管道时,实现高效的索引刷新支持就是一件很复杂的事情了。

为不断变化的数据实现扩展

为了避免过度配置,系统采用了自动缩放以根据实际查询负载来调整副本。如果索引是不可变的,那么新副本创建起来就相对容易:你只需将索引复制到新节点即可。困难之处在于处理不断变化的索引:如何确保所有副本都具有相同的索引?

错误恢复

Manas 是一项数据密集型服务,其中每台主机可提供的索引高达数百 GB。Manas 也是一个有状态的系统,一个错误的二进制文件可能会导致连回滚都无法解决的数据问题。我们需要构建一个同时支持容错和错误恢复的系统,以便从二进制错误和数据损坏中恢复。

从静态到实时

我们来简要介绍一下常规静态服务和实时服务之间的区别。如上图所示,实时服务的主要工作是将索引管道从离线迁移到在线。

 

对于静态服务,索引是通过一个批处理工作流离线生成的,然后将它们复制到 Leaf 用以在线服务。对于批处理工作流,由于高昂的框架开销,几乎不可能在几分之一秒内建立可服务的索引。实时服务不是使用脱机工作流,而是在服务中即时处理所有写入。此外,实时索引管道用的是与静态索引管道相同的索引格式来处理写入,从而使我们能够重用整个索引读取逻辑。记住这一点,我们来继续了解实时服务的工作机制。

索引接口

我们不是直接使用 RPC,而是使用了 Kafka 作为我们的高写入吞吐流。Leaf 服务器不断拉取突变以建立增量索引。事实证明,这一决策以多种方式极大简化了我们的系统:

 

  • 数据复制和写入失败由 Kafka 负责。

  • 借助回查能力,Kafka 队列也可以用作WAL

  • 在每个分区中都有严格的顺序保证,系统可以随意应用删除操作,而不必担心正确性。

架构概述

由于服务逻辑可以通过共享索引格式重用,因此我们将重点放在索引数据流上。

 

本质上,实时 Manas leaf 是一个LSM引擎,它将随机 IO 写入转换为顺序 IO,并为读取放大和写入放大应用程序提供高效的服务。如下所示,整个索引流程包括三个关键步骤。我们来一一讨论。

实时段构建

除了现有的静态段(segment)外,我们还引入了实时段。如上所示,系统中有两种实时段:活动实时段和密封(sealed)实时段。

 

  • 活动实时段是唯一可变的组件,用于累积从 Kafka 拉取的突变(添加/删除)。值得一提的是,将一个文档添加到一个实时段后,在文档级别提交后即可立即搜索。

  • 一旦活动实时段达到一个可配置的阈值,它就会被密封,转为不可变并放入一个刷新队列中。同时,系统创建了一个新的活动实时段以继续累积突变。

 

在服务重启的情况下,可以通过重播来自 Kafka 的消息来重建各个实时段。

索引刷新

索引刷新是将内存中的数据从一个实时段持久存储到一个压缩索引文件中的过程。当一个实时段被密封时将自动触发一次刷新,并且还可以使用调试命令手动触发刷新。

 

索引刷新是一种有益的运算符,可确保数据持久性,这样我们就无需在重新启动期间从头开始重建内存中的段。此外,通过压缩的不可变索引,刷新减少了一个段的内存占用,并提高了服务效率。

索引压缩

随着时间的流逝,生成的多个小段会影响服务性能。为了克服这个问题,我们引入了一个后台压缩线程来将这些小段合并为更大的段。由于删除运算符只是将文档标记为已删除,而不是物理删除它们,因此压缩线程还会保留这些已删除/过期的文档。

 

在每个刷新和压缩运算符之后,将生成一个由所有静态段组成的新索引清单。一些 Kafka 偏移量(用作检查点)也被添加到每个清单中。根据这些检查点,服务就能知道重新启动后在哪里消费消息。

设计细节

在本节中,我们将更具体地介绍几个关键领域。我们从最有趣的部分开始,即并发模型。

并发模型

如前所述,实时段是我们需要同时处理读取和写入操作的唯一可变组件。不幸的是,那些开源项目采用的近实时方法无法满足我们的业务需求。相比之下,我们选择了另一种方法,使我们能够在添加到索引后立即提交文档,而无需等待索引刷新。为了提升性能,我们针对数据结构采用了一个无锁技术,以适应我们的使用状况。现在来深入到细节吧!

实时段

每个实时段都包含一个倒排索引和一个正排索引。倒排索引在逻辑上是从 term 到发布列表(用于检索的文档 ID 列表)的映射。同时,正排索引存储一个用于完整评分和数据提取的任意二进制 Blob。我们只关注实时倒排索引部分,与正排索引相比,它更有趣且更具挑战性。

 

从高层次上讲,实时段和静态段之间的主要区别是可变性。对于实时倒排索引,从 term 到发布列表的映射必须是并发的。folly 的并发哈希图等开源项目为此提供了很好的支持。我们更关心的是发布列表的内部表示,它可以有效地支持我们的并发模型。

仅附加向量

一般来说,单写入者/多读取者模型效率更高,推理起来也更容易。我们选择了与 HDFS 类似的数据模型,它具有仅附加的无锁数据结构。我们来仔细研究一下读取者和写入者之间的互动方式。

  • 写入者将文档 ID 附加到向量中,然后提交大小(size)以使读取者可以访问它

  • 读取者在访问数据之前获取一个快照(最大到提交的大小)

为了避免随着发布列表的增长而产生的内存复制开销,我们在内部将数据作为一个存储桶列表来管理。当我们的容量用完时,只需添加一个新的存储桶即可,无需接触旧的存储桶。另外,通常搜索引擎使用跳过列表来加快跳过运算符的速度。由于采用了这种格式,我们可以方便地支持一个单级跳过列表,这对于实时倒排索引已经足够了,因为它的大小通常很小。

文档原子性

现在有了仅追加的向量,我们就可以实现单个发布列表的原子性。但是,文档可以包含一个 term 列表,并且我们最终可能会返回带有部分更新索引的意外文档。为了解决这个潜在的问题,我们引入了一个文档级别提交,以保证文档的原子性。在服务管道中使用了一个额外的过滤器来确保仅返回已提交的文档。

 

说到文档原子性,文档更新是这里值得一提的另一种情况。对于每次文档更新,我们特意将其转换为两个运算符:添加新文档,然后从索引中删除旧文档。尽管每个运算符都是原子的,但加在一起我们就不能保证原子性了。我们认为可以在很短的时间窗口内返回旧版本或新版本,但尽管如此,我们还是在服务管道中添加了重复数据删除逻辑,以在同时返回新旧版本时过滤掉旧版本。

写缩放

一个自然而然的问题是,如果你的数据结构仅支持一次写入和多次读取并发模型,那么如果单个线程不能及时处理所有写入操作该怎么办?盲目添加更多分片只是为了扩展写入吞吐量,这似乎不是一个好主意。虽说这是一个确实存在的担忧,但在我们的设计中已经考虑到了这一点。

用于数据结构的一次写入和多次读取并发模型并不意味着我们不能使用多个线程进行写入。我们计划使用 term 分片策略来支持具有多个线程的写入。如上图所示,对于具有 term 列表的给定文档,每个 term 将始终映射到固定线程,以便为单次写入和多次读取定制的所有数据结构都可以无限制地直接重用。

索引刷新

索引刷新功能是我们产品的一项关键特性,可实现快速周转并提高开发速度。一般可以使用两种方法以高效刷新索引,它们分别是动态回填和从离线构建的索引恢复。

回填索引

我们提供了以合理的吞吐量回填文档的功能。为了避免影响生产的新鲜度,我们需要一个优先级较低的单独流来处理回填流量。结果,两个流中可能会存在文档的两个版本,而旧版本将覆盖新版本。为了克服这个问题,我们需要在实时索引管道中引入一种版本控制机制和一个冲突解决程序,以决定哪个版本更新鲜。

从离线构建索引中恢复

有时,以给定的速度对整个数据集进行回填会非常耗时。我们支持的另一种更快的索引刷新方法是离线构建索引,然后使用离线构建索引和 Kafka 流之间的同步机制来从离线索引中恢复索引。

故障转移和自动扩展

出于各种原因,我们有时会需要启动新实例,例如故障转移和自动缩放等。对于静态服务,使用从索引存储下载的不变索引来启动新实例是很容易的。但是,对于具有不断变化的索引的实时服务而言,这就变得很复杂了。我们如何确保新实例最终具有与其他实例相同的索引副本呢?


我们决定使用基于 Leader 的复制,如上图所示。


我们的流程如下所示:

 

  1. Leader 定期拍摄新快照并将其上传到持久索引存储中

  2. 默认情况下,新实例从索引存储下载最新的快照

  3. 新实例根据快照索引中的检查点恢复消费来自 Kafka 的消息

  4. 一旦新实例赶上进度,便开始为流量提供服务

 

这种设计中有一些关键点值得一提:

Leader 选举

Leader 的唯一职责是拍摄快照并定期上传索引。这意味着我们可以在较短的时间内(最多几个小时)无 Leader 或有多个 Leader。因此,我们在选择 Leader 选举算法方面具有一定的灵活性。为简单起见,我们选择使用集群维护作业来静态地选举一个 Leader,在此我们会定期检查我们是否有一个好的 Leader。

快照上传

通常,新实例仅连接到 Leader 以下载最新快照。在这种方法中,从新实例下载快照可能会使 Leader 过载,从而导致级联故障。相反,我们选择将快照定期上载到索引存储,牺牲存储空间和新鲜度以保持稳定性。此外,上载的快照对于错误恢复很有用,稍后将对此介绍。

错误恢复

如上所述,错误恢复是实时服务系统的另一挑战。我们需要处理一些涉及数据损坏的特定场景。

输入数据损坏

我们使用 Kafka 作为输入写入流;不幸的是,这些消息是不可变的,因为生产者只能在其上附加消息,而不能更改现有消息的内容。这意味着一旦将数据损坏引入 Kafka 消息中,它将是永久性的。多亏了上传的快照,我们能够将索引回退到不损坏的状态,跳过损坏的消息,然后使用这个修复来消费新消息。

二进制错误导致数据损坏

尽管我们拥有成熟的静态集群索引验证管道,以确保在换入新版本之前新索引和新二进制文件均不会出现问题,但仍有一些错误会潜入生产环境。幸运的是,我们可以通过回滚二进制或索引来解决此问题。对于实时服务而言,回滚二进制文件无法回滚索引中的错误,这带来了更大的麻烦。使用快照上传机制,我们可以将二进制文件与回退的索引一起回滚,然后从 Kafka 重放消息以修复索引中的错误。

下一步计划

随着越来越多的场景加入 Manas,我们需要不断提高系统的效率、可伸缩性和能力。我们路线图中的一些有趣的项目如下:

 

  • 共同托管静态和实时集群以简化我们的服务栈

  • 优化系统以支持大型数据集

  • 构建一个基于通用嵌入的检索以支持高级场景

 

致谢:这篇文章总结了几个季度的工作,涉及多个团队。感谢 Tim Koh、Haibin Xie、George Wu、Sheng Chen、Jiacheng Hong 和 Zheng Liu 的无数贡献。感谢 Mukund Narasimhan、Angela Sheu、Ang Xu、Chengcheng Hu 和 Dumitru Daniliuc 所做的许多有意义的讨论和反馈。感谢 Roger Wang 和 Randall Keller 的出色领导。

 

原文链接:Manas Realtime — Enabling changes to be searchable in a blink of an eye

2021-03-30 09:001951

评论

发布
暂无评论
发现更多内容

filecoin矿工的收益有哪些?

fil fil收益 ipfs挖矿

互联网产品经理之需求的一生

路边水果摊

产品经理

台达DOP-100系列触摸屏(LUA程序编写用户管理应用)

林建

lua 台达 触摸屏 用户管理 DOP-100

PHA项目挖矿平台系统开发App

获客I3O6O643Z97

挖矿矿池系统开发案例 PHA矿机挖矿 PHA质押挖矿

Go 学习笔记之 函数

架构精进之路

Go 语言 7月日更

我写什么,你们决定

喵叔

从装大象中我们学会了什么设计模式

skow

Java 面试 后端 设计模式

前端智能化 or 低代码,也许不是个选择题

清秋

大前端 低代码 智能化

揪出那个无主键的表

Simon

MySQL 主键

铂金10:能工巧匠-ThreadLocal如何为线程打造私有数据空间

MetaThoughts

Java 后端 多线程 并发

免费分享Spring与SpringMVC开发的优秀图书

Java入门到架构

Java spring Java书籍推荐

云图说|ASM灰度发布,让服务发布变得更敏捷、更安全

华为云开发者联盟

灰度发布 application 云图说 应用服务网格服务 Service Mesh (ASM)

淘悠优软件系统开发内容

NNB牛气冲天系统软件开发搭建

由浅入深C A S

程序猿阿星

CAS 自旋锁

10万QPS,K6、Gatling和FunTester对比测试

FunTester

性能测试 接口测试 测试框架 压力测试 测试开发

英特尔中国研究院宋继强:AI技术已成为推动数字化转型的超级力量|WAIC 2021

E科讯

一文看懂filecoin挖矿的成本到底有哪些?

IPFS fil成本 fil挖矿

有哪些适合大型系统的项目开发管理工具?

万事ONES

项目管理 研发管理 ONES

有哪些好用的团队文档和技术资料管理的工具?

万事ONES

在线文档 ONES 协同办公

数牍科技亮相上海 AI 基金“AI 驱动企业转型” 应用场景战略合作仪式,隐私计算拓展AI应用疆域

玩转Spring Boot Actuator集成,基操,勿六

白亦杨

Java

DOGT狗狗通证软件系统开发公司

Takin Talks·上海 |开源后首场主题研讨会来了,一起解密Takin技术吧!

TakinTalks稳定性社区

目前有哪些好用的用例管理工具?

万事ONES

测试用例 ONES 测试管理

智能猫量化机器人炒币系统开发【专业定制、现成源码】

获客I3O6O643Z97

DAPP智能合约交易系统开发 量化策略 量化跟单 量化交易源码

全面解读自动驾驶数据存储关键

焱融科技

人工智能 自动驾驶 云计算 高性能 文件存储

细说.NET 缓存

喵叔

7月日更

【LeetCode】 H 指数 IIJava题解

Albert

算法 LeetCode 7月日更

详解 nebula 2.0 性能测试和 nebula-importer 数据导入调优

NebulaGraph

数据库 开源 图数据库

鸿蒙轻内核源码分析:掌握信号量使用差异

华为云开发者联盟

鸿蒙 数据结构 信号量 结构体 OpenHarmony

Pinterest搜索系统实时化的挑战和建设实践_语言 & 开发_Pinterest Engineering_InfoQ精选文章