从高度抽象角度来看,性能问题逃不出下面三个方面:
- 网络
- 磁盘
- 复杂度
对于 kafkaf这种网络分布式队列来说,网络和磁盘更是优化的重中之重,针对上面提出的抽象问题,解决方案高度抽象出来也很简单:
- 并发
- 压缩
- 批量
- 缓存
- 算法
优化点:
1.顺序写(producer -> broker)
为什么说写磁盘慢?
完成一次磁盘IO,需要经过 寻道、旋转和数据传输三个步骤:
寻道时间:Tseek是指将读写磁头移动至正确的磁道上所需要的时间
旋转延迟: trotation是指盘片旋转将请求数据所在的扇区移动到读写磁盘下方所需要的的时间
数据传输时间: Ttransfer 是指传输所请求的数据所需要的时间
因此,如果在写磁盘的时候省去寻道、旋转可以极大地提高磁盘读写的性能
kafka采用顺序写文件的方式提高磁盘写入性能。顺序写文件,基本减少磁盘寻道和旋转的次数。
kafka中每个分区是一个有序的,不可变的消息队列,新的消息不断追加到partition的末尾,在kafka中partition只是一个逻辑概念,kafka将partition 划分为多个segment,每个segment 对应一个物理文件,kafka对segemtn文件追加写,这就是顺序写文件。
kafka本质上是一个queue,queue是FIFO,数据是有序的。kafka的不可变性,有序性使得kafka可以使用追加的方式写文件。
2、零拷贝(broker->consumer)
传统io流程:需要将数据copy四次
第一次:读取磁盘文件到操作系统内核缓冲区
第二次:将内核缓冲区的数据,copy到应用程序的buffer
第三次:将应用程序的Buffer的数据copy到socket网络发送到缓冲区
第四次:将socket buffer 的数据copy网卡,由网卡进行网络传输
零拷贝就是尽量去减少上面数据的拷贝次数,从而减少拷贝的CPU开销,减少用户态内核态的上下文切换次数,从而优化数据传输的性能。Kafka 使用到了 mmap
和 sendfile
的方式来实现零拷贝
3、pageCache(缓存)
Producer生产消息到Broker时,Broker会使用pwrite()按偏移量写入数据,此时数据都会先写入page cache。Consumer 消费消息时,Broker使用sedfile()系统调用,零拷贝将数据从page cache 传输到broker的socket buffer,再通过网络传输。
pagecache 中的数据会随着内核中的flusher线程的调度以及对sync()/fsync的调用写回磁盘。如果
consumer要消费的消息不在 page cache里,才会去磁盘读取,并且会顺便预读出一些相邻的块放入page cache,以便下一次读取。
4、网络模型
kafka自己实现了网络模型做RPC。底层基于 Java NIIO,采用Netty 一样的Reactor 线程模型
- Reactor:把 IO 事件分配给对应的 handler 处理
- Acceptor:处理客户端连接事件
- Handler:处理非阻塞的任务
5、批量与压缩
Kafka Producer向Broker发送消息不是一条一条消息的发送。kafka中producer向broker发送消息的时有两个重要的参数:Batch.size 和 linger.ms ,这两个参数就是和producer的批量发送有关。
发送消息依次经过一下处理器:
- Serialize :键和值都根据传递的序列化器进行序列化。
- Partition:决定将消息写入主体的哪个分区,默认情况下遵循murmur2算法。自定义分区程序也可以传递给生产这,以控制应将消息写入哪个分区。
- Compress:默认情况下,在Kafka生产这种不启用压缩,compression不仅可以更快地从生产这传输到代理,还可以在复制过程中进行更快的传输。压缩有助于提高吞吐量,降低延迟并提高磁盘利用率
- Accumulate: accumulate 顾名思义,就是一个消息累加器。其内部为每个partition维护一个depue双端队列,队列保存将要发送的批次数据,accumulate将数据累计到一定数量,或者在一定过期时间内,便将数据以批次的方式发送出去。
- Group Send:记录累积器中分区的批次将他们发送到代理分组。批处理中的记录基于batch.size和linger.ms属性发送到代理。
Kafka 支持多种压缩算法:lz4、snappy、gzip。Kafka 2.1.0 正式支持 ZStandard —— ZStandard 是 Facebook 开源的压缩算法,旨在提供超高的压缩比 (compression ratio)。Producer、Broker 和 Consumer 使用相同的压缩算法,在 producer 向 Broker 写入数据,Consumer 向 Broker 读取数据时甚至可以不用解压缩,最终在 Consumer Poll 到消息时才解压,这样节省了大量的网络和磁盘开销
6、分区并发
kafka的topic可以分为多个partition,每个partition 类似于一个队列,保证数据有序。同一个Group下不同的consumer并发消费partition,分区实际上是调优kafka并行度的最小单元,因此,可以说,每增加一个partition就增加一个消费并发。
kafka具有优秀的分区分配算法—StickyAssignor,可以保证分区的分配尽量地均衡,且每一次重新分配的结构与上一次分配结果保持一致。这样,整个集群的分区尽量地均衡,各个broker 和consumer 的处理不至于出行太大的倾斜。越多的分区需要打开更多的文件句柄,客户端 / 服务器端需要使用的内存就越多,会降低高可用。
7、文件结构
kafka消息是以Topic为单位进行归类,各个topic之间是彼此独立的,互不影响。每个topic又可以分为一个或多个分区。每个分区各自存在一个记录消息数据的日志文件。
Kafka每个分区日志在物理上实际按大小被分成多个Segment 。
- segment file 组成:由2大部分,分别为index file 和data file ,成对出现,后缀 .index和.log 分别表示为segment索引文件、数据文件。
- segment 文件命名规则:partition 全局的第一个segment从0开始,后续每个segment文件名为上一个segment文件最后一条消息offset值。数值最大为64位long大小,19位数字字符长度,没有数字用0填充。
index 采用稀疏索引,这样每个index文件大小有限,kafka采用mmap的方式,直接将Index文件映射到内存,这样对Index的操作就不需要操作磁盘io。
Kafka充分利用二分法来查找对应offset的消息位置:
- 按照二分法找到小于 offset 的 segment 的.log 和.index
- 用目标 offset 减去文件名中的 offset 得到消息在这个 segment 中的偏移量。
- 再次用二分法在 index 文件中找到对应的索引。
- 到 log 文件中,顺序查找,直到找到 offset 对应的消息。