网络协议与延迟:TCP vs UDP 在低延迟场景下的选择
聊到低延迟消息传递,第一个绕不开的话题就是——选 TCP 还是 UDP?
这个问题,我几乎在每个低延迟项目里都会被问到。说实话,没有标准答案。但有一点我可以肯定:选错了协议,后面再怎么优化都是白搭。
TCP 的「原罪」:可靠性的代价
TCP 的设计初衷是什么?是可靠。它保证数据不丢、不乱、不重复。但这份「可靠」是有代价的。
- 三次握手:每次建立连接都要来回折腾,延迟至少增加一个 RTT
- 拥塞控制:网络一丢包,TCP 就自动降速,哪怕你的业务根本不需要
- 头部开销:TCP 头部 20 字节,加上 IP 头部 20 字节,小消息的传输效率极低
- 粘包问题:TCP 是流式协议,你得自己处理消息边界
我在一个高频交易项目中遇到过这种情况:两台机器之间只有 1 微秒的物理距离,但 TCP 的握手和确认机制硬生生把延迟拉到了 10 微秒以上。你说气不气人?
UDP:轻量但「不靠谱」
UDP 就简单多了。无连接、无确认、无重传。发出去就完事,收不收得到看天意。
但别急着下结论说 UDP 不好。我见过很多团队把 UDP 用得很漂亮——他们在应用层自己实现了轻量级的可靠传输,只针对关键消息做确认,非关键消息直接丢。这样既保留了 UDP 的低延迟特性,又解决了可靠性问题。
| 特性 | TCP | UDP |
|---|---|---|
| 连接建立 | 三次握手(1.5 RTT) | 无连接(0 RTT) |
| 可靠性 | 自动保证 | 不保证,需应用层处理 |
| 头部开销 | 20 字节 | 8 字节 |
| 适用场景 | 文件传输、Web 请求 | 实时音视频、游戏、高频交易 |
TCP_NODELAY 与 Nagle 算法
说到 TCP 的延迟,就不得不提 Nagle 算法。这个算法是干嘛的?说白了就是「攒够了再发」。
Nagle 算法的逻辑是这样的:如果上一个数据包还没收到 ACK,新来的小数据包就先攒着,等攒到足够大或者收到 ACK 了再一起发。这个设计在早期网络带宽稀缺的年代很有用,但在低延迟场景下就是灾难。
我记得有一次排查一个交易系统的延迟抖动,发现每次 Nagle 算法触发时,延迟会从 50 微秒直接跳到 500 微秒。原因就是小消息被「卡」住了,等 ACK 回来才释放。
解决方案很简单:设置 TCP_NODELAY 选项,禁用 Nagle 算法。
// C 语言示例
int flag = 1;
setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (char *)&flag, sizeof(flag));
// Java 示例
socket.setTcpNoDelay(true);
// Python 示例
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
RDMA 与内核旁路技术
好了,前面说的都是「软件层面」的优化。但如果你真的追求极致延迟,就得动硬件了。
RDMA(Remote Direct Memory Access)是什么?简单说就是:数据从一台机器的内存直接搬到另一台机器的内存,中间不经过 CPU,不经过内核。
传统网络通信的路径是这样的:
- 应用层数据 → 内核缓冲区(系统调用)
- 内核缓冲区 → 网卡驱动
- 网卡驱动 → 物理网卡
- 物理网卡 → 网络
- 接收端反向走一遍
每一步都有延迟。系统调用、上下文切换、数据拷贝……加起来就是几十微秒甚至上百微秒。
RDMA 的路径就简单多了:
- 应用层数据 → 网卡(直接内存访问,零拷贝)
- 网卡 → 网络
- 接收端网卡 → 应用层内存(零拷贝)
没有系统调用,没有内核参与,没有数据拷贝。延迟可以降到 1-3 微秒。
我曾经在一个金融客户那里部署 RDMA 方案。硬件到位后,我们花了整整两周调优——从网卡固件版本到 PCIe 通道配置,再到应用层的内存注册策略。最终延迟从 15 微秒降到了 2.8 微秒。客户很满意,但过程确实折腾。
内核旁路技术
如果 RDMA 太贵或者不适用,还有另一种选择:内核旁路。
典型的技术包括 DPDK(Data Plane Development Kit)和 Solarflare 的 OpenOnload。它们的思路是:让应用层直接操作网卡,绕过内核协议栈。
DPDK 的做法是:
- 在用户态实现网卡驱动
- 使用轮询模式代替中断模式
- 大页内存减少 TLB miss
- CPU 亲和性绑定,避免缓存抖动
用 DPDK 处理网络包,延迟可以降到 5-10 微秒,比传统 TCP 快一个数量级。
// DPDK 示例:初始化网卡
struct rte_mempool *mbuf_pool;
mbuf_pool = rte_pktmbuf_pool_create("MBUF_POOL", NUM_MBUFS,
MBUF_CACHE_SIZE, 0, RTE_MBUF_DEFAULT_BUF_SIZE, rte_socket_id());
// 配置网卡端口
rte_eth_dev_configure(port_id, 1, 1, &port_conf);
rte_eth_rx_queue_setup(port_id, 0, 1024, rte_eth_dev_socket_id(port_id), NULL, mbuf_pool);
如何选择?一张图说清楚
下面这张图是我自己整理的决策流程,帮你快速判断该用哪种方案:
嗯,这张图基本覆盖了大部分场景。但我要提醒你:实际项目中往往需要混合使用。比如控制通道用 TCP 保证可靠性,数据通道用 UDP 追求低延迟。我在一个量化交易系统里就是这么干的——订单确认走 TCP,行情数据走 UDP。
最后说一句:没有银弹。低延迟通信的本质是「用可控的代价换取速度」。你愿意牺牲多少可靠性、多少开发成本、多少运维复杂度,决定了你能达到多低的延迟。