随着业务上的增长与迭代,业务使用的消息集群会创建越来越多主题,在业务流量不断增长的情况下,还需要不断增加主题的分区数量,Kafka 由于本身的存储机制特点,随着主题和分区数的增加,性能会不断下降,无法满足业务上的发展。通常我们的做法是扩容集群,但随着集群的不断扩大,又会伴随着很多问题,随着集群的扩容节点,创建主题和分区数不断增多,存储在 zk 上的元数据就会越来越多,每当需要全量同步元数据到 Broker 节点时,会是一笔很大的网络开销,由于当 contrller 切换时往往需要全量同步元数据到每个 Broker 上,因此,元数据越多,controller 的切换时长会越长,而且由于 Kafka 会独立一个复制线程进行分区副本的复制,多个分区共享该线程,因此 Broker上的分区不断增多后会造成复制线程负载增大,严重时会会造成某些分区副本复制跟不上,导致 ISR 频繁变化。
最简单的做法就是将集群拆分成若干个小集群,将主题平均分配到这些小集群中,但这会使得用户需要变更系统配置,那有没有一种办法可以在不影响用户系统的情况下,同时还能兼容小集群的模式呢?中通消息平台(以下文简称 ZMS)应运而生。
ZMS 脱胎于中通内部对消息引擎的实践经验总结,它屏蔽底层消息引擎具体实现,通过唯一标识动态路由消息,同时为开发运维人员提供自动化部署运维集群,主题、消费组申请与审批、实时监控、自动告警、容灾迁移等功能。
ZMS 整体架构如下图所示:
ZMS-SDK 模块从技术的角度是 ZMS 核心技术实现,封装消息引擎内核,ZMS 核心功能都围绕着 ZMS-SDK 展开;从用户的角度上可以让用户与消息集群解耦,屏蔽各消息集群差异,用户无需关心消息引擎底层技术细节。
ZMS-SDK 模块具体实现是将用户在控制台申请的主题消费组元数据信息保存在 ZK 节点,当用户使用 ZMS-SDK 发送消息时,ZMS-SDK 会从 ZK 对应节点获取该主题元数据信息,并为主题创建一个 Producer,Producer 会缓存在本地,同时 ZMS-SDK 会监听该主题对应的 ZK 节点,一旦该主题元数据变更就会销毁缓存中的 Producer,同时再创建一个新的 Producer 缓存在本地中。ZMS-SDK 订阅消费逻辑同理消息发送。
如下图所示:
基于 ZMS-SDK 核心逻辑,再结合 ZMS 的主题迁移功能,ZMS 就可以解决消息集群的主题分区数过多带来性能下降的问题,通过 ZMS 可以将它管理下的主题分散在各个小集群当中,用户只需申请主题消费组即可,审批到哪个集群并不需要关心,这完全由运维根据集群负载情况决定主题消费组被审批到相对应的集群中,而用户待主题消费组审批通过后,就可以通过 ZMS-SDK 进行发送和消费消息,如果主题消费组由变更,ZMS-SDK 就会感知对应 ZK 节点的变化,更新本地 Producer 和 Consumer 缓存。用户感知不到扩容迁移动作,真正实现无感知生产和消费。
通过上述的一些改造,我们就可以支持更大的业务规模,用户在使用时只需要知道主题/消费组名字即可。
但缺点也有,ZMS 所管理下的消息集群,就必须要依赖于 ZMS-SDK,才能实现以上功能,而目前 ZMS-SDK 仅有 Java 版本,对于 Go 项目,那么以上功能就失效了,而且 ZMS 很多监控指标都是依赖于 ZMS-SDK 进行上报的,因此,如果用户使用非 ZMS-SDK 连接 ZMS 管理下的集群,就会失去很多 ZMS 功能。
我想归根结底是 ZMS-SDK 并没有对消息内核进行改造,仅仅是对其客户端进行了一些改造与封装,在 ZMS-SDK 内部缓存了多个集群的客户端实例,如下图所示:
这也许会带来另外一个问题:
如果用户的系统使用了很多个主题/消费组,且这些资源都不在一个集群上,ZMS-SDK 则会为每个主题/消费组创建一个客户端,如果在一个系统中创建过多的客户端会导致创建过多的线程,导致项目 Socket 连接开销巨大。当然,在中通内部使用,通常来说用户的系统不会存在过多的主题/消费组,因此目前还没有遇到这个问题。
如何解决 ZMS-SDK 当前设计上的存在问题呢?
对这方面我也有过一些探索,我在腾讯云中间件看到一篇关于 Kafka 集群突破百万 partition 的技术探索文章,对我的启发非常大,它对 Kafka 内部进行了一系列的改造,使用小集群组建逻辑集群的思想,实现单客户端对应一个逻辑集群: