网络基础回顾:TCP与UDP在行情场景下的抉择

做低延迟行情系统,第一个绕不开的问题就是:用 TCP 还是 UDP?

我见过不少团队,一上来就拍脑袋选 UDP,觉得它快。但实际跑起来,丢包、乱序、重传逻辑写得比业务代码还复杂。也有死守 TCP 的,结果行情峰值一来,延迟直接飙到毫秒级——在金融交易里,这已经算“灾难”了。

说白了,这不是简单的“谁快选谁”的问题。你得理解它们各自的脾气,再结合你的场景做取舍。

TCP:可靠但“慢”在哪里?

TCP 的可靠性,靠的是确认重传机制。你发一个包,对方得回一个 ACK,你才能发下一个。这本身就有延迟。

更关键的是,TCP 为了保证“公平”,内置了拥塞控制。当网络出现抖动,它会主动降低发送速率。这在行情场景下是致命的——行情数据不会等你,错过了就是错过了。

我记得有一次排查一个行情网关的延迟抖动,最后发现是 TCP 的慢启动在作祟。行情数据量突然增大,TCP 还在慢慢“试探”带宽,结果延迟从 50 微秒跳到了 2 毫秒。嗯,2 毫秒在别的场景不算什么,但在高频交易里,足够让策略亏钱了。

核心结论: TCP 适合对数据完整性要求极高、但对延迟不敏感的场景。比如订单确认、成交回报。

UDP:快,但你要自己兜底

UDP 没有握手,没有确认,没有重传。发出去就完了。延迟确实低,但代价是:丢包、乱序、重复,你都得自己处理。

在行情场景下,UDP 是主流选择。因为行情数据本身就有“时效性”——如果你丢了一个 tick,下一个 tick 马上就到,你完全可以忽略前一个。没必要为了一个过时的数据包,去阻塞后续所有数据。

我做过一个项目,用 UDP 接收美股行情。刚开始没做乱序处理,结果偶尔出现价格“回跳”——因为后发的包先到了,先发的包反而后到。你想想看,一个价格从 100 跳到 101,又跳回 100,策略直接懵了。

我的建议: 用 UDP 做行情接收,但一定要实现一个轻量级的乱序重组和丢包检测。别想着完全可靠,够用就行。

一张图看懂 TCP vs UDP 在行情场景下的抉择

下面这张图,是我自己总结的决策逻辑。每次做新项目,我都会拿出来看一眼。

行情场景下 TCP vs UDP 抉择流程图 开始:选择传输协议 数据完整性 要求极高? 选择 TCP 延迟敏感? 选择 UDP 根据具体需求权衡

Nagle算法与延迟的关系

Nagle 算法,说白了就是 TCP 的一个“攒包”机制。它会把多个小包攒成一个大的再发出去,目的是减少网络中的小包数量,提高带宽利用率。

但问题是,在低延迟场景下,这个“攒”的过程就是延迟。你想想看,行情数据来了,你希望立刻发出去,结果 Nagle 说“等等,再攒几个”,这一等可能就是几十毫秒。

我遇到过最夸张的一次,一个行情网关因为默认开启了 Nagle 算法,延迟从 100 微秒直接飙到 30 毫秒。排查了三天,最后发现是 Nagle 在作怪。从那以后,我所有低延迟项目的 TCP 连接,第一件事就是关掉 Nagle。

避坑指南: 我曾经在一个项目中,只关了服务端的 Nagle,没关客户端的。结果服务端发数据很快,客户端却还在“攒包”,导致整体延迟还是很高。记住:两端都要关!

如何关闭 Nagle 算法?

在 Linux 下,通过 setsockopt 设置 TCP_NODELAY 选项即可:

int flag = 1;
setsockopt(sockfd, IPPROTO_TCP, TCP_NODELAY, &flag, sizeof(flag));

就这么一行代码,延迟可能从毫秒级降到微秒级。但要注意,关闭 Nagle 后,小包会变多,网络带宽利用率会下降。在行情内网这种高带宽、低延迟的环境下,这点带宽浪费完全可以接受。

Socket缓冲区调优

Socket 缓冲区,是内核里的一块内存区域,用来暂存待发送或待接收的数据。它的大小直接影响延迟和吞吐量。

缓冲区太小,数据频繁被阻塞,延迟增加。缓冲区太大,内存浪费,而且数据在缓冲区里“排队”的时间也会变长。说白了,这是个 trade-off。

发送缓冲区 vs 接收缓冲区

缓冲区类型 作用 调优方向
发送缓冲区 暂存应用层写入、但尚未发送的数据 行情场景下,建议调小,减少数据在缓冲区内的排队时间
接收缓冲区 暂存内核收到、但应用层尚未读取的数据 行情场景下,建议调大,防止突发流量导致丢包

我个人的习惯是:发送缓冲区设为 16KB,接收缓冲区设为 256KB。这个配置在大多数行情场景下表现都不错。当然,具体数值还得根据你的数据量和网络带宽来微调。

如何调整 Socket 缓冲区大小?

同样通过 setsockopt:

int sendbuf = 16384;   // 16KB
int recvbuf = 262144;  // 256KB
setsockopt(sockfd, SOL_SOCKET, SO_SNDBUF, &sendbuf, sizeof(sendbuf));
setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &recvbuf, sizeof(recvbuf));
一个小技巧: 调完缓冲区后,可以用 getsockopt 读取实际值,确认内核是否接受了你的设置。因为内核可能会对设置值做上下限限制。

缓冲区调优的避坑指南

  • 不要盲目调大接收缓冲区:我曾经把接收缓冲区调到 1MB,结果内存占用飙升,反而因为缓存行失效导致性能下降。
  • 注意内核参数限制:Linux 内核有 tcp_rmem 和 tcp_wmem 两个参数,分别控制 TCP 接收和发送缓冲区的最大值。你通过 setsockopt 设置的值不能超过这个上限。
  • 结合 NUMA 架构考虑:在多核服务器上,Socket 缓冲区的内存分配可能跨 NUMA 节点,导致访问延迟增加。我建议绑定网卡中断和应用程序到同一个 NUMA 节点。

嗯,关于 TCP 与 UDP 的抉择、Nagle 算法、Socket 缓冲区调优,今天就聊这么多。这些基础概念看似简单,但真正在行情场景下用好,需要大量的实践和踩坑。希望我的经验能帮你少走一些弯路。

专注资料整理