众所周知,Apache Kafka 是一个分布式开源流和事件处理平台,广泛应用于各大互联网公司以及基于不同体系的软件架构的业务场景中。其实,基于早期的设计理念而言,Kafka 最初被设想为消息队列,并基于分布式提交日志的抽象。然而,自 2011 年由 LinkedIn 创建并开源以来,Kafka 已迅速从消息队列演变为成熟的事件流处理平台。
最初 Kafka 是在 Apache 许可下进行开发的,但后来 Confluent 对其进行了分支改造并提供了一个更为强大的版本。实际上,Confluent 使用自身的平台提供了最为完整体系的 Kafka 发行版。同时,为了获得更为广阔的市场份额, Confluent 平台基于额外的社区组织和商业功能不断优化改进 Kafka,这些功能旨在大规模增强运营商和开发人员在生产中的流媒体体验。
Apache Kafka 是一款强大的消息系统组件,在实际的业务场景中,基于此,能够协助我们创建易于扩展的实时、高吞吐量、低延迟的数据流。经过优化后的 Kafka,其各方面的特性均有所提升,有的关键点甚至使得 Kafka 组件的固有功能特性发挥的淋漓尽致。例如,抵抗集群内部发生的机器/节点故障、集群上数据和消息的持久性以及业务的可连续性。这也是在实际的项目开发活动中,对 Kafka 优化如此重要的原因。
通常,从本质上讲,基于不同的业务场景实现,Kafka 框架的优化应该是一个优先、持续、不断迭代的事件过程。但同时,我们往往会因运行环境的差异、业务逻辑更新而很难知道如何准确、充分地去优化 Kafka。基于 Kafka 框架的相关特性,在本篇文章中,我将为大家带来四个 Kafka 最佳优化实践的场景,以便能够在实际的业务开发活动中充分利用 Kafka 组件的相关功能特性。
通常,针对 Kafka 组件的性能优化取决于不同的因素,例如,集群模型、架构拓扑、承载环境以及业务逻辑实现。因此,基于业务场景特性,Kafka 的选型及部署可能是一个具有挑战的活动事项,毕竟,基于分布式架构将会涉及多个层面,并且在这些层内有不同差异的参数可作为调优对象。以下为 Kafka 集群架构拓扑参考模型:
例如,通常情况下,具有自动化数据冗余的高吞吐量发布-订阅 (Pub / Sub) 模式在很多业务场景中具有十分重要的意义,无论是基于服务组件解耦或者针对高并发请求下的流量削峰。然而,在某些特定的环境条件下,当我们的服务的消费者努力跟上数据流时,或者如果他们无法读取消息,因为这些消息在消费者到达之前就消失了,那么需要做一些工作来支持消费应用程序的性能需求。
正如上述所说,Kafka 是一个强大的实时数据流框架及平台。但在实际的业务场景中,需要依据所承载的业务逻辑进行适应性调整,因为,在某些情况下,我们依据默认的配置或所推荐的版本进行应用架构的交互过程中可能存在各方面的问题,比如,消息流传输缓慢和生产者或消费者处理滞后等等。
通常来讲,Kafka 优化是一个广泛的主题,基于业务场景,可以非常深入和细化,但这里有四个高度利用的 Kafka 最佳实践可以帮助大家进行快速入门:
1、升级至最新版本
2、理解数据吞吐率
3、按需实现自定义分区
4、调整消费者套接字缓冲区
1、升级至最新版本
这听起来可能有点无聊,但是在实际的开发活动中,我们压根无法知晓有多少人、团队依旧使用旧版本的 Kafka。当然,并不是说老版本的 Kafka 不满足实际的业务场景,而是基于从性能、稳定性角度而言。毕竟,最新的版本无论是基于缺陷修复还是功能改进、优化,都是值得尝试的。从另外一个角度而言,随着业务架构的不断演进,其底层依赖的组件可能会从兼容特性或者安全、性能特性发生较大变化,导致之前所支撑的组件在新的业务架构面前显得无所适从,影响业务系统稳定性。
截止到目前,Kafka 目前总共演进了 8 个大版本,分别是 0.7、0.8、0.9、0.10、0.11、1.0 、2.0 及 3.0,其中的小版本和 Patch 版本很多。Kafka 每次更新都会略有改进。最新的 Kafka 版本已于 2021 年 9 月发布,是一个多方面的主要版本。Apache Kafka 3.0 引入了各种新功能、突破性的 API 更改以及对 KRaft 的改进——Apache Kafka 的内置共识机制将取代 Apache ZooKeeper 。虽然 KRaft 尚未被推荐用于生产,但其对 KRaft 元数据和 API 进行了较多改进。以及所涉及的 Exactly-once 和分区重新分配支持值得强调。
基于 Kafka 所演进的不同版本,以前期的 0.7 版本为例,Kafka 从 0.7 时代演进到 0.8 之后正式引入了副本机制,至此 Kafka 成为了一个真正意义上完备的分布式高可靠消息队列解决方案。基于副本备份机制,使得 Kafka 尽可能比较好地做到消息无丢失。虽然基于当时的生产和消费消息使用的还是老版本的客户端 API,所谓的老版本是指当你用它们的 API 开发生产者和消费者应用时,你需要指定 ZooKeeper 的地址而非 Broker的地址。
诚然,基于 0.8 的版本在大多数环境条件下能够提高分布式高可靠的解决方案,但在某些特定的场景中可能会造成消息的丢失,因此 0.8.2.0 版本社区引入了新版本 Producer API,即需要指定 Broker 地址的 Producer。经过几年的发展,2015 年 11 月,社区正式发布了 0.9.0.0 版本。基于当前的环境,这是一个重量级的大版本更迭,0.9 大版本增加了基础的安全认证 / 权限功能,同时使用 Java 重写了新版本消费者 API,另外还引入了 Kafka Connect 组件用于实现高性能的数据抽取。
在后续的历史发展过程中,0.10.0.0 是一款里程碑式的大版本,毕竟,此版本引入了 Kafka Streams。正式向大家宣布了从这个版本起,Kafka 正式升级成分布式流处理平台,同时,不久后,在 2017 年 6 月,社区发布了 0.11.0.0 版本,引入了两个重量级的功能变更:一个是提供幂等性 Producer API 以及事务(Transaction) API。
因此,当我们的业务场景无特殊要求时,若基于当前的版本出现各种性能问题,建议在后续的迭代开发中进行 Kafka 版本的升级(Server 端与 Client 端),以提升系统运行能力。(此种优化策略在笔者之前所经历的公司业务场景中均曾出现过)
2. 理解数据吞吐率
在整个 Kafka 体系中,Partition 分区是吞吐量性能所基于的存储层。
每个分区的数据速率是消息的平均大小乘以每秒的消息数。简而言之,它是数据通过分区的速率。所需的吞吐率决定了分区的目标架构。Kafka 中的 Topic 中的内容可以被划分为多分 Partition ,每个 Partition 又分为多个段 Segment,所以每次操作都是针对一小部分做操作,很轻便,并且增加并行操作的能力
Kafka 基于磁盘顺序读写来提升性能的。Kafka 的消息是不断追加到本地磁盘文件末尾,而不是随机的写入,这使得 Kafka 写入吞吐量得到了显著提升 。在 Kafka 中, 每一个partition其实都是一个文件 ,收到消息后 Kafka 会把数据插入到文件末尾。
因此,在某些特定的场景中,或许,此种方案也是一个关键的 Kafka 优化技巧:为了提高吞吐量,我们可以扩大请求中获取的最小数据量。这导致更少的请求。然后以更大的批次传递消息。这一点至关重要,尤其是在生成的数据量较少时。对 Kafka 吞吐量指标的广泛了解将帮助用户在这种情况下完全优化他们的 Kafka 系统。
3. 写入 Topic 时依据架构要求实现自定义分区
其实,分区的作用就是提供负载均衡的能力,或者说对数据进行分区的主要原因,就是为了实现系统的高伸缩性(Scalability)。不同的分区能够被放置到不同节点的机器上,而数据的读写操作也都是针对分区这个粒度而进行的,这样每个节点的机器都能独立地执行各自分区的读写请求处理,并且,我们还可以通过添加新的节点机器来增加整体系统的吞吐量
分区是实现负载均衡以及高吞吐量的关键,故在生产者这一端就要仔细盘算合适的分区策略,避免造成消息数据的倾斜,使得某些分区成为性能瓶颈,这样极易引发下游数据消费的性能下降。在实际的业务场景中,我们希望每个分区都支持类似的数据量和吞吐率。实际上,数据速率随着时间的推移而变化,生产者和消费者的原始数量也是如此。
可变性带来的性能挑战是消费者滞后的可能性,即消费者读取率落后于生产者写入率。在前期的业务场景中,随着 Kafka 环境的扩展,随机分区或许是一种有效的方法,可确保我们不会在尝试将静态定义应用于移动性能目标时引入人为瓶颈。
分区领导通常是通过 Zookeeper 维护的元数据进行简单选举的产物。然而,领导选举不考虑单个分区的性能。
根据当前业务架构所选型的 Kafka 版本,可以利用专有的平衡器。但是缺少这样的工具,随机分区提供了平衡性能的最不干涉的途径。
这就是为什么有的时候、有的场景,随机分区是我们推荐的关键 Apache Kafka 最佳实践之一。它为消费者平均分配负载。因此,扩展消费者变得更加容易。当使用默认分区器而不手动识别特定分区或消息键时,实际上会发生这种情况。随机分区最适合无状态或“令人尴尬的并行”服务。
基于以上,分区是实现负载均衡以及高吞吐量的关键,故在生产者端我们需要仔细盘算合适的分区策略,避免造成消息数据的倾斜,使得某些分区成为性能瓶颈,这样极易引发下游数据消费的性能下降。
4. 调整消费者套接字缓冲区
在 Kafka 中消息存储模式中,数据存储在底层文件系统中。当有 Consumer 订阅了相应的 Topic 消息,数据需要从磁盘中读取然后将数据写回到套接字中(Socket)。此动作看似只需较少的 CPU 活动,但它的效率非常低:首先内核读出全盘数据,然后将数据跨越内核用户推到应用程序,然后应用程序再次跨越内核用户将数据推回,写出到套接字。应用程序实际上在这里担当了一个不怎么高效的中介角色,将磁盘文件的数据转入套接字。
基于 Zero-Copy 零拷贝技术,使应用程序要求内核直接将数据从磁盘文件拷贝到套接字,而无需通过应用程序。零拷贝不仅大大地提高了应用程序的性能,而且还减少了内核与用户模式间的上下文切换。
在较旧的 Kafka 版本中,参数 receive.buffer.bytes 设置为 64kB 作为其默认值。在较新的 Kafka 版本中,该参数为 socket.receive.buffer.bytes,默认为 100kB。
这对 Kafka 优化意味着什么?对于高吞吐量环境,这些默认值太小,因此不足以支撑当前的业务架构要求。当 Broker 和 Consumer 之间的网络带宽延迟乘积大于 LAN(局域网)的带宽延迟乘积时,就会出现这种情况。
除此,当没有足够的磁盘空间时,线程执行会变慢并受到限制。最重要的 Apache Kafka 最佳实践之一是增加网络请求缓冲区的大小。这样做将可以帮助应用提高吞吐量。如果我们的网络以 10 Gbps 或更高的速度运行并且延迟为 1 毫秒或更多,则建议将其套接字缓冲区调整为 8 或 16 MB。如果内存表现异常,可以尝试考虑配置为 1 MB。因此,在某些特定的场景中,我们可以通过调整消费者套接字缓冲区以实现高速摄取,从而提升性能。
基于上述所述,在实际的业务场景中,依据当前的业务架构及应用场景,进行合理的、适配的性能调优显得尤为重要。尤其是在高复杂性、高稳定性、高吞吐性的应用场景中更具有参考意义。