Kafka 的高性能设计可以说是全方位的,从 Prodcuer 、到 Broker、再到 Consumer,


文章目录

1. 如何理解高性能设计

对于线程池、多级缓存、IO 多路复用、零拷贝等技术是一个系统性的问题,至少需要深入到操作系统层面。从 CPU 和存储入手,去了解底层的实现机制,然后再自底往上,一层一层去解密和贯穿起来。

高性能设计离不开的就是计算和IO

计算:

1、让更多的核来参与计算:比如用多线程代替单线程、用集群代替单机等。

2、减少计算量:比如用索引来取代全局扫描、用同步代替异步、通过限流来减少请求处理量、采用更高效的数据结构和算法等。

IO:

Linux 系统的 IO 栈
Kafka高性能设计_1_java
应用程序、操作系统、磁盘等各个层次来考虑性能优化

  • 1.加快IO速度: 比如用磁盘顺序写代替随机写、用 NIO 代替 BIO、用性能更好的 SSD 代替机械硬盘等。
  • 2.减少IO 次数: 比如借助系统缓存或者外部缓存、通过零拷贝技术减少 IO 复制次数、批量读写、数据压缩等。

2. Kafka 高性能设计的全景图

消息队列本质就是

发 - 存 - 消费

生产消息、存储消息、消费消息:
Kafka高性能设计_1_序列化_02

3. 生产消息的性能优化手段

Kafka高性能设计_1_分布式_03

3.1 批量发送

Kafka 作为一个消息队列,很显然是一个 IO 密集型应用,它所面临的挑战除了磁盘 IO(Broker 端需要对消息持久化),还有网络 IO(Producer 到 Broker,Broker 到 Consumer,都需要通过网络进行消息传输)。

磁盘顺序 IO 的速度其实非常快,不亚于内存随机读写。这样网络 IO 便成为了 Kafka 的性能瓶颈所在。

Kafka 采用了批量发送消息的方式,通过将多条消息按照分区进行分组,然后每次发送一个消息集合,从而大大减少了网络传输的 overhead。

3.2 消息压缩

消息压缩的目的是为了进一步减少网络传输带宽。而对于压缩算法来说,通常是:​​数据量越大,压缩效果才会越好​​。

因为有了批量发送这个前期,从而使得 Kafka 的消息压缩机制能真正发挥出它的威力(压缩的本质取决于多消息的重复性)。对比压缩单条消息,同时对多条消息进行压缩,能大幅减少数据量,从而更大程度提高网络传输率。

kafka支持3中压缩方式: gzip, snappy, lz4

Kafka高性能设计_1_网络传输_04
gzip 压缩效果最好,但是生成耗时更长,综合对比 lz4 性能最佳。

压缩消息不仅仅减少了网络 IO,它还大大降低了磁盘 IO。因为批量消息在持久化到Brocker中的磁盘时,保存的是压缩状态, 最终在Consumer段解压缩, 这种端到端的压缩设计, 大大提高了写磁盘的效率。

3.3 高效序列化

Kafka 消息中的 Key 和 Value,都支持自定义类型,只需要提供相应的序列化和反序列化器即可。因此,用户可以根据实际情况选用快速且紧凑的序列化方式(比如 ​​ProtoBuf、Avro​​)来减少实际的网络传输量以及磁盘存储量,进一步提高吞吐量。

3.4 内存池复用

前面说过 Producer 发送消息是批量的,因此消息都会先写入 Producer 的内存中进行缓冲,直到多条消息组成了一个 Batch,才会通过网络把 Batch 发给 Broker。

当这个 Batch 发送完毕后,显然这部分数据还会在 Producer 端的 JVM 内存中,由于不存在引用了,它是可以被 JVM 回收掉的。

JVM GC 时一定会存在 Stop The World 的过程,即使采用最先进的垃圾回收器,也势必会导致工作线程的短暂停顿,这对于 Kafka 这种高并发场景肯定会带来性能上的影响。

Kafka使用内存池机制, 和连接池, 线程池一样, 都是为了提高复用, 减少频繁的创建和释放

RecordAccumulator默认为32M, 一个Deque中的ProduceBatch默认为16K
发送条件: 1.batch-size:只有达到batch-size的时候sender才可以发送数据 2.linger.ms:如果数据未达到batch-size的时候, sender等待设置的时间。

Kafka高性能设计_1_kafka_05
Producer 一上来就会占用一个固定大小的内存块,比如 64MB,然后将 64 MB 划分成 M 个小内存块(比如一个小内存块大小是 16KB)。

当需要创建一个新的 Batch 时,直接从内存池中取出一个 16 KB 的内存块即可,然后往里面不断写入消息,但最大写入量就是 16 KB,接着将 Batch 发送给 Broker ,此时该内存块就可以还回到缓冲池中继续复用了,根本不涉及垃圾回收。最终整个流程如下图所示:

主线程: KafkaProducer: main主线程 -> 拦截器Interceptor -> 序列化Selializer -> 分区器Partitioner

Kafka高性能设计_1_分布式_06
sender -> NewWorkClient -> Selector -> Kafka.Broker
Kafka高性能设计_1_分布式_07

主线程: KafkaProducer: main主线程 -> 拦截器Interceptor -> 序列化Selializer -> 分区器Partitioner

Kafka高性能设计_1_kafka_08
Kafka高性能设计_1_kafka_09
Selector -> Brocker后,会有一个ack的应答: 0/1/-1 -> 应答成功的话清除掉请求, 同时清理掉分区的数据, 失败重试.重试次数默认为int的最大值。

同时Kafka支持异步发送:

同步发送:当达到条件后发送给Broker, 异步发送则是无脑发送

4.Kafka源码分析

初始化阶段:
Kafka高性能设计_1_网络传输_10
监控Kafka

Kafka高性能设计_1_序列化_11
获取分区器:
Kafka高性能设计_1_kafka_12
序列化配置:
Kafka高性能设计_1_序列化_13
配置拦截器 -> 拦截器链

Kafka高性能设计_1_分布式_14
设置单条日志大小

设置缓冲区大小

设置压缩格式

Kafka高性能设计_1_java_15
设置缓冲区:
Kafka高性能设计_1_java_16
获取Kafka集群连接和元数据

Kafka高性能设计_1_kafka_17
sender:

Kafka高性能设计_1_java_18
其中sender中有一些初始化参数:缓存请求的个数默认为5个, 请求超时时间默认为30s,

创建一个客户端对象, clienid为id,

Kafka高性能设计_1_网络传输_19