作者:字节跳动消息队列研发工程师|姬索肇
随着“万物”互联网化的发展,许多公司内部服务间面对的数据流量也越来越大,在应对大量的数据通信需求时,多数公司都会选择将消息队列作为削峰填谷的关键工具。
字节跳动的消息队列团队不仅要支撑公司内部消息队列系统的设计、开发和维护工作,还要解决诸多技术难题和痛点,例如如何稳定高效地处理海量数据、如何降低运维成本等。目前经过技术优化和迭代改进,字节跳动的消息队列平台支持弹性扩缩容、高吞吐、低延迟等特性,已经可以稳定承载每秒数十 T bytes 的流量。受限于篇幅,本系列文章将分为上下篇。本文将主要从字节消息队列的演进过程及在过程中遇到的痛点问题,和如何通过自研云原生化消息队列引擎解决相关问题方面进行介绍。
Kafka 时代
在初期阶段,字节跳动使用 Apache Kafka 进行数据的实时处理和流转,Kafka 同样也在各大互联网公司的产品和大数据系统中得到了广泛的应用。
Kafka 集群(Cluster)由多台机器组成,每个集群里面可以拥有多个主题(Topic)。用户可以将所有逻辑上相关的数据放到同一个 Topic 中。由于 Topic 可能会有大量的数据,所以可以通过分区(Partition)去切分数据。每一条写入 Kafka 的消息都有一个唯一标识,也就是偏移量(Offset)。在 Kafka 集群内,(Topic, Partition, Offset)这个三元组可以唯一定位一条消息。
从用户的角度来看,有两个关键的角色:生产者(Producer)和消费者(Consumer)。生产者负责写消息到 Kafka;消费者负责读取消息。
从架构上来看 Kafka 的架构非常简单,只有 Broker 组件负责所有的读写操作。在 Kafka 集群中,一个 Broker 节点会被选举为控制器(Controller)监管集群的状态,并负责处理相关问题,例如所有 Broker 的健康状态和主从切换等。同时 Broker 还要承担协调者(Coordinator)的角色,负责协调消费者组成员和消费者消费的分区。
Kafka 通过多副本机制保证数据的可靠性,其中主副本(Leader)负责处理所有的读写请求;从副本(Follower)会持续从主副本拉取数据。若主副本与从副本的数据差距在一定范围内, Controller 会认为副本的状态是健康的。如果数据差距过大,副本就会被标记为不健康的状态。
运维操作
在Kafka的运维过程中,有四种常见的操作:重启、替换、扩容和缩容。
总体来说,Kafka 的常用运维操作涉及数据拷贝和 IO 的开销会导致运维操作无法快速解决容量和运维窗口期短的问题。
负载均衡
在 Kafka 的使用过程中,数据的负载均衡(Balance)是一个重要而复杂的问题。首先,需要考虑多种因素,包括存储空间、写入吞吐量以及消费吞吐量等。此外,热点问题也是一个值得注意的问题,因为每个 Partition 的负载可能并不一致,有一些 IO 开销大,有一些存储空间占用较多,这就导致了调度的复杂度很高。
在实际场景中,每一个 Partition 都存放在同一块磁盘上的,而且每一个业务其特点都不一样。例如,一些模型训练的任务可能会有大量的写入,而且下游可能有十几个甚至几十个消费者,这就使得吞吐量非常大。一旦击穿 Cache,对于磁盘的 IO 开销就会非常大。另外,也有一些业务可能吞吐量没那么高,但是需要长时间存储数据,这种情况下就会有很大的容量开销。
另外在负载均衡时需要拷贝数据就会导致无法实时对负载进行调整。特别是在晚高峰或业务突发的时候,对流量的调度很难及时响应。如果出现热点就更难以快速响应和化解问题。
故障恢复
在实际运行 Kafka 的过程中,故障恢复是我们经常要考虑的问题。可以根据故障的机器数量将其分为单机故障和多机故障。
当出现单机故障即某一个 Broker 挂掉时,我们可以进行故障切换。具体操作是:Controller 在发现 Broker 挂掉后,自动将其上的 Leader 角色切换到别的健康 Broker。例如上图中的 Partition 3 中,Leader 所在的 Broker 挂掉之后,Controller 便会把 Leader 角色切换到 Broker B 接管流量以保障服务的正常运行。需要注意的是,这种情况下是否会丢失数据,取决于用户写入参数和集群的配置,可以看作是写入延迟和稳定性的权衡。
对于多机故障情况要更为复杂。如果某个 Partition 的所有副本都出现了故障,那么这个 Partition 的读写就会完全断流。例如上图的 Partition 3 中,如果两个副本的 Broker 都挂掉,那么这个 Partition 3 就找不到 Leader,从而导致它的写入和消费完全断流。更为糟糕的情况是,如果无法恢复这两台机器,或者磁盘数据丢失,那么存储在 Partition 3 的所有数据也会因此丢失,造成不可挽回的损失。
Page Cache
Kafka 的数据缓存只有操作系统的 Page Cache 可用,并没有自己的缓存,这也使得其在处理大规模、高并发的数据请求时性能不尽如人意。因为 Kafka 对 Page Cache 的使用是不可控的,又由于缓存机制的运行原理,我们无法规定哪些流量可以进入缓存,哪些流量不允许进入缓存。
例如某个高负载的业务在高峰期决定升级,把服务暂停再重启后,由于有延迟消息(Lag),会出现大量的Cache Miss,也就是对应的数据无法在Page Cache中找到。这部分流量直接通过了Page Cache,穿透到了磁盘中,这会对磁盘产生较大的冲击。
此外,一旦这种情况出现,就很难再恢复到正常状态。因为穿透到磁盘的流量往往无法被及时消费掉,进而导致延迟(Lag)的现象。这份延迟将长期存在,会继续冲击着磁盘,使磁盘的读写压力持续增加。而增加的压力又将影响磁盘上所有的写入操作和其他消费者的读操作。这就形成了一个连锁反应:当 Page Cache 发生问题后,磁盘压力增加,进一步影响 Kafka 的读写性能,甚至会导致服务质量下降。在这种情况下,我们只有等到整个集群压力降低后才能完成恢复。
痛点总结
综上所述,字节跳动消息队列团队在使用和维护 Kafka 时有以下痛点:
-
运维操作耗时长:随着数据量的增加以及集群负载的提升,所有相关的运维操作都需要非常长的时间才能完成,这对团队的效率有着明显的影响。
-
负载均衡算法复杂,均衡代价大:Kafka的负载均衡算法相当复杂。并且,由于负载均衡代价大,无法对负载进行实时调度。
-
缺少自动的故障恢复手段:在面临故障场景时,特别是多机故障时,Kafka的恢复能力非常弱,使得我们的运维工作中充满困难。
-
重度依赖 Page Cache,同时影响读写:由于Kafka重度依赖Page Cache,在高负载情况下会明显影响其读写性能。
-
状态重,难以云原生化:由于大量使用本地磁盘,Kafka的状态非常“重”,导致其很难进行弹性改造,例如在云平台上进行CPU和内存的弹性出让等。这对我们进行云原生化改造带来了巨大的困难。
为了解决这些问题,字节跳动消息队列团队自主研发了云原生消息引擎——BMQ。BMQ ****是一款能够兼容 Kafka 协议的云原生 下一代分布式消息队列,支持云原生、存储和计算分离的理念,并具有弹性可扩展性。这使得 BMQ 能够灵活应对数据量的增长,显著提高数据处理效率的同时能够降低流程的复杂性和成本。
在提供了大规模、高吞吐、低延迟的实时数据传输服务的同时,也确保了系统的稳定运行。目前字节跳动 95% 以上的业务已经从 Kafka 过渡到了 BMQ。
自研云原生消息引擎
BMQ 具有多项优势,使其在处理海量数据时提供了强大的性能和稳定性。以下是 BMQ 的主要特性:
- 高性能:BMQ 使用 C++ 语言编写,避免了 Kafka 使用的 Java 语言的 GC 等问题。相比于 Kafka 只使用操作系统的 Page Cache,BMQ 提供了多级缓存机制,拥有高效的数据处理能力。
- 存算分离:BMQ 充分使用云存储,实现存储与计算的分离,提高了计算资源的利用率,由云存储组件CloudFS保证数据一致性。
- 高吞吐:BMQ 将数据拆分成多个 Segment 文件,存储在不同的分布式存储系统的不同机器上的不同磁盘上,从而提高了吞吐性能。
- 低延迟:BMQ 的 Broker 节点自动感知写入文件尾部的消息延迟变高,会创建新的 Segment 文件来降低延迟。
技术架构
通过采用存算分离架构实现了计算和存储的解耦。在这种架构中,BMQ 计算层仅负责执行相关的计算逻辑,将数据的持久化工作通过网络交给了分布式存储系统进行实现。这种存算分离的架构不仅提升了性能,同时也进一步优化了资源的利用率。
计算层:
-
Proxy:负责处理来自客户端的请求。
- 对于 Produce 请求,Proxy 对请求中的 Topic-Partition 做聚合后转发给对应的 Broker,由 Broker 处理完成后返回结果给客户端;
- 对于 Consume 请求则结合元数据直接读取分布式存储系统中的数据,同时提供多层缓存机制。
-
Broker:负责处理来自 Proxy 的 Produce 请求,将数据持久化到分布式存储系统中,同时负责删除过期的数据。
-
Controller:根据统计信息来对 Partition 进行负载均衡,同时还负责集群管理的任务,如新建 Topic、扩容 Partition 等。
-
Coordinator:协调一个消费组下多个 Client 之间的负载均衡以及处理该消费组的 Offset 相关请求。
存储层:
- 每个 Partition 对应分布式存储CloudFS中的一个目录,数据被切分为多个 Segment 文件并存储。
弹性扩缩容
由于 BMQ 将存储资源和计算资源进行了分离,使得计算层的服务完全变得无状态。借助于 Kubernetes 的强大运维能力,BMQ 非常适合那些业务流量波动大、高峰和低谷比较明显的业务场景,可以在秒级别实现资源的动态扩缩容。
这种设计也考虑到了计算资源和存储资源难以平衡的问题。BMQ 在执行扩缩容操作时,可以直接通过对计算资源和存储资源做出独立调整,能够降低资源浪费,还进一步提升了整体资源的利用率。
高吞吐
BMQ 中针对同一 Partition 的所有数据,遵循一个清晰的数据组织方式:它们都存储于同一个目录下,并按照大小被拆分成不同的 Segment 文件。每一个 Segment 文件都存储在分布式存储系统中不同的机器上的不同磁盘上,与 Kafka 中采用的将所有 Partition 数据存储在一台机器上的单一磁盘的方式形成鲜明对比。
通过这种设计,BMQ 可以避免单机磁盘瓶颈所导致的数据生产和消费问题。同时这种方式有助于将 IO 压力分散到各个机器,从而实现了更高的吞吐性能,使得 BMQ 能够在高数据流量场景下保持高性能和稳定性。
低延迟
对于通常的写入底层分布式存储系统的操作,可能会遇到由于存储系统写入速度慢导致的客户端观察到的写入延迟增高的问题。BMQ 针对这种问题,采用了一套独特的低延迟策略—通过 Broker 自动感知写入一个文件尾部的部分消息延迟增高的情况,进而通过自动创建新文件进行处理。
在新文件创建后,请求队列中之前未能成功落盘的消息,会被重新写入到新的文件中,从而确保了这些数据不会丢失。同时,对于原先文件中的数据,无论这部分数据是否落盘成功,该文件的元数据中都不会更新这部分数据对应的长度,这样就避免了这些数据的重复出现,达到了数据不重复也不丢失的目标。
除了计算层的优化,存储层的设计也是实现低延迟的关键。在 BMQ 中,存储层的分布式存储系统采用了挂载 NVMe 盘作为加速单元改善性能的技术方案。
- 对于写请求,首先将数据写入加速单元 DataNote(以下简称 DN)中,之后由后台线程以异步方式将数据上传到对象存储 TOS(以下简称 TOS)中。这种方式可以有效减少写入过程中可能出现的延迟情况。
- 对于读请求,系统会优先从 DN 中读取数据。如果 DN中 不存在请求的数据,系统会从 TOS 中加载对应的数据并缓存到 DN 中。这种设计使得读取过程更为高效,大大缩短了数据从请求到交付的时间,实现了出色的低延迟性能。
综合对比
Kafka | BMQ | |
---|---|---|
升级/重启 | 分钟级重启 | 秒级重启 |
机器替换 | 新机器高负载 | 新机器正常负载 |
扩容/缩容 | 机器间 Balance 数据 | 无其他操作 |
负载均衡 | 机器间搬迁数据 | 无需搬迁数据 |
流量突发 | 天级别扩容 | 秒级扩容 |
实践案例
BMQ 在火山引擎上的落地以某大型广告代理服务商的实时数据处理系统为例,他们在面临着原有平台实施成本高,可扩展性有限等多重挑战下,选择了火山引擎作为数据处理的解决方案,并希望通过火山引擎的技术架构优势,协助他们实现业务的实时分析查询以及消息传递需求,以此来提升业务效率,降低运维成本。
经过了解,该公司原有系统的单一产品形式无法满足数据实时分析和高频更新的双重要求,产品的弹性能力也无法满足需求。在基于火山引擎技术架构的新方案中,采用 BMQ 作为云原生广告分析平台的消息中间件,最终也为该客户带来了三大核心收益:首先使用云原生架构解决了平台的快速动态弹性能力问题;其次在采用了云搜索服务配合实时分析引擎的能力实现了数据亚秒级可见;最后通过提供基于 Serverless Flink 和 BMQ 的数据同步链路实现了数据的快速集成。
经过了上文对经典消息队列 Kafka 在弹性、规模、成本及运维方面都无法满足业务需求的分析,到字节消息队列团队结合业务需求自研了计算存储分离的云原生消息队列 BMQ,并最终落地火山通过了用户实际业务场景考验的介绍。下篇将继续从架构、容灾容错能力以及实战应用几个方面详细介绍字节跳动新一代云原生消息队列实践。敬请关注「字节跳动云原生计算」微信公众号~
火山引擎云原生消息引擎 BMQ 基于云原生全托管服务,支持灵活动态的扩缩容和流批一体化计算,能够有效地处理大数据量级的实时流数据,帮助用户构建数据处理的“中枢神经系统”,广泛应用于日志收集、数据聚合、离线数据分析等业务场景。
更多详情参考:www.volcengine.com/docs/6820/1…