消息顺序写入磁盘
磁盘大多数都还是机械结构(ssd不在讨论的范围内),如果将消息以随机写的方式存入磁盘,就需要按柱面、磁头、扇区的方式寻址,寻址是一个“机械动作”也最耗时。为了提高读写硬盘的速度,kafka就是使用顺序i/o。
图 1 kafka顺序io
上图中,每个partition就是一个文件,每条消息都被append 到该 partition 中,属于顺序写磁盘,因此效率非常高。这种方法有一个缺陷—— 没有办法删除数据 ,所以kafka是不会删除数据的,它会把所有的数据都保留下来,每个消费者(consumer)对每个topic都有一个offset用来表示读取到了第几条数据 。
关于磁盘顺序读写和随机读写的性能,引用一组kafka官方给出的测试数据(raid-5,7200rpm):
sequence i/o: 600mb/s
random i/o: 100kb/s
所以通过只做sequence i/o,给kafka带来了性能的极大提升。
zero copy
考虑一个web程序读取文件内容并传输到网络的场景,实现的核心代码如下:
图 2 普通read方法
虽然只是两个调用,但却经过了4次copy,其中有2次cpu copy,还有多次用户态与内核态的上下文切换,这会加重cpu的负担,而零拷贝就是为了解决这种低效。
# mmap:
减少拷贝次数的一种方法是调用mmap()来代替read()调用:
应用程序调用mmap(),磁盘上的数据会通过dma被拷贝到内核缓冲区,接着操作系统会把这段内核缓冲区与应用程序共享,这样就不需要把内核缓冲区的内容往用户空间拷贝。应用程序再调用write(),操作系统直接将内核缓冲区的内容拷贝到socket缓冲区中,最后再把数据发到网卡去。
图 3 mmap方法
使用mmap可以减少一次cpu copy,但也会遇到一些陷阱,当你的程序map了一个文件,但是当这个文件被另一个进程截断(truncate)时, write系统调用会因为访问非法地址而被sigbus信号终止。通常可以通过,为sigbus信号建立信号处理程序或使用文件租凭(file leasing)的方式去解决,这里就不再赘述了。
# sendfile:
从2.1版内核开始,linux引入了sendfile来简化操作
图 4 sendfile方法
sendfile() 方法引发 dma 引擎将文件内容拷贝到一个读取缓冲区(dma copy)然后由内核将数据拷贝到socket buffer(cpu copy)最后再拷贝到网卡(dma copy)使用sendfile不仅减少了数据拷贝的次数,还减少了上下文切换,数据传送始终只发生在kernel space
聊到这里,sendfile至少还需要一次cpu copy,那么这一步能不能省去呢为了消除内核完成的所有数据复制,我们需要一个支持收集(gather)操作的网络接口。同时,在内核版本2.4中,也修改了套接字缓冲区描述符以适应零拷贝要求。 这种方法不仅减少了多个上下文切换,还完全取消了cpu copy。
图 5 sendfile方法(dma gather)
sendfile系统调用利用dma引擎将文件内容拷贝到内核缓冲区去,然后将带有文件位置和长度信息的缓冲区描述符添加socket缓冲区去,这一步不会将内核中的数据拷贝到socket缓冲区中,dma引擎会将内核缓冲区的数据拷贝到协议引擎中去,避免了最后一次cpu拷贝。
零拷贝技术非常普遍,java的transferto、transferfrom方法就是zero copy。
AiChinaTech