网络协议与延迟:TCP vs UDP 在低延迟场景下的选择

聊到低延迟消息传递,第一个绕不开的话题就是——选 TCP 还是 UDP?

这个问题,我几乎在每个低延迟项目里都会被问到。说实话,没有标准答案。但有一点我可以肯定:选错了协议,后面再怎么优化都是白搭

TCP 的「原罪」:可靠性的代价

TCP 的设计初衷是什么?是可靠。它保证数据不丢、不乱、不重复。但这份「可靠」是有代价的。

  • 三次握手:每次建立连接都要来回折腾,延迟至少增加一个 RTT
  • 拥塞控制:网络一丢包,TCP 就自动降速,哪怕你的业务根本不需要
  • 头部开销:TCP 头部 20 字节,加上 IP 头部 20 字节,小消息的传输效率极低
  • 粘包问题:TCP 是流式协议,你得自己处理消息边界

我在一个高频交易项目中遇到过这种情况:两台机器之间只有 1 微秒的物理距离,但 TCP 的握手和确认机制硬生生把延迟拉到了 10 微秒以上。你说气不气人?

核心结论:如果你的场景能容忍偶尔丢包,或者你有自己的重传机制,UDP 往往是更好的选择。

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)
我的建议:在低延迟场景下,永远开启 TCP_NODELAY。但要注意,这可能会增加小数据包的数量,导致网络带宽利用率下降。你需要根据实际业务做权衡。

RDMA 与内核旁路技术

好了,前面说的都是「软件层面」的优化。但如果你真的追求极致延迟,就得动硬件了。

RDMA(Remote Direct Memory Access)是什么?简单说就是:数据从一台机器的内存直接搬到另一台机器的内存,中间不经过 CPU,不经过内核

传统网络通信的路径是这样的:

  1. 应用层数据 → 内核缓冲区(系统调用)
  2. 内核缓冲区 → 网卡驱动
  3. 网卡驱动 → 物理网卡
  4. 物理网卡 → 网络
  5. 接收端反向走一遍

每一步都有延迟。系统调用、上下文切换、数据拷贝……加起来就是几十微秒甚至上百微秒。

RDMA 的路径就简单多了:

  1. 应用层数据 → 网卡(直接内存访问,零拷贝)
  2. 网卡 → 网络
  3. 接收端网卡 → 应用层内存(零拷贝)

没有系统调用,没有内核参与,没有数据拷贝。延迟可以降到 1-3 微秒。

注意:RDMA 需要专门的网卡(如 Mellanox ConnectX 系列)和交换机支持。部署成本较高,而且对网络环境要求苛刻——丢包会导致性能急剧下降。

我曾经在一个金融客户那里部署 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);
避坑指南:我曾经在一个项目里直接用 DPDK 替换了原来的 TCP 通信,结果发现应用层代码需要大改——因为 DPDK 不提供 socket API,你得自己实现协议栈。后来我们改用 OpenOnload,它兼容 socket API,迁移成本低很多。

如何选择?一张图说清楚

下面这张图是我自己整理的决策流程,帮你快速判断该用哪种方案:

低延迟消息传递方案决策树 延迟要求 < 10μs? 预算充足? RDMA DPDK / OpenOnload 消息大小? 小消息 大消息 UDP + 应用层可靠 TCP + NODELAY 注:小消息指 < 1KB,大消息指 ≥ 1KB ⚠️ 重要提醒 RDMA 和 DPDK 需要专门的硬件和驱动支持 UDP 方案需要自行处理丢包、乱序、重复等问题 TCP 方案虽然延迟较高,但开发成本最低

嗯,这张图基本覆盖了大部分场景。但我要提醒你:实际项目中往往需要混合使用。比如控制通道用 TCP 保证可靠性,数据通道用 UDP 追求低延迟。我在一个量化交易系统里就是这么干的——订单确认走 TCP,行情数据走 UDP。

最后说一句:没有银弹。低延迟通信的本质是「用可控的代价换取速度」。你愿意牺牲多少可靠性、多少开发成本、多少运维复杂度,决定了你能达到多低的延迟。


专注资料整理