Consumer to Consumer转发消息队列设计

125 阅读4分钟

项目背景

消息队列是广泛使用的基础组件,在一些场景中,有成千上万个实例需要完整消费同一个topic的需求,而仅仅因为单机网络带宽的限制就部署大量的消息队列server显然是一种浪费。

比如快手的推荐系统就在使用一款自研的支持Consumer to Consumer转发的消息队列:BTQueue,但BTQueue在设计时考虑的比较简单,在后续使用中发现了许多问题,比如BTQueue没有Sequence Number,只能依靠时间戳定位,导致基于它做数据同步的存储模块无法精准地控制数据一致,而且BTQueue server端没有使用一致性协议,本身就有一致性风险,还存在稳定性、内存消耗等其它方面的问题。本文希望设计一套完善的C2C转发消息队列组件,并举出几种C2C转发消息队列在存储系统上的落地方案。

衍生Topic

除了类似BTQueue的数据转发功能外,新的消息队列还将支持通过轻量算子衍生新的topic的能力,衍生出的topic不需要多副本存储,由上游topic消息经过算子计算直接获得,此特性能够简化服务的部署,方便各模块间解耦,避免重复计算,解决传统中间模块消费-转发过程中转发失败难以处理的问题。

架构设计

每个集群会提前分好固定数目的partition,每个partition有独立的Sequence Number,整个集群的元数据存在一个元数据服务上,除了源头topic,每个衍生topic配置固定的上游topic。

每个topic的所有consumer,会互相转发消息,每个partition的转发的拓扑是一个多叉树,由一个独立的调度模块,统筹全局的转发拓扑,多叉树上每个节点的内部结构设计如下:

image.png

源头topic可以支持Kafka、一致性协议等多种数据源,一个源头topic会启动多个实例,调度模块会将partition均衡调度到各个实例上,并且支持多个备份容灾。

下游实例的输入端设置为C2C Consumer,并指定好topic,输出端默认会输出一个与输入相同的topic,用于转发流量。使用者可以自行实现多个算子,产出衍生topic,也可以实现本地回调,作为末端消费。

流量转发

调度策略会让每个partition由尽量少的节点转发,并且转发负载尽量均衡,节点不会缓存不负责转发的partition的数据,这相比BTQueue能够大幅降低内存开销。

BTQueue在遇到过多Consumer由于自身原因产生lag时,会产生大量回源流量,把源头资源耗尽,这个问题无法从根源上避免,但可以本系统可以从两点改善:

1.由于每个节点只转发缓存少量partition,故而整体上可以缓存更多的数据,短期的lag可以靠缓存覆盖,减少回源流量

2.每个topic要限制回源并发量,优先保证健康的consumer的正常消费,将有lag的消费按照SequenceNumber、消费速率等因素分到不同队列中,每个队列从最小SequenceNumber从上游拉取数据,只产生一份回源流量

边缘缓存

许多场景下,每个调用实例的热点数据是基本一致的,所以可以由存储服务搜集统计信息,筛选出热点数据,然后产出一份只有热点数据相关operate的操作日志,请求方只需要订阅这部分操作,即可近实时更新本地缓存,且不会为非热点数据浪费资源。

image.png

元数据服务

etcd、zookeeper、Nacos等元数据服务是广泛使用的基础设施,它们的watch机制是许多生产场景依赖的核心特性,他们现在的实现中,watch的对象都是单个节点,但在实际应用中,同一个业务不同实例需要订阅的对象往往是完全相同的集合,这种情况就非常适合引入topic的概念,即每个topic对应一组watch规则,订阅方可以按topic订阅事件,而不必自己注册一堆watch节点。

上文中的算子过滤正适合用来实现这种topic:输入端订阅元数据服务的原始日志,每个topic的订阅规则对应一个算子,经过算子过滤后,输出对应数据,业务进程通过C2C Consumer订阅目标topic,订阅相同topic的进程之间通过C2C转发,分担元数据服务的压力。