Facebook 有序队列服务设计原理和高性能浅析

【CSDN 编者按】作为全球最领先的社交网络,Facebook的高性能集群系统承担了海量数据的处理,它的服务器架构一直为业界人所关注。

作者 | Coder的技术之路       

责编 | 欧阳姝黎

出品 | CSDN(ID:CSDNnews)

前言

Facebook 生态系统是由成千上万的分布式系统和微服务驱动构成的,其中许多服务都得益于异步作业,特别是在在线流量的高峰时期。异步化提供了诸多好处:更有效地利用资源、提高系统可靠性、允许计划执行,以及微服务彼此间可靠通信。实现这些优势都需要一个队列——一个存储作业的地方,允许其异步发生,或者从一个服务传递到另一个服务。facebook 有序队列服务 FOQS 应运而生。

FOQS 在 Facebook 上支持数百个服务,包括:

- Async (Facebook 的异步计算平台),是Facebook上广泛使用的通用异步计算平台。它提供了各种功能,从通知到完整性检查,再到为任务计划执行,利用 FOQS 的能力来存储大量作业的积压,推迟作业运行,从而达到削峰填谷。

- 视频编码服务,支持异步视频编码服务。当视频被上传时,它们被分解成多个组件,每个组件存储在 FOQS 中,然后进行处理。

- 语言翻译技术,为语言间的帖子翻译提供了支持。这种工作在计算上可能非常昂贵,通过将其分解为多个作业,存储在 FOQS 中,并由 workers 并行运行而从并行化中获益等。

facebook engineering[1]

构建分布式优先队列

FOQS 的主要能力是存储位于 namespace 中的 topic 中的 item。它公开了一个 Thrift API,包含以下操作:

  • Enqueue

  • Dequeue

  • Ack

  • Nack

  • GetActiveTopics

FOQS 通过内部服务 Shard Manager 来管理对主机的分片分配。每个分片分配给一台主机。为了更容易地与其他后端服务通信,FOQS 实现了 Thrift 接口。下面来分别介绍各部分的原理和设计:

Item

item 是 FOQS 中优先队列的消息,其中包含用户指定的数据。一般来说,它由以下字段组成:

  • Namespace  FOQS的多租户单元

  • Topic 即一个优先队列;  一个 namespace可以包含许多(数千个) topics.

  • Priority (用户指定的32位整数), 数值越小优先级越高。

  • Payload 不可变二进制大对象,大小可以到10kb。开发人员可以自由地在这里放置他们想要的任何东西。

  • Metadata 可变二进制对象。开发人员可以自由地在这里放置他们想要的任何东西。通常,元数据应该只有几百字节。

  • Dequeue delay — Item应该从队列中退出的时间戳。这也称为deliver_after.

  • Lease duration 一个Item需要被消费者 ACK 或者 NACK 而出队列的持续时间,如果消费者什么都没有做,则 FOQS 可以根据客户指定的重试策略(至少一次、最多一次和最大重试计数)重新投递Item。

  • FOQS-assigned unique ID 用于通过 API标识一个Item.

  • TTL 限制 Item 在队列中的驻留时间。一旦一个 Item 的生存时间(TTL)被命中,它将被删除。

「FOQS 中的每个 Item 对应于 MySQL表中的一行。在进入队列时,会给一个 Item 分配一个 ID。」

topic

一个 topic 就是一个逻辑优先队列,一般是一个字符串,由用户指定。它包含item,并按它们的优先级和 deliver_after 值对它们进行排序。主题是廉价且而且是动态变动的,只需将 item 排队并指定 topic 标识就可以创建 topic。

由于 topic 是动态的,FOQS 为开发人员提供了一个 API,通过查询活动 topic(至少包含一个 item)来发现 topic。当一个 topic 没有更多的 item 时,它就不再存在。

namespace

一个 namespace 和一个队列用例相匹配。它是 FOQS 的多租户单位。每个 namespace 都有一定的容量保证,以每分钟的队列数量衡量。命名空间可以共享同一列(一列是 FOQS 主机和 MySQL 分片的集合,为一组命名空间提供服务),且不相互影响。命名空间只映射到一个列。

Enqueue

Enqueues 是 item 进入 FOQS 的入口。如果成功进入队列,则会执行持久化,最终出队列。

当一个入队请求到达 FOQS 主机时,请求被缓冲下来并返回一个 promise。每个 MySQL 分片都有一个对应的 worker,它从缓冲区中读取 item 并将它们插入到 MySQL 中。一个数据库行对应一个 item。一旦插入完成(成功或失败),promise就会完成实现,并将队列响应发送回客户机。如下图所示:

FOQS 使用熔断设计模式来标记不健康的分片。其健康状况由慢查询(滚动窗口上平均毫秒数大于 x ms)或错误率(滚动窗口上平均错误数大于 x%)定义。如果分片被判定为不健康,worker 将停止工作,直到分片健康。这样,FOQS 就不会继续向已经不健康的分片添加新 item 了。

如果插入成功,enqueue API 返回一个项目的唯一 ID。该 ID 是一个字符串,包含分片 ID 和分片中的 64 位主键。这种组合唯一地标识了 FOQS 中的每一项。

Dequeue

dequeue API 的入参是(topic, count)的参数对的集合。对于每个 topic,FOQS 最多会返回对该 topic 的 count 个 item。这些 item 是按优先级和 deliver_after 排序的,因此优先级较低的物品将首先被交付。如果多个 item 的优先级最低,较低的 deliver_after(即较老的)item 将首先交付。

队列 API 允许指定项目的过期期限。当一个 item 出队列时,它的过期判定也会开始。如果 item 没有在期限内被 ack 或被 nack,它可以被重投。这是为了避免下游消费者在 ack 或 nack item 之前崩溃时丢失 item。FOQS 支持至少一次和最多一次的投递。如果一个 item 最多投递一次,则在过期时间到期后将其删除;如果至少一次,将尝试重新投递。

由于 FOQS 支持优先级,每台主机需要在它关联的分片上做一个 reduce 操作,以找到优先级最高的 item。为了优化,FOQS 维护了一个叫做预取缓冲区(Prefetch Buffer)的数据结构,它在后台运行,从所有分片中取优先级最高的 item,然后进行缓存,以便客户端从队列中取出。

每个分片维护一个按优先级排序的,准备投递的 item 主键的 内存索引。该索引被所有可能标记一个 item 已经准备好投递的操作(如 enqueues)进行更新。并允许预取缓冲区通过 k-way merge 和 select 查询来高效地找到优先级最高的主键。这些 item 的状态在数据库中也被更新为“已投递”,避免重复投递。

预取缓冲区(Prefetch Buffer)通过存储每个 topic 的客户端请求(出队率)来补充自身。预取缓冲区(Prefetch Buffer)将以与客户端请求成比例的速率请求 item。快速出队的 topic 将获得更多的 item 放入预取缓冲区。

dequeue API 只是从预取缓冲区读取项目并将它们返回给客户机:

Ack/Nack

ack 表示该 item 已退出队列并已成功处理,不需要再次发送。

nack 表示一个 item 应该被重新投递,因为客户端需要再次处理。当一个项被 NACK 时,是可以延迟处理的,允许客户端在处理失败的 item 时利用指数后退。此外,客户端可以在 nack 上更新该 item 的元数据,以便在该 item 中存储部分结果。

因为每个 MySQL 分片最多属于一个 FOQS 主机,一个 ack/nack 请求需要落在分片对应的主机上。由于 shard ID 编码在每个 item ID 中,FOQS 客户端使用 shard 来定位主机。这个映射通过 Shard Manager 查找。

一旦 ack/nack 被路由到正确的主机,它就会被发送到特定分片的内存缓冲区。worker 从 ack 缓冲区中取出 item,然后从 MySQL 分片中删除这些行; 类似地,worker 从 nack 缓冲区中提取 item。但不是删除,而是使用新的 deliver_after 时间和元数据(如果客户端更新了它)更新 item。如果 ack 或 nack 操作因为任何原因丢失,例如 MySQL 不可用或 FOQS 节点崩溃,这些 item 将被考虑在租约到期后重新投递。

Push vs. Pull

FOQS 提供了一个基于拉的接口,消费者使用 dequeue API 来获取可用数据。为了理解在 FOQS API 中提供拉模型背后的动机,我们看看使用 FOQS 的作业的多样性。它包括以下特征:

  • 端到端延迟处理的需要:端到端处理延迟,是指 item 从准备好到被消费者从队列中拉取消费所经历的时间。快速消费和缓慢消费的作业混在一起。有的可以被毫秒级消费,而有的会延迟好几天。

  • 处理速率 :  topic 对于 item 的消费速率可能是不同的(每分钟 10 个 item 到每分钟1000 多个 item)。但是,根据下游资源在特定时间的可用性,可能有别于它们日常的处理速度。

  • 优先级: topic 级别或 topic 内单个 item 级别的处理优先级不同。

  • 处理的位置 : 某些 topic 和 item 需要在特定的区域进行处理,以确保它们与正在处理的数据的关联性。

FOQS 的大规模实践

FOQS 在过去几年中经历了指数级的增长,现在每天处理近一万亿件产品。而处理的积压订单已经达到数千亿项,反映了系统处理能力普遍欠缺。为了处理这种规模,我们必须实现一些优化。检查点 CheckPointing

FOQS 专门设置有后台线程,来运行比如延迟的 item 准备投递、租约过期和清除过期的 item,这些操作依赖于记录行中的时间戳字段。

比如,如果我们想更新所有准备交付的 item 的状态,来标识它们已经准备好投递,则需要一个查询:

where timestamp_column <= UNIX_TIMESTAMP() for update

对所有行进行更新。

这种查询的问题是 MySQL 需要用时间戳≲now 锁定对所有行更新(不仅仅是符合条件的那些记录)。、历史越长,读取查询就越慢。

通过 checkpoinging,FOQS 在查询上维护了一个下界(最后处理的已知时间戳),它限定了 where 子句。where 子句变成:

WHERE <checkpoint> <= timestamp_column AND timestamp_column <= UNIX_TIMESTAMP()

通过在两边绑定查询,表示历史记录的行数就会更少,从而使读取(和更新)的总体性能更好。

灾备

Facebook 的基础设施需要能够承受一整个数据中心发生异常。所以,每个 FOQS MySQL 分片被复制到两个冗余的灾备集群。跨区复制是异步的,但是 MySQL binlog 以同步的方式持久化到同一区域的另一个灾备集群中。

如果数据中心需要被清空(或者 MySQL 数据库正在进行维护),MySQL 主数据库将暂时处于只读模式,直到副本能够和主节点同步。

这通常需要几毫秒。一旦副本和主节点数据达到一致,副本就被提升为主节点。

而这时会变成 MySQL 的主节点在另一个区域,而分区被分配给该区域的 FOQS 主机。这将最大限度地减少跨区域的网络流量,但相对来说比较昂贵。推动 MySQL 副本成为主节点的事件会导致跨地区的流量不平衡(一般来说,FOQS 不能假设哪里有多少流量)。为了处理这些场景,FOQS 不得不改进它的路由,使入队列路由到有足够容量的主机,而出队列路由到具有高优先级 item 的主机。

FOQS 本身使用的一些灾难可靠性优化:

  • 入队转发: 如果入队请求落在一个负载过重的主机上,FOQS 将它转发给另一个有处理能力的主机。

  • 全局速率限制:  由于 namespace 是 foqs 的多租户单元,所以每个namespace都有一个速率限制(计算为每分钟排队数)。FOQS 在全局(所有地区)强制执行这个速率限制。在一个特定的区域内保证速率限制是不可能的,但是 FOQS 确实使用流量模式来尝试将处理能力与流量配置在一起,以减少跨区域的流量。

Reference

[1]facebook engineering: facebook工程师技术博客

60+专家,13个技术领域,CSDN 《IT 人才成长路线图》重磅来袭!

直接扫码或微信搜索「CSDN」公众号,后台回复关键词「路线图」,即可获取完整路线图!


☞“拒绝在 iPad 上运行 Xcode!”☞CentOS 消亡?不怕,替代品 AlmaLinux 将获商业支持!
☞“开源社区运营就像种菜”,黄东旭谈开源商业化 | 独家
已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 代码科技 设计师:Amelia_0503 返回首页