Kafka 是公认的高性能消息件,其中有一个重要部分就是 Zero Copy。
传统数据传输
传统数据传输,以磁盘文件到 Socket 为例:
- 首先需要从文件读出数据到一个内核地址空间缓冲区,DMA 执行了一次拷贝
- 然后再被复制到用户缓冲区,这是第二次拷贝
- 发送时,将引发用户模式到内核模式的一次 Context 切换,数据从用户缓冲区到内核地址空间的 Socket 发送缓冲区,数据被第三次拷贝
- DMA 将数据从 Socket 发送缓冲区拷贝到 NIC (network interface card)
下面有一张图可以很好表达上述过程(引用图):
内核中间缓冲区在数据量小时,内核的预读算法和异步写入可以极好的提高性能。
当数据量大时,数据将在磁盘、内核缓冲区、用户缓冲区之间被拷贝多次,将成为性能瓶颈。
接下来,就到了 Zero Copy 登场的时候。
Zero Copy
看了上面传统的数据传输过程,那么有没可能避免用户和内核间的数据拷贝呢?
因为这些数据拷贝是多余的,它们应该可以直接从 Read Buffer 到 Socket Buffer。
Java 中有一个 transferTo() 方法:java.nio.channels.FileChannel#transferTo(long position, long count, WritableByteChannel target)
,
可以把数据从文件通道传到另一个可写通道。
当然,它依赖底层操作系统对零拷贝的支持,在 Linux 中是一个叫 sendfile() 的内核系统调用。
#include <sys/socket.h> |
使用 transferTo()
之后,首先 DMA 会将数据拷贝到内核缓冲区,
最后 DMA 再把 Socket Buffer 的该数据写到 NIC Buffer。
如果网卡等设备支持收集操作的话(而且是 Linux 内核 2.4 及后期版本中),还可以再避免内核中的数据复制,
直接从 Read Buffer 到 NIC buffer. 方法是将数据位置和长度等信息 (Descriptor) 追加到 Socket Buffer,
然后 DMA 通过 Descriptor 信息把指定位置的数据写到 NIC。如下图: