Kafka 最初由 LinkedIn 公司开发,用于满足其实时数据处理和日志收集的需求。在大数据时代,处理海量数据和实时数据成为关键挑战,传统消息传递系统难以满足实时性和可伸缩性需求,这正是 Kafka 出现的背景。
Kafka 的主要设计目标包括高吞吐量、持久化数据存储和分布式系统等特点。在高吞吐量方面,即使在非常廉价的商用机器上,Kafka 也能做到单机支持每秒 100K 条以上消息的传输。例如,Apache Kafka 作为一款高性能的消息队列系统,能够在大规模分布式环境中实现高吞吐量。其高吞吐量的实现主要依赖于分布式架构、高效存储与索引、批处理与压缩、高效消费者模型以及网络通信优化等多个层面的设计与技术。
对于持久化数据存储,Kafka 的消息被持久化存储在磁盘上,保证消息不会丢失,即使消费者未及时处理。Kafka 很大程度上依赖文件系统来存储和缓存消息,以页面缓存(pagecache)为中心的设计风格,使得代码实现简单,不需要尽力维护内存中的数据,数据会立即写入磁盘。
在分布式系统方面,Kafka 采用分布式架构,可以轻松地扩展到多个节点,以处理高吞吐量和大规模数据。每个节点(称为 Kafka Broker)负责处理一部分数据和请求。生产者和消费者可以同时与多个 Kafka Broker 进行通信,从而实现负载均衡和扩展性。Kafka 将每个主题(Topic)划分为多个分区(Partition),每个分区在多个 Broker 上进行副本复制,以提供容错性和高可用性。分区和副本的结合使得 Kafka 可以同时处理大量的消息和连接,并允许多个消费者并行地读取数据。
二、Kafka 性能优化
(一)批量处理
在传统消息中间件中,消息的发送和消费通常是针对单条消息进行的。对于生产者而言,发送一条消息后,broker 返回 ACK,产生两次 RPC;对于消费者而言,先请求接收消息,broker 返回消息后再发送 ACK 表示已消费,产生三次 RPC。而 Kafka 采用批量处理的方式,生产者聚合一批消息后,通过两次 RPC 将消息存入 broker。例如,假设需要发送 1000 条消息,每条消息大小 1KB,传统消息中间件需要 2000 次 RPC,而 Kafka 可能会把这 1000 条消息包装成 1 个 1MB 的消息,仅需两次 RPC 就可完成任务。
(二)客户端优化
新版生产者客户端摒弃了以往的单线程,采用了双线程:主线程和 Sender 线程。主线程负责将消息置入客户端缓存,Sender 线程负责从缓存中发送消息,而这个缓存会聚合多个消息为一个批次。这种方式有效地提高了消息的发送效率。
(三)日志格式变革
Kafka 从 0.8 版本开始日志格式历经了三次变革:v0、v1、v2。Kafka 的日志格式越来越利于批量消息的处理。例如在 v0 版本中,消息由日志头和消息记录组成;v1 版本相比于 v0 版本,只多了一个 timestamp 字段;v2 版本的消息集换成了 Record Batch,内部包含一条或多条消息,并且引入了变长整形,节省了空间。
(四)日志编码优化
Kafka 最新的版本中采用了变成字段 Varints 和 ZigZag 编码,有效地降低了日志附加字段的占用大小。例如,原本这些附加字段按照固定的大小占用一定的篇幅,现在数值越小,占用的字节数就越少。这样使得日志(消息)尽可能变小,网络传输的效率提高,日志存盘的效率也提升,从而整体性能得到提升。
(五)消息压缩
Kafka 支持多种消息压缩方式,如 gzip、snappy、lz4。对消息进行压缩可以极大地减少网络传输量、降低网络 I/O,从而提高整体的性能。生产者可以通过配置 compression.type 参数来开启压缩功能。例如,腾讯云提供的 Kafka 相关产品中,在生产者端发送消息时可以选择合适的压缩算法,提高数据传输效率。
(六)建立索引
每个日志分段文件对应了两个索引文件,主要用来提高查找消息的效率,这也是提升性能的一种方式。偏移量索引文件用来建立消息偏移量与物理地址之间的映射关系,时间戳索引文件则是用来建立时间戳与偏移量的映射关系。通过索引文件,可以采用二分法快速定位到小于指定偏移量的最大偏移量,然后根据对应的物理地址继续查找对应偏移量的消息。
(七)分区策略
分区是提升 Kafka 性能的一种非常有效的方式。分区可以让数据分布在不同的节点上,实现负载均衡,允许多个消费者并行地读取数据,提高处理效率。然而,一昧地增加分区并不能一直带来性能的提升。例如,如果分区数过多,会带来资源管理上的消耗,清除日志时间变长,集群 broker 故障后分区 leader 重选时间变长,客户端消费端线程数需求增加,甚至导致连接所需的 socket 消耗增加。
(八)一致性优化
绝大多数的资料在讲述 Kafka 性能优化的举措之时是不会提及一致性的东西。Kafka 采用类似 PacificA 的做法,不是“拍大腿”拍出来的,采用这种模型会提升整体的效率。具体的细节有待进一步分析,类似《在 Kafka 中使用 Raft 替换 PacificA 的可行性分析及优缺点》。
(九)顺序写盘
操作系统可以针对线性读写做深层次的优化,比如预读和后写技术。Kafka 在设计时采用了文件追加的方式来写入消息,即只能在日志文件的尾部追加新的消息,并且也不允许修改已写入的消息,这种方式属于典型的顺序写盘的操作。即使 Kafka 使用磁盘作为存储介质,它所能承载的吞吐量也不容小觑。
(十)页缓存优化
页缓存是操作系统实现的一种主要的磁盘缓存,以此用来减少对磁盘 I/O 的操作。具体来说,就是把磁盘中的数据缓存到内存中,把对磁盘的访问变为对内存的访问。当一个进程准备读取磁盘上的文件内容时,操作系统会先查看待读取的数据所在的页是否在页缓存中,如果存在则直接返回数据,从而避免了对物理磁盘的 I/O 操作。Kafka 性能高的一个重要原因就是利用了页缓存这一层的优化。
三、Kafka 应用场景
(一)消息系统
Kafka 作为传统消息系统的替代者,具有显著的优势。与传统消息系统相比,Kafka 有更好的吞吐量和可用性,能够轻松处理大规模消息。例如,在一些大型企业的分布式系统中,Kafka 可以解耦数据生产者和消费者,缓存未处理的消息,确保数据的高效传输。根据实际应用经验,通常消息传递对吞吐量要求较低,但可能要求较低的端到端延迟,而 Kafka 可靠的 durable 机制能够满足这一需求。在这方面,Kafka 可以与传统的消息传递系统如 ActiveMQ 和 RabbitMQ 相媲美。
(二)存储系统
Kafka 将数据落地到磁盘上,并且有冗余备份,这使得它能够保证数据的可用性。同时,Kafka 允许 client 自行控制读取位置,因此可以认为它是一种特殊的文件系统,能够提供高性能、低延迟、高可用的日志提交存储。例如,一些金融机构利用 Kafka 的存储功能,将交易数据存储在 Kafka 中,以便后续的分析和审计。
(三)日志聚合
Kafka 常被用来替代其他日志聚合解决方案。与 Scribe、Flume 相比,Kafka 提供同样好的性能、更健壮的堆积保障和更低的端到端延迟。在与 ELK(Elasticsearch、Logstash、Kibana)配合使用时,Kafka 主要起到 buffer 的作用,必要时可进行日志的汇流。例如,在一个大型电商平台中,Kafka 可以收集各个服务器的日志,然后将这些日志传输到 Logstash 进行处理,最后存储在 Elasticsearch 中,以便通过 Kibana 进行可视化分析。
(四)系统监控与报警
在系统监控与报警方面,Kafka 发挥着重要作用。它可以收集系统指标进行监控和故障排除,与日志分析系统类似,但区别在于指标是结构化数据,而日志是非结构化文本。指标数据发送到 Kafka 并在 Flink 中聚合,聚合数据由实时监控仪表板和警报系统(例如 PagerDuty)使用。例如,在一个云计算平台中,Kafka 可以收集各个服务器的性能指标,如 CPU 使用率、内存使用率等,然后将这些数据传输到 Flink 进行实时分析,当指标超过阈值时,触发警报系统进行报警。
(五)Commit Log
Kafka 可充当分布式系统的外部提交日志。日志有助于在节点之间复制数据,并充当故障节点恢复数据的重新同步机制。Kafka 中的日志压缩功能有助于支持这种用法。例如,在一个分布式数据库系统中,Kafka 可以作为提交日志,记录数据库的变更,以便在节点故障时进行数据恢复。
(六)跟踪网站活动 - 推荐系统
Kafka 将用户行为跟踪管道重构为实时发布 - 订阅源。把网站活动(浏览网页、搜索或其他的用户操作)发布到中心 topics 中,每种活动类型对应一个 topic。基于这些订阅源,可以实现一系列用例,如实时处理、实时监视、批量地将 Kafka 的数据加载到 Hadoop 或离线数仓系统,进行离线数据处理并生成报告。每个用户浏览网页时都生成了许多活动信息,因此活动跟踪的数据量通常非常大。像亚马逊这样的电子商务网站使用过去的行为和相似的用户来计算产品推荐。Kafka 传输原始点击流数据,Flink 对其进行处理,模型训练则使用来自数据湖的聚合数据。这使得能够持续改进每个用户的推荐的相关性。Kafka 的另一个重要用例是实时点击流分析。
(七)流处理 - kafka stream API
Kafka 通过 Streams API 提供轻量但功能强大的流处理。Streams API 帮助解决流引用中的棘手问题,比如处理无序的数据、代码变化后再次处理数据、进行有状态的流式计算等。Streams API 的流处理包含多个阶段,从 input topics 消费数据,做各种处理,将结果写入到目标 topic。Streans API 基于 kafka 提供的核心原语构建,它使用 kafka consumer、producer 来输入、输出,用 Kafka 来做状态存储。虽然 flink、spark streaming、Storm 本是正统流处理框架,但 Kafka 在流处理中更多扮演流存储角色。
(八)CDC(Change data capture)
Kafka 是构建 data pipeline 的绝佳工具,它可以将数据库变化流式传输到其他系统,以进行复制或缓存/索引更新。例如,在一个企业的数据仓库系统中,Kafka 可以将数据库的变更实时传输到数据仓库中,以便进行实时分析和报表生成。
(九)系统迁移升级
在系统迁移升级方面,利用 MQ(如 Kafka)可以降低遗留服务升级的风险。例如,为升级订单服务,更新旧的订单服务以消费来自 Kafka 的输入并将结果写入 ORDER topic。新订单服务使用相同的输入并将结果写入 ORDERNEW topic。然后,通过调节服务比较 ORDER 和 ORDERNEW。如果它们相同,则新服务通过测试。
(十)事件溯源
Kafka 作为主要事件存储,捕获一系列事件中状态的变化。如果发生任何故障、回滚或需要重建状态,可随时重新应用 Kafka 中的事件。例如,在一个微服务架构中,Kafka 可以记录微服务间的事件,如订单创建、支付完成等,实现业务逻辑的协调和同步。当出现故障时,可以通过重新应用 Kafka 中的事件来重建系统状态。
四、Kafka 与其他消息队列比较
(一)Kafka 与 RabbitMQ 对比
- 架构模型:RabbitMQ 遵循 AMQP 协议,以 broker 为中心,确保消息的可靠传递和灵活的路由机制。在 RabbitMQ 中,消息生产者将消息发送到 broker,broker 根据预先配置的交换器和队列规则将消息路由到相应的消费者。而 Kafka 以 consumer 为中心,无消息确认机制,消费者主动从 Kafka 集群中拉取消息进行处理。
- 吞吐量:Kafka 具有高吞吐量,这得益于其内部采用的消息批量处理和零拷贝机制。Kafka 可以将多个消息打包成一个批次进行传输和存储,减少了网络开销和磁盘 I/O 操作。而 RabbitMQ 稍逊一筹,不支持批量操作,每个消息都需要单独进行处理和传递。
- 可用性:RabbitMQ 支持 mirror queue,通过在不同的节点上创建镜像队列来实现高可用性。当主队列出现故障时,镜像队列可以自动切换为主队列,继续接收和处理消息。而 Kafka 的 broker 支持主备模式,每个分区都有一个 leader 和多个 follower,leader 负责处理读写请求,follower 同步 leader 的数据。当 leader 出现故障时,follower 会自动选举出一个新的 leader,继续提供服务。
- 集群负载均衡:Kafka 采用 zookeeper 管理集群,通过 zookeeper 实现 broker 的自动发现和负载均衡。Kafka 可以随机或轮询发送消息,将消息均匀地分布到不同的 broker 上,提高集群的吞吐量和可用性。而 RabbitMQ 需要单独的 loadbalancer 来实现负载均衡,增加了系统的复杂性和维护成本。
(二)Kafka 与 ActiveMQ 对比
- 性能:Kafka 高吞吐,适合大数据处理。Kafka 以其高效的存储和处理机制,能够在大规模分布式环境下处理海量数据。其内部采用了分布式架构、分区和副本机制,以及高效的网络通信和存储技术,使得 Kafka 能够实现高吞吐量和低延迟的消息处理。而 ActiveMQ 单机吞吐量较低,在处理大规模数据时可能会出现性能瓶颈。
- 消息安全性:Kafka 有副本机制,消息存在于 Leader 的 log 和 Replica 的内存中,确保了消息的高可用性和可靠性。即使某个 broker 出现故障,消息也不会丢失,因为副本可以自动切换为主节点继续提供服务。而 ActiveMQ 存在数据丢失风险,在某些情况下,如服务器故障或网络问题,可能会导致消息丢失。
- 服务器稳定性容错性:Kafka 天然 HA,broker 支持集群和副本机制,能够自动处理节点故障和数据恢复。Kafka 的分布式架构和副本机制使得它具有很高的容错性和稳定性,能够在大规模分布式环境下稳定运行。而 ActiveMQ 稳定性良好但相对较弱,在处理大规模数据和高并发请求时,可能会出现性能下降或故障。
- 吞吐量:Kafka 性能卓越,能够在单机上实现每秒数十万条消息的吞吐量。而 ActiveMQ 不适合大规模消息处理,其吞吐量相对较低,在处理大规模数据时可能会出现性能瓶颈。
(三)Kafka 与 RocketMQ 对比
- 性能:Kafka 和 RocketMQ 都具有高吞吐量和低延迟,能够满足大规模分布式系统的需求。在性能方面,两者都采用了分布式架构、分区和副本机制,以及高效的存储和处理技术,使得它们能够实现高吞吐量和低延迟的消息处理。然而,RocketMQ 在分布式事务支持方面更优,能够更好地满足企业级应用的需求。
- 社区活跃度:Kafka 用户基数庞大,生态丰富。Kafka 作为一款开源的分布式消息队列系统,拥有庞大的用户群体和活跃的社区。社区不断推出新的功能和优化,使得 Kafka 能够不断适应新的应用场景和需求。而 RocketMQ 社区活跃度略逊一筹,虽然也在不断发展和完善,但在用户基数和生态丰富度方面还有一定的差距。
- 学习曲线:RocketMQ 部分高级特性需要一定学习和实践经验。RocketMQ 提供了丰富的功能和特性,如分布式事务、消息过滤、顺序消息等。然而,这些高级特性需要一定的学习和实践经验才能掌握和应用。相比之下,Kafka 的学习曲线相对较平缓,更容易上手和使用。
五、Kafka 工作原理
(一)生产者发布消息
应用程序作为生产者将消息发布到指定主题,可选择发送到特定分区或使用默认分区选择策略。生产者在发送消息时,首先会经过一系列的步骤。例如,根据Kafka生产者是如何发送消息的?_kafka生产者发送消息-CSDN博客,生产者调用send方法发送消息后,会先经过一层拦截器,接着进入序列化器对消息的Key和Value进行序列化。然后通过分区器选择消息的分区,若指定了分区partition,消息会发送到指定的分区上;若存在Key,会采用将Key的hash值与分区数取余的方式得到指定分区;若只存在Value,Kafka 内部会采用Sticky partition,随机选择一个分区使用。消息会进入到一个名为RecordAccumulator的缓冲队列,当满足消息累计达到batch.size(默认是 16kb)或者等待时间达到linger.ms(默认是 0 毫秒)这两个条件的任意一个之后,消息由sender线程发送。Sender线程首先会通过sender读取数据,并创建发送的请求,针对 Kafka 集群里的每一个Broker,都会有一个InFlightRequests请求队列存放在NetWorkClient中,默认每个InFlightRequests请求队列中缓存 5 个请求。接着这些请求就会通过Selector发送到 Kafka 集群中。
(二)消息存储
Kafka 将消息持久化存储在主题的一个或多个分区中,每个分区是有序的不可变消息日志。Kafka 中的消息是以文件的方式持久化到磁盘中进行存储的。消息存储采用分区的方式,每个主题下的消息都会被划分成多个分区进行管理。每个分区都是一个有序的、不变的消息队列,消息按照追加的顺序被添加到队列尾部。分区会被进一步划分成多个Segment,Segment是逻辑上的文件组,方便进行数据的管理和查找。每个Segment里都包含多个文件,这些文件名相同且被集合在一起,包括索引文件和数据文件。索引文件存储了当前数据文件的索引信息,而数据文件则存储了当前索引文件名对应的数据信息。消息的偏移量信息则会被记录在索引文件中,Kafka 中的每个消息都会被分配到一个特定的分区中,然后根据分区内的Segment划分,被存储到对应的数据文件中。当消费者读取消息时,可以通过偏移量信息来确定需要从哪个位置开始读取。
(三)消息复制
支持多副本复制机制,提供高可用性和容错性,防止 Broker 故障导致消息丢失。Kafka 的复制是以分区为粒度的,分区的预写日志被复制到n个服务器。在n个副本中,一个副本作为 leader,其他副本成为 followers。消息会先发送到 leader 副本,然后 follower 副本才能从 leader 副本中拉取消息进行同步。同步期间内 follower 副本相对于 leader 副本而言会有一定程度的滞后。“一定程度的同步”是指可忍受的滞后范围,这个范围可以通过参数进行配置。AR=ISR+OSR,其中ISR是所有与 leader 副本保持一定程度同步的副本(包括 leader 副本在内),OSR是与 leader 副本同步滞后过多的副本。在正常情况下,所有的 follower 副本都应该与 leader 副本保持一定程度的同步,即AR=ISR,OSR集合为空。leader 副本负责维护和跟踪ISR集合中所有 follower 副本的滞后状态,当 follower 副本落后太多或失效时,leader 副本会把它从ISR集合中剔除。如果OSR集合中有 follower 副本“追上”了 leader 副本,那么 leader 副本会把它从OSR集合转移至ISR集合。默认情况下,当 leader 副本发生故障时,只有在ISR集合中的副本才有资格被选举为新的 leader,而在OSR集合中的副本则没有任何机会(这个原则也可以通过修改相应的参数配置来改变)。
(四)消费者订阅主题
消费者以不同消费组形式订阅主题,从 Broker 拉取消息并处理,维护自己的偏移量。消费者可使用subscribe()方法订阅一个主题,订阅主题时,既可以以集合的形式订阅多个主题,也可以以正则表达式的形式订阅特定模式的主题。对于以正则表达式的形式订阅主题,在之后的过程中,如果有人又创建了新的主题,并且主题的名字与正则表达式相匹配,那么这个消费者就可以消费到新添加的主题中的消息。除了以主题的形式订阅外,Kafka 提供了可以更细致的订阅主题下某个分区的功能,所设计的函数就是assign。此方法只接受一个参数partitions,用来指定需要订阅的分区集合。
(五)消费者消费消息
消费者通过轮询或订阅通知方式从 Broker 中拉取消息,控制消息提交方式实现不同语义的消费。消费者从 Kafka 集群中拉取消息后,可以选择自动提交偏移量或手动提交偏移量。如果采用自动提交偏移量,Kafka 会在一定的时间间隔内自动提交消费者的偏移量,这种方式比较方便,但可能会导致消息重复消费的情况。如果采用手动提交偏移量,消费者可以在处理完消息后,根据实际情况决定何时提交偏移量,这种方式可以更好地控制消息的消费语义,但需要开发者自己管理偏移量的提交。
(六)消费者组协调
协调分配消息给每个消费者组中的消费者,确保每个分区的消息只能被一个消费者组中的一个消费者消费。在多个消费者的情况下,Kafka 可以根据分区分配策略来自动分配各个消费者与分区的关系。当消费组内的消费者增加或减少时,分区分配关系会自动调整,以实现消费负载均衡及故障自动转移。这一点从subscribe()方法中有ConsumerRebalanceListener类型参数,而assign()方法却没有可以看出端倪。
(七)动态扩展和水平伸缩
通过增加 Broker 节点或分区来增加吞吐量和容量,实现动态扩展和水平伸缩。Kafka 的集群动态扩容和缩容可以通过以下步骤实现:扩容时,在集群中添加新的 Kafka 节点,将新节点的地址添加到集群的 Broker 列表中,在 Topic 的分区分配中为新节点添加分区,可以使用 Kafka 的分区重分配工具(例如kafka-reassign-partitions.sh)为新节点添加分区,以便新节点可以参与数据的读写和复制。缩容时,从集群中移除要缩容的 Kafka 节点,将其从集群的 Broker 列表中移除,执行分区重分配操作,将该节点上的分区重新分配给其他节点,以确保数据的完整性和可用性。在增加分区的场景下,可以使用--alter就能实现,将原来的分区改成更多分区。在增加副本时,可以创建 json 文件,使用指定的 json 文件增加 topic 的副本数,使用指定的 json 文件查看 topic 的副本数增加的进度。