Kafka性能篇:为何Kafka这么"快"?
网络 磁盘 复杂度
并发 压缩 批量 缓存 算法
Producer Broker Consumer
提出问题
> 列出问题点
> 列出优化方法
> 列出具体可切入的点
> tradeoff和细化实现
。“ 65 哥:不行啊,我很笨,也很懒,你还是直接和我说吧,我白嫖比较行。 ”
“ 65 哥:人家 Redis 是基于纯内存的系统,你 kafka 还要读写磁盘,能比? ”
为什么说写磁盘慢?
“ 65 哥:鬼还留着哦,课程还没上到一半书就没了。要不是考试俺眼神好,估计现在还没毕业。 ”
寻道
、旋转
和数据传输
三个步骤。寻道时间:Tseek 是指将读写磁头移动至正确的磁道上所需要的时间。寻道时间越短,I/O 操作越快,目前磁盘的平均寻道时间一般在 3-15ms。 旋转延迟:Trotation 是指盘片旋转将请求数据所在的扇区移动到读写磁盘下方所需要的时间。旋转延迟取决于磁盘转速,通常用磁盘旋转一周所需时间的 1/2 表示。比如:7200rpm 的磁盘平均旋转延迟大约为 60*1000/7200/2 = 4.17ms,而转速为 15000rpm 的磁盘其平均旋转延迟为 2ms。 数据传输时间:Ttransfer 是指完成传输所请求的数据所需要的时间,它取决于数据传输率,其值等于数据大小除以数据传输率。目前 IDE/ATA 能达到 133MB/s,SATA II 可达到 300MB/s 的接口数据传输率,数据传输时间通常远小于前两部分消耗时间。简单计算时可忽略。
寻道
、旋转
可以极大地提高磁盘读写的性能。顺序写
文件的方式来提高磁盘写入性能。顺序写
文件,基本减少了磁盘寻道
和旋转
的次数。磁头再也不用在磁道上乱舞了,而是一路向前飞速前行。“ 65 哥:为什么 Kafka 可以使用追加写的方式呢? ”
Queue
,而 Redis 就是一个HashMap
。Queue
和Map
的区别是什么?Queue
是 FIFO 的,数据是有序的;HashMap
数据是无序的,是随机读写的。Kafka 的不可变性,有序性使得 Kafka 可以使用追加写的方式写文件。Redis
的 AOF 文件,各种数据库的WAL(Write ahead log)
机制等等。“ 所以清楚明白自身业务的特点,就可以针对性地做出优化。 ”
“ 65 哥:哈哈,这个我面试被问到过。可惜答得一般般,唉。 ”
什么是零拷贝?
readFile(buffer)
send(buffer)
第一次:读取磁盘文件到操作系统内核缓冲区; 第二次:将内核缓冲区的数据,copy 到应用程序的 buffer; 第三步:将应用程序 buffer 中的数据,copy 到 socket 网络发送缓冲区; 第四次:将 socket buffer 的数据,copy 到网卡,由网卡进行网络传输。
“ 65 哥:啊,操作系统这么傻吗?copy 来 copy 去的。 ”
零拷贝
技术,英文——Zero-Copy
。零拷贝
就是尽量去减少上面数据的拷贝次数,从而减少拷贝的 CPU 开销,减少用户态内核态的上下文切换次数,从而优化数据传输的性能。直接 I/O:数据直接跨过内核,在用户地址空间与 I/O 设备之间传递,内核只是进行必要的虚拟存储配置等辅助工作; 避免内核和用户空间之间的数据拷贝:当应用程序不需要对数据进行访问时,则可以避免将数据从内核空间拷贝到用户空间; 写时复制:数据不需要提前拷贝,而是当需要修改的时候再进行部分拷贝。
mmap
和 sendfile
的方式来实现零拷贝
。分别对应 Java 的 MappedByteBuffer
和 FileChannel.transferTo
。零拷贝
,如下:FileChannel.transferTo()
transferTo()
方法指示块设备通过 DMA 引擎将数据读取到读取缓冲区中。然后,将该缓冲区复制到另一个内核缓冲区以暂存到套接字。最后,套接字缓冲区通过 DMA 复制到 NIC 缓冲区。transferTo()
方法会使设备通过 DMA 引擎将数据读取到内核读取缓冲区中。但是,使用gather
操作时,读取缓冲区和套接字缓冲区之间没有复制。取而代之的是,给 NIC 一个指向读取缓冲区的指针以及偏移量和长度,该偏移量和长度由 DMA 清除。CPU 绝对不参与复制缓冲区。零拷贝
详情,可以详读这篇文章零拷贝 (Zero-copy) 浅析及其应用。PageCache
page cache
。consumer 消费消息时,Broker 使用 sendfile() 系统调用【对应 FileChannel.transferTo() API】,零拷贝地将数据从 page cache 传输到 broker 的 Socket buffer,再通过网络传输。page cache
中的数据会随着内核中 flusher 线程的调度以及对 sync()/fsync() 的调用写回到磁盘,就算进程崩溃,也不用担心数据丢失。另外,如果 consumer 要消费的消息不在page cache
里,才会去磁盘读取,并且会顺便预读出一些相邻的块放入 page cache,以方便下一次读取。网络模型
“ 65 哥:网络嘛,作为 Java 程序员,自然是 Netty ”
Reactor:把 IO 事件分配给对应的 handler 处理 Acceptor:处理客户端连接事件 Handler:处理非阻塞的任务
Acceptor
线程,用于处理新的连接,Acceptor
有 N 个 Processor
线程 select 和 read socket 请求,N 个 Handler
线程处理请求并相应,即处理业务逻辑。KafkaServer
设计是一个优秀的网络架构,有想了解 Java 网络编程,或需要使用到这方面技术的同学不妨去读一读源码。后续『码哥』的 Kafka 系列文章也将涉及这块源码的解读。批量与压缩
batch.size
和linger.ms
。这两个参数就和 Producer 的批量发送有关。Serialize:键和值都根据传递的序列化器进行序列化。优秀的序列化方式可以提高网络传输的效率。 Partition:决定将消息写入主题的哪个分区,默认情况下遵循 murmur2 算法。自定义分区程序也可以传递给生产者,以控制应将消息写入哪个分区。 Compress:默认情况下,在 Kafka 生产者中不启用压缩.Compression 不仅可以更快地从生产者传输到代理,还可以在复制过程中进行更快的传输。压缩有助于提高吞吐量,降低延迟并提高磁盘利用率。 Accumulate: Accumulate
顾名思义,就是一个消息累计器。其内部为每个 Partition 维护一个Deque
双端队列,队列保存将要发送的批次数据,Accumulate
将数据累计到一定数量,或者在一定过期时间内,便将数据以批次的方式发送出去。记录被累积在主题每个分区的缓冲区中。根据生产者批次大小属性将记录分组。主题中的每个分区都有一个单独的累加器 / 缓冲区。Group Send:记录累积器中分区的批次按将它们发送到的代理分组。批处理中的记录基于 batch.size 和 linger.ms 属性发送到代理。记录由生产者根据两个条件发送。当达到定义的批次大小或达到定义的延迟时间时。
分区并发
“ 65 哥:那是不是分区数越多越好呢? ”
越多的分区需要打开更多的文件句柄
客户端 / 服务器端需要使用的内存就越多
降低高可用性
文件结构
segment file 组成:由 2 大部分组成,分别为 index file 和 data file,此 2 个文件一一对应,成对出现,后缀”.index”和“.log”分别表示为 segment 索引文件、数据文件。 segment 文件命名规则:partion 全局的第一个 segment 从 0 开始,后续每个 segment 文件名为上一个 segment 文件最后一条消息的 offset 值。数值最大为 64 位 long 大小,19 位数字字符长度,没有数字用 0 填充。
mmap
的方式,直接将 index 文件映射到内存,这样对 index 的操作就不需要操作磁盘 IO。mmap
的 Java 实现对应 MappedByteBuffer
。“ 65 哥笔记:mmap 是一种内存映射文件的方法。即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用 read,write 等系统调用函数。相反,内核空间对这段区域的修改也直接反映用户空间,从而可以实现不同进程间的文件共享。 ”
按照二分法找到小于 offset 的 segment 的.log 和.index 用目标 offset 减去文件名中的 offset 得到消息在这个 segment 中的偏移量。 再次用二分法在 index 文件中找到对应的索引。 到 log 文件中,顺序查找,直到找到 offset 对应的消息。
总结
零拷贝网络和磁盘 优秀的网络模型,基于 Java NIO 高效的文件数据结构设计 Parition 并行和可扩展 数据批量传输 数据压缩 顺序读写磁盘 无锁轻量级 offset
有道无术,术可成;有术无道,止于术
欢迎大家关注Java之道公众号
好文章,我在看❤️
评论