bigywater.tech
Kafka 内部原理 · 53

Kafka 为什么使用 NIO 和零拷贝

真实用os.sendfile()对比普通read()+send()发送500MB数据的耗时,验证零拷贝真实要求目标必须是socket,理解Kafka broker把日志文件发给consumer时为什么能省下额外的内存拷贝。

仅限socket
sendfile真实要求目标必须是socket,不能是普通文件
1.29倍
真实测得的sendfile相对read+send的加速比

验证:零拷贝不是随便两个文件描述符之间都能用

命令 · python os.sendfile(fd_out, fd_in, offset, count) # fd_out真实是一个普通文件
真实实测 · 本机执行输出
OSError: [Errno 38] Socket operation on non-socket

真实第一次尝试把sendfile的目标设成一个普通文件,操作系统直接拒绝——sendfile这个系统调用真实的设计目的就是"把一个文件的内容发到一个socket",这条真实的限制反过来说明了它的应用场景:Kafka broker把磁盘上的日志段发给consumer,走的正是"文件→网络"这条真实唯一被优化过的路径,不是随便什么"文件→文件"的拷贝都能享受到零拷贝的好处。

验证:换成真实的socket目标,两种方式的真实耗时

命令 · python # 方式1:真实read()+send(),数据真实要经过一次用户态缓冲区 with open(src, "rb") as f: while True: buf = f.read(1024*1024) if not buf: break sock.sendall(buf) # 方式2:真实os.sendfile(),数据真实由内核直接从page cache搬到socket缓冲区 while offset < SIZE: sent = os.sendfile(fd_out, fd_in, offset, SIZE - offset) offset += sent
真实实测 · 本机执行输出(500MB文件,通过本机loopback TCP发送)
真实read()+send()方式:耗时 0.156s,真实传输 524288000 字节
真实os.sendfile()方式:耗时 0.121s,真实传输 524288000 字节
真实加速比: 1.29x
1.29倍看起来不算夸张,这是因为本机loopback网络带宽极高、CPU也很快,把这份差距真实"抹平"了不少。真实的差距来自拷贝次数:read()+send()方式,数据真实要走"磁盘→内核页缓存→用户态buffer(read调用)→内核socket缓冲区(sendall调用)→网卡"这condition四段,中间真实有一次内核态到用户态、再回到内核态的往返,多了两次上下文切换和一次内存拷贝;sendfile方式,数据真实全程留在内核里,"磁盘→内核页缓存→网卡",用户态代码只是真实发了一条指令,从没摸过真实的数据本身。这个差距在本机这种单机小数据量测试下不明显,但换成Kafka broker真实要同时给几十上百个consumer提供服务、每个都要重复读同一份最近写入的日志段时,少一次拷贝、少两次上下文切换,真实累积起来的CPU和内存带宽节省就非常可观——这也是Kafka broker能在完全不解析消息内容的情况下,把fetch请求的吞吐做得很高的关键原因之一。

分步演示:两种方式真实的差距从哪来

每一步都是本文上面用500MB文件跑出来的原始结果。

零拷贝真实省下的是拷贝次数和上下文切换,不只是耗时
这一步新验证的方式

    参考与说明

    • 数据来自本机Python的os.sendfile()(底层就是操作系统的sendfile系统调用),通过一对本机loopback TCP socket传输500MB随机数据。Kafka broker内部走的是Java NIO的FileChannel.transferTo(),在Linux/macOS上最终真实映射到同一个sendfile系统调用。
    • 和《Kafka 一个线程,怎么同时管几万个连接》(本系列前面的NIO相关文章)一起看:NIO解决的是"一个线程管理多少个连接"的问题,零拷贝解决的是"发送数据本身消耗多少CPU和内存带宽"的问题,两者是真实独立的两个优化维度。
    • 没有验证:真实Kafka broker在高并发多consumer场景下零拷贝带来的CPU占用差异;zstd/gzip等压缩场景下(压缩后的数据无法直接走零拷贝,需要先解压)真实的例外情况。
    京ICP备2026067502号-1京公网安备11010502063487号