先预告一下 Kafka 系列文章,大家敬请期待哦:
以讲解性能作为 Kafka 之旅的开篇之作,让我们一起来深入了解 Kafka “快”的内部秘密。你不仅可以学习到 Kafka 性能优化的各种手段,也可以提炼出各种性能优化的方法论,这些方法论也可以应用到我们自己的项目之中,助力我们写出高性能的项目。
关公战秦琼
65: Redis 和 Kafka 完全是不同作用的中间件,有比较性吗?
是的,所以此文讲的不是《分布式缓存的选型》
,也不是《分布式中间件对比》
。我们聚焦于这两个不同领域的项目对性能的优化,看一看优秀项目对性能优化的通用手段,以及在针对不同场景下的特色的优化方式。
很多人学习了很多东西,了解了很多框架,但在遇到实际问题时,却常常会感觉到知识不足。这就是没有将学习到的知识体系化,没有从具体的实现中抽象出可以行之有效的方法论
。
学习开源项目很重要的一点就是归纳
,将不同项目的优秀实现总结出方法论,然后演绎
到自我的实践中去。
码哥寄语
理性、客观、谨慎是程序员的特点,也是优点,但是很多时候我们也需要带一点感性,带一点冲动,这个时候可以帮助我们更快的做决策。「悲观者正确、乐观者成功。」希望大家都是一个乐观地解决问题的人。
Kafka 性能全景
从高度抽象的角度来看,性能问题逃不出下面三个方面:
- 网络
- 磁盘
- 复杂度
对于 Kafka 这种网络分布式队列来说,网络和磁盘更是优化的重中之重。针对于上面提出的抽象问题,解决方案高度抽象出来也很简单:
- 并发
- 压缩
- 批量
- 缓存
- 算法
知道了问题和思路,我们再来看看,在 Kafka 中,有哪些角色,而这些角色就是可以优化的点:
- Producer
- Broker
- Consumer
是的,所有的问题,思路,优化点都已经列出来了,我们可以尽可能的细化,三个方向都可以细化,如此,所有的实现便一目了然,即使不看 Kafka 的实现,我们自己也可以想到一二点可以优化的地方。
这就是思考方式。提出问题
> 列出问题点
> 列出优化方法
> 列出具体可切入的点
> tradeoff和细化实现
。
现在,你也可以尝试自己想一想优化的点和方法,不用尽善尽美,不用管好不好实现,想一点是一点。
65 哥:不行啊,我很笨,也很懒,你还是直接和我说吧,我白嫖比较行。
顺序写
65 哥:人家 Redis 是基于纯内存的系统,你 kafka 还要读写磁盘,能比?
为什么说写磁盘慢?
我们不能只知道结论,而不知其所以然。要回答这个问题,就得回到在校时我们学的操作系统课程了。65 哥还留着课本吗?来,翻到讲磁盘的章节,让我们回顾一下磁盘的运行原理。
65 哥:鬼还留着哦,课程还没上到一半书就没了。要不是考试俺眼神好,估计现在还没毕业。
看经典大图:
完成一次磁盘 IO,需要经过寻道
、旋转
和数据传输
三个步骤。
影响磁盘 IO 性能的因素也就发生在上面三个步骤上,因此主要花费的时间就是:
- 寻道时间: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 的接口数据传输率,数据传输时间通常远小于前两部分消耗时间。简单计算时可忽略。
因此,如果在写磁盘的时候省去寻道
、旋转
可以极大地提高磁盘读写的性能。
Kafka 采用顺序写
文件的方式来提高磁盘写入性能。顺序写
文件,基本减少了磁盘寻道
和旋转
的次数。磁头再也不用在磁道上乱舞了,而是一路向前飞速前行。
Kafka 中每个分区是一个有序的,不可变的消息序列,新的消息不断追加到 Partition 的末尾,在 Kafka 中 Partition 只是一个逻辑概念,Kafka 将 Partition 划分为多个 Segment,每个 Segment 对应一个物理文件,Kafka 对 segment 文件追加写,这就是顺序写文件。
65 哥:为什么 Kafka 可以使用追加写的方式呢?
这和 Kafka 的性质有关,我们来看看 Kafka 和 Redis,说白了,Kafka 就是一个Queue
,而 Redis 就是一个HashMap
。Queue
和Map
的区别是什么?
Queue
是 FIFO 的,数据是有序的;HashMap
数据是无序的,是随机读写的。Kafka 的不可变性,有序性使得 Kafka 可以使用追加写的方式写文件。
其实很多符合以上特性的数据系统,都可以采用追加写的方式来优化磁盘性能。典型的有Redis
的 AOF 文件,各种数据库的WAL(Write ahead log)
机制等等。
所以清楚明白自身业务的特点,就可以针对性地做出优化。
零拷贝
65 哥:哈哈,这个我面试被问到过。可惜答得一般般,唉。
什么是零拷贝?
我们从 Kafka 的场景来看,Kafka Consumer 消费存储在 Broker 磁盘的数据,从读取 Broker 磁盘到网络传输给 Consumer,期间涉及哪些系统交互。Kafka Consumer 从 Broker 消费数据,Broker 读取 Log,就使用了 sendfile。如果使用传统的 IO 模型,伪代码逻辑就如下所示:
readFile(buffer) send(buffer)
如图,如果采用传统的 IO 流程,先读取网络 IO,再写入磁盘 IO,实际需要将数据 Copy 四次。
- 第一次:读取磁盘文件到操作系统内核缓冲区;
- 第二次:将内核缓冲区的数据,copy 到应用程序的 buffer;
- 第三步:将应用程序 buffer 中的数据,copy 到 socket 网络发送缓冲区;
- 第四次:将 socket buffer 的数据,copy 到网卡,由网卡进行网络传输。
65 哥:啊,操作系统这么傻吗?copy 来 copy 去的。
并不是操作系统傻,操作系统的设计就是每个应用程序都有自己的用户内存,用户内存和内核内存隔离,这是为了程序和系统安全考虑,否则的话每个应用程序内存满天飞,随意读写那还得了。
不过,还有零拷贝
技术,英文——Zero-Copy
。零拷贝
就是尽量去减少上面数据的拷贝次数,从而减少拷贝的 CPU 开销,减少用户态内核态的上下文切换次数,从而优化数据传输的性能。
常见的零拷贝思路主要有三种:
- 直接 I/O:数据直接跨过内核,在用户地址空间与 I/O 设备之间传递,内核只是进行必要的虚拟存储配置等辅助工作;
- 避免内核和用户空间之间的数据拷贝:当应用程序不需要对数据进行访问时,则可以避免将数据从内核空间拷贝到用户空间;
- 写时复制:数据不需要提前拷贝,而是当需要修改的时候再进行部分拷贝。
Kafka 使用到了 mmap
和 sendfile
的方式来实现零拷贝
。分别对应 Java 的 MappedByteBuffer
和 FileChannel.transferTo
。
使用 Java NIO 实现零拷贝
,如下:
FileChannel.transferTo()
在此模型下,上下文切换的数量减少到一个。具体而言,transferTo()
方法指示块设备通过 DMA 引擎将数据读取到读取缓冲区中。然后,将该缓冲区复制到另一个内核缓冲区以暂存到套接字。最后,套接字缓冲区通过 DMA 复制到 NIC 缓冲区。
我们将副本数从四减少到三,并且这些副本中只有一个涉及 CPU。 我们还将上下文切换的数量从四个减少到了两个。这是一个很大的改进,但是还没有查询零副本。当运行 Linux 内核 2.4 及更高版本以及支持收集操作的网络接口卡时,后者可以作为进一步的优化来实现。如下所示。
根据前面的示例,调用transferTo()
方法会使设备通过 DMA 引擎将数据读取到内核读取缓冲区中。但是,使用gather
操作时,读取缓冲区和套接字缓冲区之间没有复制。取而代之的是,给 NIC 一个指向读取缓冲区的指针以及偏移量和长度,该偏移量和长度由 DMA 清除。CPU 绝对不参与复制缓冲区。