网络协议栈深度优化:从TCP到RDMA的微秒级跨越
做港股交易系统,网络协议栈这块儿,我踩过的坑比代码行数还多。今天咱们聊聊TCP/IP协议栈的延迟瓶颈、UDP在交易中的应用,以及RDMA技术入门。说白了,就是怎么让数据在网络上跑得更快。
TCP/IP协议栈的延迟瓶颈
很多人觉得TCP很可靠,但做高频交易的人都知道——可靠是有代价的。我当年在优化一个港股行情系统时,发现TCP的延迟抖动能达到几百微秒。为什么会这样?
- 三次握手延迟:每次建立连接都要来回三次,这还没算上Nagle算法可能带来的额外等待
- 拥塞控制:TCP的慢启动、拥塞避免,说白了就是"先试探再加速",这在交易场景下就是灾难
- 头部开销:TCP头部20字节,IP头部20字节,加上各种选项,一个包可能40%都是协议头
- 内核态切换:每次收发数据都要从用户态切到内核态,这个上下文切换成本,嗯,你想想看
核心问题:TCP是为"可靠传输"设计的,不是为"低延迟"设计的。在交易系统中,我们往往需要的是"在可接受的可靠性下,追求极致的速度"。
我记得有一次,团队里有人提议用TCP做行情推送。我直接说:"你试试看,一个TCP重传就能让你丢掉几百笔交易机会。"后来我们改用UDP,延迟直接降了一个数量级。
UDP在交易中的应用
UDP无连接、无状态、无拥塞控制——这三个"无"恰恰是交易系统最需要的。说白了,UDP就是"发了就不管",但我们可以自己控制可靠性。
我个人习惯的做法是:
- 行情数据用UDP组播:港股行情数据量大,用UDP组播可以做到"一发多收",带宽利用率极高
- 订单数据用UDP单播+应用层ACK:自己实现一个轻量级的确认机制,比TCP的ACK快得多
- 心跳检测用UDP:几微秒就能完成一次心跳,TCP的保活机制太慢了
避坑指南:我曾经在UDP应用层ACK上犯过一个错误——把ACK和业务数据混在一起。结果行情高峰期,ACK被行情数据堵住了,导致订单确认延迟。后来我改成独立的ACK通道,问题就解决了。
这里给一个简单的UDP收发示例:
// UDP发送端 - 极简实现
int udp_send(const char* data, int len, const char* ip, int port) {
int sock = socket(AF_INET, SOCK_DGRAM, 0);
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
inet_pton(AF_INET, ip, &addr.sin_addr);
// 关键优化:设置发送缓冲区为0
int bufsize = 0;
setsockopt(sock, SOL_SOCKET, SO_SNDBUF, &bufsize, sizeof(bufsize));
int ret = sendto(sock, data, len, 0,
(struct sockaddr*)&addr, sizeof(addr));
close(sock);
return ret;
}
你可能会问:UDP丢包怎么办?嗯,在交易场景下,我们通常的做法是:
- 行情数据:允许少量丢包,用FEC(前向纠错)恢复
- 订单数据:应用层重传,超时时间设为100微秒级别
- 关键数据:双通道冗余发送
RDMA技术入门
如果说UDP是把延迟从毫秒级降到微秒级,那RDMA就是把微秒级降到纳秒级。RDMA(Remote Direct Memory Access)说白了就是"让网卡直接读写内存,绕过CPU"。
我刚开始接触RDMA时,觉得这东西太复杂了。但后来发现,其实核心概念就几个:
| 概念 | 说明 | 类比 |
|---|---|---|
| QP(队列对) | 发送和接收的通道 | 像TCP的socket |
| MR(内存注册) | 告诉网卡哪些内存可以访问 | 像DMA的缓冲区 |
| CQ(完成队列) | 操作完成的通知 | 像中断信号 |
| WR(工作请求) | 具体的读写操作 | 像网络包 |
RDMA有两种主流实现:InfiniBand和RoCE。我个人更倾向于RoCE,因为它可以跑在现有的以太网基础设施上。但要注意,RoCE对网络质量要求很高——我曾经在交换机上没开PFC(优先级流控),结果RDMA性能直接腰斩。
警告:RDMA不是银弹。如果你的系统延迟要求是100微秒级别,用UDP就够了。只有当你需要10微秒以下的确定性延迟时,才值得上RDMA。而且RDMA的调试成本很高,我见过不少团队花了三个月都没调通。
这里给一个RDMA的初始化代码片段,感受一下:
// RDMA初始化 - 创建QP
struct ibv_qp_init_attr qp_attr = {
.send_cq = cq,
.recv_cq = cq,
.cap = {
.max_send_wr = 128,
.max_recv_wr = 128,
.max_send_sge = 1,
.max_recv_sge = 1
},
.qp_type = IBV_QPT_RC // 可靠连接
};
struct ibv_qp *qp = ibv_create_qp(pd, &qp_attr);
if (!qp) {
// 嗯,这里我踩过坑 - 忘记先创建PD和CQ了
fprintf(stderr, "创建QP失败\n");
return -1;
}
RDMA的优化空间很大。我记得有一次,通过调整WR的批量提交数量,延迟从8微秒降到了3微秒。说白了,就是"一次提交多个操作,减少与网卡的交互次数"。
三种方案的对比
最后,我整理了一个对比表,方便你根据场景选择:
| 方案 | 典型延迟 | 抖动 | 复杂度 | 适用场景 |
|---|---|---|---|---|
| TCP | 50-200μs | 高 | 低 | 非实时数据、管理通道 |
| UDP | 5-20μs | 中 | 中 | 行情推送、订单通道 |
| RDMA | 1-5μs | 低 | 高 | 极速交易、内存同步 |
做港股交易系统,我的建议是:先用UDP把延迟降到可接受范围,如果还不够,再考虑RDMA。别一上来就上RDMA,那玩意儿调试起来,嗯,够你喝一壶的。
核心总结:网络协议栈优化,说白了就是在"可靠"和"快速"之间找平衡。TCP太慢,UDP不够可靠,RDMA太复杂——但每个方案都有它的用武之地。关键是要理解你的业务场景,然后选择最合适的方案。