- 顺序读写
每一个 Partition 其实都是一个文件 ,收到消息后 Kafka 会把数据插入到文件末尾,所以kafka的读写都是顺序读写,为什么磁盘顺序读写速度快?
硬盘内部主要部件为磁盘盘片、传动手臂、读写磁头和主轴马达。实际数据都是写在盘片上,读写主要是通过传动手臂上的读写磁头来完成。实际运行时,主轴让磁盘盘片转动,然后传动手臂可伸展让读取头在盘片上进行读写操作
影响硬盘性能主要有三个因素:
- 寻道时间:指将读写磁头移动至正确的磁道上所需要的时间。寻道时间越短,I/O操作越快,目前磁盘的平均寻道时间一般在3-15ms。
- 旋转延迟:指盘片旋转将请求数据所在的扇区移动到读写磁盘下方所需要的时间。旋转延迟取决于磁盘转速,通常用磁盘旋转一周所需时间的1/2表示。
- 数据传输时间:指完成传输所请求的数据所需要的时间,它取决于数据传输率,其值等于数据大小除以数据传输率。数据传输时间通常远小于前两部分消耗时间。简单计算时可忽略。
当磁盘顺序读写的时候,省去了读写磁头寻道的时间,直接可以在盘片上进行读写。一些情况下磁盘顺序读写性能甚至要高于内存随机读写
- Producer到Broker(mmap)
减少一次从内核态到用户态的CPU拷贝
- Broker到consumer(零拷贝)
传统IO
零拷贝
从 Linux 内核2.4版本之前sendfile() 系统调用会多一次CPU拷贝,从内核缓冲区拷贝到socket缓冲区。
2.4 版本开始起,对于支持网卡支持 SG-DMA 技术的情况下, sendfile() 系统调用的过程发生了点变化。
具体过程如下:
第一步,通过 DMA 将磁盘上的数据拷贝到内核缓冲区里;
第二步,缓冲区描述符和数据长度传到 socket 缓冲区,这样网卡的 SG-DMA 控制器就可以直接将内核缓存中的数据拷贝到网卡的缓冲区里,此过程不需要将数据从操作系统内核缓冲区拷贝到 socket 缓冲区中,这样就减少了一次数据拷贝。
这就是所谓的零拷贝(Zero-copy)技术,因为我们没有在内存层面去拷贝数据,也就是说全程没有通过 CPU 来搬运数据,所有的数据都是通过 DMA 来进行传输的。
零拷贝技术的文件传输方式相比传统文件传输的方式,减少了 2 次上下文切换和数据拷贝次数,只需要 2 次上下文切换和数据拷贝次数,就可以完成文件的传输,而且 2 次的数据拷贝过程,都不需要通过 CPU,2 次都是由 DMA 来搬运。
简单说一下DMA拷贝
DMA全称为直接内存访问(Direct Memory Access),
正常情况下我们要将磁盘文件中的数据拷贝到内核缓冲区需要经过5步,
- CPU 发出对应的指令给磁盘控制器,然后返回;
- 磁盘控制器收到指令后,于是就开始准备数据,
- 会把数据放入到磁盘控制器的内部缓冲区中,然后产生一个中断;
- CPU 收到中断信号后,停下手头的工作,接着把磁盘控制器的缓冲区的数据一次一个字节地读进自己的寄存器
- 然后再把寄存器里的数据写入到内存,而在数据传输的期间 CPU 是无法执行其他任务的。
使用了DMA技术后,磁盘和内存在进行数据传输时,数据搬运的工作全部交给了DMA控制器,CPU将不再参与,可以去处理其他别的事务。
- PageCache(页缓存)
如果每条消息写入都执行一次磁盘IO,性能消耗会非常大, 如果需要用到缓存,但kafka没有自己实现缓存,其依赖底层操作系统提供的PageCache功能,当上层有写操作时,操作系统只是将数据写入PageCache,同时标记Page属性为Dirty。当读操作发生时,先从PageCache中查找,如果发生缺页才进行磁盘调度,最终返回需要的数据。实际上PageCache是把尽可能多的空闲内存都当做了磁盘缓存来使用。
使用PageCache功能同时可以避免在JVM内部缓存数据,导致频繁GC带来不必要的开销。
PageCache 来缓存最近被访问的数据,当空间不足时淘汰最久未被访问的缓存。
「预读功能」
读取磁盘数据的时候,需要找到数据所在的位置,但是对于机械磁盘来说,就是通过磁头旋转到数据所在的扇区,再开始「顺序」读取数据,但是旋转磁头这个物理动作是非常耗时的,为了降低它的影响,PageCache 使用了预读功能。
假设 read 方法每次只会读 32 KB 的字节,虽然 read 刚开始只会读 0 ~ 32 KB 的字节,但内核会把其后面的 32~64 KB 也读取到 PageCache,这样后面读取 32~64 KB 的成本就很低,如果在 32~64 KB 淘汰出 PageCache 前,进程读取到它了,收益就非常大。
但因为kafka本身绝大数情况下就是顺序读,所以不存在频繁需要磁头旋转的情况,所以预读功能可能更多的用于减少IO次数。
- 通过索引查找消息过程
每个 partition 对应于一个 log 文件,通过顺序追加的方式写入日志文件,当日志大小达到一定大小(log.segment.bytes配置,还跟其他配置有关)就会切分,形成一个新的日志文件,一个个的日志文件就称为日志段,日志段的引入方便了kafka数据的查询与定位。
日志段分为活跃日志段和非活跃日志段,只有活跃日志段(当前日志段,一个分区只存在一个)可以被读写,非活跃日志段只能被读取。
1、先通过offset(二分法)找到所在的index文件。
2、index索引文件中保存了,消息的offest,绝对地址,大小,因为索引文件中每条数据的大小相同,所以定位很快,kafka还有一个配置是(log.index.interval.bytes),可以配置索引稀疏程度(稀疏索引,时间+空间),然后通过找到离目标最近的索引,再顺序遍历消息文件找到目标文件。这波操作时间复杂度为O(log2n)+O(m),n是索引文件里索引的个数,m为稀疏程度。
3、然后再到log文件中,通过绝对地址,和大小,精确地定位到消息位置。