Kafka 核心全面总结,高可靠高性能核心原理探究(下篇)
Kafka 高性能探究
Kafka 高性能的核心是保障系统低延迟、高吞吐地处理消息,为此,Kafaka 采用了许多精妙的设计:
- 异步发送
- 批量发送
- 压缩技术
- Pagecache 机制&顺序追加落盘
- 零拷贝
- 稀疏索引
- broker & 数据分区
- 多 reactor 多线程网络模型
异步发送
如上文所述,Kafka 提供了异步和同步两种消息发送方式。在异步发送中,整个流程都是异步的。调用异步发送方法后,消息会被写入 channel,然后立即返回成功。Dispatcher 协程会从 channel 轮询消息,将其发送到 Broker,同时会有另一个异步协程负责处理 Broker 返回的结果。同步发送本质上也是异步的,但是在处理结果时,同步发送通过 waitGroup 将异步操作转换为同步。使用异步发送可以最大化提高消息发送的吞吐能力。
批量发送
Kafka 支持批量发送消息,将多个消息打包成一个批次进行发送,从而减少网络传输的开销,提高网络传输的效率和吞吐量。Kafka 的批量发送消息是通过以下两个参数来控制的:
- batch.size:控制批量发送消息的大小,默认值为 16KB,可适当增加 batch.size 参数值提升吞吐。但是,需要注意的是,如果批量发送的大小设置得过大,可能会导致消息发送的延迟增加,因此需要根据实际情况进行调整。
- linger.ms:控制消息在批量发送前的等待时间,默认值为 0。当 linger.ms 大于 0 时,如果有消息发送,Kafka 会等待指定的时间,如果等待时间到达或者批量大小达到 batch.size,就会将消息打包成一个批次进行发送。可适当增加 linger.ms 参数值提升吞吐,比如 10 ~ 100。
在 Kafka 的生产者客户端中,当发送消息时,如果启用了批量发送,Kafka 会将消息缓存到缓冲区中。当缓冲区中的消息大小达到 batch.size 或者等待时间到达 linger.ms 时,Kafka 会将缓冲区中的消息打包成一个批次进行发送。如果在等待时间内没有达到 batch.size,Kafka 也会将缓冲区中的消息发送出去,从而避免消息积压。
压缩技术
Kafka 支持压缩技术,通过将消息进行压缩后再进行传输,从而减少网络传输的开销(压缩和解压缩的过程会消耗一定的 CPU 资源,因此需要根据实际情况进行调整。),提高网络传输的效率和吞吐量。Kafka 支持多种压缩算法,在 Kafka2.1.0 版本之前,仅支持 GZIP,Snappy 和 LZ4,2.1.0 后还支持 Zstandard 算法(Facebook 开源,能够提供超高压缩比)。这些压缩算法性能对比(两指标都是越高越好)如下:
- 吞吐量:LZ4>Snappy>zstd 和 GZIP,压缩比:zstd>LZ4>GZIP>Snappy。
在 Kafka 中,压缩技术是通过以下两个参数来控制的:
- compression.type:控制压缩算法的类型,默认值为 none,表示不进行压缩。
- compression.level:控制压缩的级别,取值范围为 0-9,默认值为-1。当值为-1 时,表示使用默认的压缩级别。
在 Kafka 的生产者客户端中,当发送消息时,如果启用了压缩技术,Kafka 会将消息进行压缩后再进行传输。在消费者客户端中,如果消息进行了压缩,Kafka 会在消费消息时将其解压缩。注意:Broker 如果设置了和生产者不通的压缩算法,接收消息后会解压后重新压缩保存。Broker 如果存在消息版本兼容也会触发解压后再压缩。
Pagecache 机制&顺序追加落盘
kafka 为了提升系统吞吐、降低时延,Broker 接收到消息后只是将数据写入PageCache后便认为消息已写入成功,而 PageCache 中的数据通过 linux 的 flusher 程序进行异步刷盘(避免了同步刷盘的巨大系统开销),将数据顺序追加写到磁盘日志文件中。由于 pagecache 是在内存中进行缓存,因此读写速度非常快,可以大大提高读写效率。顺序追加写充分利用顺序 I/O 写操作,避免了缓慢的随机 I/O 操作,可有效提升 Kafka 吞吐。
如上图所示,消息被顺序追加到每个分区日志文件的尾部。
零拷贝
Kafka 中存在大量的网络数据持久化到磁盘(Producer 到 Broker)和磁盘文件通过网络发送(Broker 到 Consumer)的过程,这一过程的性能直接影响 Kafka 的整体吞吐量。传统的 IO 操作存在多次数据拷贝和上下文切换,性能比较低。Kafka 利用零拷贝技术提升上述过程性能,其中网络数据持久化磁盘主要用 mmap 技术,网络数据传输环节主要使用 sendfile 技术。
索引加速之 mmap
传统模式下,数据从网络传输到文件需要 4 次数据拷贝、4 次上下文切换和两次系统调用。如下图所示:
为了减少上下文切换以及数据拷贝带来的性能开销,Kafka使用mmap来处理其索引文件。Kafka中的索引文件用于在提取日志文件中的消息时进行高效查找。这些索引文件被维护为内存映射文件,这允许Kafka快速访问和搜索内存中的索引,从而加速在日志文件中定位消息的过程。mmap 将内核中读缓冲区(read buffer)的地址与用户空间的缓冲区(user buffer)进行映射,从而实现内核缓冲区与应用程序内存的共享,省去了将数据从内核读缓冲区(read buffer)拷贝到用户缓冲区(user buffer)的过程,整个拷贝过程会发生 4 次上下文切换,1 次CPU 拷贝和 2次 DMA 拷贝。
网络数据传输之 sendfile
传统方式实现:先读取磁盘、再用 socket 发送,实际也是进过四次 copy。如下图所示:
为了减少上下文切换以及数据拷贝带来的性能开销,Kafka 在 Consumer 从 Broker 读数据过程中使用了 sendfile 技术。具体在这里采用的方案是通过 NIO 的 transferTo/transferFrom
调用操作系统的 sendfile 实现零拷贝。总共发生 2 次内核数据拷贝、2 次上下文切换和一次系统调用,消除了 CPU 数据拷贝,如下:
稀疏索引
为了方便对日志进行检索和过期清理,kafka 日志文件除了有用于存储日志的.log 文件,还有一个位移索引文件.index和一个时间戳索引文件.timeindex 文件,并且三文件的名字完全相同,如下:
Kafka 的索引文件是按照稀疏索引的思想进行设计的。稀疏索引的核心是不会为每个记录都保存索引,而是写入一定的记录之后才会增加一个索引值,具体这个间隔有多大则通过 log.index.interval.bytes 参数进行控制,默认大小为 4 KB,意味着 Kafka 至少写入 4KB 消息数据之后,才会在索引文件中增加一个索引项。可见,单条消息大小会影响 Kakfa 索引的插入频率,因此 log.index.interval.bytes 也是 Kafka 调优一个重要参数值。由于索引文件也是按照消息的顺序性进行增加索引项的,因此 Kafka 可以利用二分查找算法来搜索目标索引项,把时间复杂度降到了 O(lgN),大大减少了查找的时间。
位移索引文件.index
位移索引文件的索引项结构如下:
相对位移:保存于索引文件名字上面的起始位移的差值,假设一个索引文件为:00000000000000000100.index,那么起始位移值即 100,当存储位移为 150 的消息索引时,在索引文件中的相对位移则为 150 - 100 = 50,这么做的好处是使用 4 字节保存位移即可,可以节省非常多的磁盘空间。
文件物理位置:消息在 log 文件中保存的位置,也就是说 Kafka 可根据消息位移,通过位移索引文件快速找到消息在 log 文件中的物理位置,有了该物理位置的值,我们就可以快速地从 log 文件中找到对应的消息了。下面我用图来表示 Kafka 是如何快速检索消息:
假设 Kafka 需要找出位移为 3550 的消息,那么 Kafka 首先会使用二分查找算法找到小于 3550 的最大索引项:[3528, 2310272],得到索引项之后,Kafka 会根据该索引项的文件物理位置在 log 文件中从位置 2310272 开始顺序查找,直至找到位移为 3550 的消息记录为止。
时间戳索引文件.timeindex
Kafka 在 0.10.0.0 以后的版本当中,消息中增加了时间戳信息,为了满足用户需要根据时间戳查询消息记录,Kafka 增加了时间戳索引文件,时间戳索引文件的索引项结构如下:
时间戳索引文件的检索与位移索引文件类似,如下快速检索消息示意图:
broker & 数据分区
Kafka 集群包含多个 broker。一个 topic 下通常有多个 partition,partition 分布在不同的 Broker 上,用于存储 topic 的消息,这使 Kafka 可以在多台机器上处理、存储消息,给 kafka 提供给了并行的消息处理能力和横向扩容能力。
多 reactor 多线程网络模型
多 Reactor 多线程网络模型 是一种高效的网络通信模型,可以充分利用多核 CPU 的性能,提高系统的吞吐量和响应速度。Kafka 为了提升系统的吞吐,在 Broker 端处理消息时采用了该模型,示意如下:
SocketServer和KafkaRequestHandlerPool是其中最重要的两个组件:
- SocketServer:实现 Reactor 模式,用于处理多个 Client(包括客户端和其他 broker 节点)的并发请求,并将处理结果返回给 Client
- KafkaRequestHandlerPool:Reactor 模式中的 Worker 线程池,里面定义了多个工作线程,用于处理实际的 I/O 请求逻辑。
整个服务端处理请求的流程大致分为以下几个步骤:
- Acceptor 接收客户端发来的请求
- 轮询分发给 Processor 线程处理
- Processor 将请求封装成 Request 对象,放到 RequestQueue 队列
- KafkaRequestHandlerPool 分配工作线程,处理 RequestQueue 中的请求
- KafkaRequestHandler 线程处理完请求后,将响应 Response 返回给 Processor 线程
- Processor 线程将响应返回给客户端
其他知识探究
负载均衡
生产者负载均衡
Kafka 生产端的负载均衡主要指如何将消息发送到合适的分区。Kafka 生产者生产消息时,根据分区器将消息投递到指定的分区中,所以 Kafka 的负载均衡很大程度上依赖于分区器。Kafka 默认的分区器是 Kafka 提供的 DefaultPartitioner。它的分区策略是根据 Key 值进行分区分配的:
- 如果 key 不为 null:对 Key 值进行 Hash 计算,从所有分区中根据 Key 的 Hash 值计算出一个分区号;拥有相同 Key 值的消息被写入同一个分区,顺序消息实现的关键;
- 如果 key 为 null:消息将以轮询的方式,在所有可用分区中分别写入消息。如果不想使用 Kafka 默认的分区器,用户可以实现 Partitioner 接口,自行实现分区方法。
消费者负载均衡
在 Kafka 中,每个分区(Partition)只能由一个消费者组中的一个消费者消费。当消费者组中有多个消费者时,Kafka 会自动进行负载均衡,将分区均匀地分配给每个消费者。在 Kafka 中,消费者负载均衡算法可以通过设置消费者组的 partition.assignment.strategy 参数来选择。目前主流的分区分配策略以下几种:
- range:在保证均衡的前提下,将连续的分区分配给消费者,对应的实现是 RangeAssignor;
- round-robin:在保证均衡的前提下,轮询分配,对应的实现是 RoundRobinAssignor;
- 0.11.0.0 版本引入了一种新的分区分配策略 StickyAssignor,其优势在于能够保证分区均衡的前提下尽量保持原有的分区分配结果,从而避免许多冗余的分区分配操作,减少分区再分配的执行时间。
集群管理
Kafka 借助 ZooKeeper 进行集群管理。Kafka 中很多信息都在 ZK 中维护,如 broker 集群信息、consumer 集群信息、 topic 相关信息、 partition 信息等。Kafka 的很多功能也是基于 ZK 实现的,如 partition 选主、broker 集群管理、consumer 负载均衡等,限于篇幅本文将不展开陈述,这里先附一张网上截图大家感受下:
参考文献
- https://www.cnblogs.com/arvinhuang/p/16437948.html
- https://segmentfault.com/a/1190000039133960
- http://matt33.com/2018/11/04/kafka-transaction/
- https://blog.51cto.com/u_14020077/5836698
- https://t1mek1ller.github.io/2020/02/15/kafka-leader-epoch/
- https://cwiki.apache.org/confluence/display/KAFKA/KIP-101+-+Alter+Replication+Protocol+to+use+Leader+Epoch+rather+than+High+Watermark+for+Truncation
- https://xie.infoq.cn/article/c06fea629926e2b6a8073e2f0
- https://xie.infoq.cn/article/8191412c8da131e78cbfa6600
- https://mp.weixin.qq.com/s/iEk0loXsKsMO_OCVlUsk2Q
- https://cloud.tencent.com/developer/article/1657649
- https://www.cnblogs.com/vivotech/p/16347074.html
文章转载自公众号:码哥字节