队列与数据库
原文由 Salvatore Sanfilippo 于 发布,订阅该博客
队列是现代计算中极其有用的工具,在 Web 应用中常被用来将一些可能比较耗时的计算推迟到稍后执行。简单来说,队列可以将一次计算拆分为两个时间点:任务被调度的时间,以及任务被实际执行的时间。“生产者”会把待执行的任务放入队列,而“消费者”或“工作进程”则会从队列中取出任务并执行。例如,当新用户在 Web 应用中完成注册流程后,Web 应用会向队列中添加一个新任务,用于发送带有激活链接的邮件。而实际发送邮件的过程——如果遇到临时性网络故障或其他错误可能还需要重试——则由工作进程来负责。
严格来说,我们可以把队列看作一种进程间消息传递原语,其中接收方进程需要对消息的接收进行确认。消息不能是即发即忘的,因为队列需要知道消息是否可以从队列中移除,因此某种形式的确认是必不可少的。
当接收消息会触发任务执行时——就像我们所讨论的这类队列一样——确认收到消息的时机会改变队列的语义。当工作进程在处理消息之前就确认收到消息时,如果工作进程发生故障,消息可能会在任务尚未执行时就丢失。如果确认仅在消息处理之后才发送,那么一旦工作进程故障或出现网络分区,队列就可能会再次投递该消息。无论队列具备怎样的一致性保证,这种情况都会发生,因此,即使队列是基于提供强一致性的系统来建模的,这种不确定性依然存在:
- 如果在处理之前确认消息,队列将具有至多一次投递的特性。这意味着消息可能会被处理零次或一次。
- 如果在处理之后才确认消息,队列将具有至少一次投递的特性。这意味着消息可能会被处理一次到无数次。
虽然这两种情况都不完美,但在实际应用中,第二种行为往往更受欢迎,因为相比一个时不时会完全漏掉某些任务的系统,处理消息被多次投递(从而导致任务被多次执行)的情况通常要简单得多。Amazon SQS(Simple Queue Service)就是一个至少一次投递系统的例子。
至少一次投递系统更受青睐还有一个根本原因,这与分布式系统有关:另一种语义(至多一次投递)要求队列是强一致的:一旦某条消息被确认,就不能再有其他工作进程能够确认同一条消息,这是一个很强的要求。
一旦我们将关注点转向至少一次投递的系统,就会发现用 CP 系统来建模队列不仅是一种浪费,也是一种劣势:
- 无论如何,我们都无法保证比至少一次投递更强的语义。
- 我们的队列会在网络分区中处于少数派的一侧时无法工作。
- 由于一致性要求,队列需要达成共识,因此我们在没有充分理由的情况下白白损耗了性能并增加了延迟。
由于消息可能会被多次投递,从概念上讲,我们想要的是一种满足交换律的数据结构和一个最终一致的系统。消息可以存储在一个复制到 N 个节点上的集合数据结构中,其合并函数就是对各集合求并集。工作进程在执行完消息后发回的确认,从概念上讲也是集合中的元素,用于将某个元素标记为已处理。这是一个极为简单的例子,对于真实世界的系统来说未必特别实用,但它表明了特定类型的队列如何能很好地用分布式系统的特定属性集合来建模。
从实用角度来说,我们的队列还可以尝试提供一些其他有用的特性:
- 至少在一段时间内保证消息只投递给单个工作进程:虽然允许多次投递,但我们希望尽可能避免这种情况。
- 尽力避免在消息已经被处理过的情况下,因超时而重新投递该消息。同样,我们无法保证这一特性,但可以尽力减少对已处理消息的重复投递。
- 在正常运行期间保留足够的状态,将消息按 FIFO 的方式处理,使先到达的消息先被处理。
- 自动清理内部数据结构。
除此之外,我们还需要在网络分区期间保留消息,因此从概念上讲(即使实际中我们可能会使用不同的数据结构),待投递的消息集合是所有节点上所有消息的并集。
遗憾的是,虽然已经存在许多基于 Redis 的队列实现,但没有一个尝试利用 N 个独立的 Redis 节点及其提供的原语来作为构建具有上述特性的分布式系统的基石。利用 Redis 的数据结构和性能,以及能够提供某些有用保证的算法,或许可以构建出一个非常实用、易于管理和扩展,同时在单节点上就能提供出色性能(消息/秒)的队列系统。
因为我觉得这个话题很有意思,而且这是 Redis 的一个绝佳用例,我正在非常缓慢地为这样一个基于 Redis 的队列系统进行设计。如果时间允许,我希望在接下来的几周内能展示一些成果。
随机一篇博客
评论
登录后参与讨论