C1000k 新思路:用户态 TCP/IP 协议栈
如今的server支撑上百万个并发 TCP 连接已经不是新闻(余锋2010年的演讲,ideawu 的 iComet 开源项目,WhatsApp 做到了 2.5M)。实现 C1000k 的常规做法是调整内核參数,提高文件数,降低每一个连接的内存消耗(參考 ideawu 的博客)。
在今年的 BSDCan2014 会议上, Patrick Kelsey 介绍了把 FreeBSD 9.x 的 TCP/IP 协议栈移植到了用户态(slides, github.com/pkelsey/libuinet),并用于 WANProxy 项目。在用户态执行 TCP/IP 协议栈意味着并发 TCP 连接不再占用系统文件数,仅仅占内存,攻克了 C1000k 的一大瓶颈,内核仅仅要提供一个收发网络 packet 的接口即可(比如netmap)。
内核的网络协议栈强调通用性,主要是为吞吐量优化(性能指标一般是 MB/s 或 packets per second),顺带兼顾大量并发连接。为了支持 C1000k,要调整内核參数让每一个连接少占资源,这与内核代码的设计初衷是违背的。
用户态协议栈捅破了这层窗户纸,能够依据应用的特点来剪裁协议栈功能。优化也更直接,不再是调黑盒參数组合,而是直接上 profiling,依据结果修改应用程序和协议栈的代码。
用户态协议栈的吞吐量比不上内核,只是对 C1000k 的应用场合(比如 comet)应该不成问题。
muduo 的 C1000k 实验
我用 muduo 做了一次 C1000k 的实验,用的是传统方案,没实用 libuinet。在一台 16GB 内存的 Dell WS490 旧工作站上创建了 50万个 TCP 连接,提供 echo 服务。系统可用内存降低了 5286MiB,即每一个连接 10.8KiB(当中服务进程占用了 1421MiB 内存,即每一个连接 2.9KiB,其余 8KiB 左右是内核协议栈的开销)。client是一台 8GB 内存的 i5-2500,内存消耗也是 5GB 多,因此这次实验仅仅试到了 C500k。客户机绑定了 10 个 IP,每一个 IP 上发出 5 万 TCP 连接,执行 pingpong 协议,每一个连接轮流收发 64 字节的消息。測得 QPS 大约是 11k,server的 CPU 占用率约为 60%(单线程)。profile 显示 CPU 的主要开销在内核中,我对这个结果基本惬意。
复活 4.4BSD-Lite2 的网络协议栈
受 libuinet 启示,我把 4.4BSD-Lite2 的网络协议栈也移植到了 Linux 用户态(github.com/chenshuo/4.4BSD-Lite2),方便《TCP/IP 具体解释 第2卷》的读者跟踪调试其代码。下面是 Eclipse CDT 单步跟踪的截图。
也能够用各种现成的工具来分析函数的调用关系:
我在《谈一谈网络编程学习经验》中说这本书的“代码仅仅能看,不能上机执行,也不能修改试验”如今不再成立了。
我在《关于 TCP 并发连接的几个思考题与试验》中用 TAP/TUN 作为自己写的协议栈的对外接口,对 4.4BSD-Lite2 也可如法炮制,让 20 年前的 TCP/IP 协议栈与如今的机器通信。除了与本机通信,还能够通过 NAT 转发,让 4.4BSD-Lite2 连上如今的 Internet。(sudo iptables -t nat -A PREROUTING -p tcp --dport 2009 -i eth0 -j DNAT --to192.168.0.2:2009)