网络延迟优化:从物理层到应用层
做市商系统里,网络延迟就是真金白银。我见过太多团队,算法写得再漂亮,结果被网络拖了后腿。今天咱们就聊聊,怎么把网络延迟从物理层一路压到应用层。
物理层:别让光纤成为瓶颈
很多人一上来就调代码,其实物理层才是基础。我记得有一次帮一家量化公司做优化,发现他们的服务器和交换机之间用了根劣质网线,丢包率高达0.5%。你想想看,0.5%的丢包在普通系统里可能无所谓,但在高频交易里,这直接意味着每天几十次交易机会的丢失。
物理层优化其实就几点:
- 光纤直连:能直连就别经过交换机,每多一跳就多几十微秒
- 网卡选型:Solarflare、Mellanox这些低延迟网卡是标配,别用消费级网卡
- PCIe通道:网卡要插在离CPU最近的PCIe插槽上,减少跨NUMA访问
Kernel Bypass:绕过操作系统
传统网络栈里,数据包要经过内核协议栈、socket缓冲区、上下文切换……这一套下来,几百微秒就没了。做市商系统等不起。
Kernel bypass技术说白了就是让应用程序直接操作网卡硬件,跳过内核。主流方案有两个:
DPDK(Data Plane Development Kit)
DPDK通过UIO(Userspace I/O)机制,把网卡寄存器映射到用户空间。数据包从网卡直接到用户态,零拷贝。
// DPDK初始化示例
int rte_eal_init(int argc, char **argv);
struct rte_mempool *mbuf_pool = rte_pktmbuf_pool_create("MBUF_POOL",
NUM_MBUFS, MBUF_CACHE_SIZE, 0, RTE_MBUF_DEFAULT_BUF_SIZE, rte_socket_id());
// 接收数据包
struct rte_mbuf *bufs[BURST_SIZE];
uint16_t nb_rx = rte_eth_rx_burst(port_id, queue_id, bufs, BURST_SIZE);
我在项目中实测过,DPDK能把网络延迟从几十微秒压到1-2微秒。代价是什么?你得自己处理协议栈,TCP/IP协议得重新实现。
RDMA(Remote Direct Memory Access)
RDMA更狠,它允许一台机器的应用程序直接读写另一台机器的内存,完全绕过CPU和内核。延迟能到亚微秒级别。
TCP优化:别让协议拖后腿
如果你们系统还在用TCP,那有几个参数必须调。我见过太多人直接用默认配置,结果延迟高得离谱。
Nagle算法:关掉它
Nagle算法会把小包攒成大包再发,目的是减少网络拥塞。但做市商系统里,每个订单都是独立的,攒包只会增加延迟。
// 关闭Nagle算法
int flag = 1;
setsockopt(sockfd, IPPROTO_TCP, TCP_NODELAY, (void *)&flag, sizeof(flag));
嗯,这里要注意:TCP_NODELAY必须和Nagle算法一起理解。Nagle算法是攒包,TCP_NODELAY是告诉内核「别攒了,有数据就发」。两者是配套的。
TCP快速打开(TFO)
传统TCP三次握手需要1.5个RTT才能开始传数据。TFO允许在SYN包中就携带数据,减少一次握手延迟。
// 服务端启用TFO
int qlen = 5; // 最大TFO请求队列长度
setsockopt(sockfd, IPPROTO_TCP, TCP_FASTOPEN, &qlen, sizeof(qlen));
// 客户端使用TFO
sendto(sockfd, data, len, MSG_FASTOPEN, (struct sockaddr *)&addr, sizeof(addr));
多路复用:从select到io_uring
做市商系统要同时处理大量连接,多路复用是必须的。我经历过从select到epoll再到io_uring的演进,每一步都是质的飞跃。
| 技术 | 时间复杂度 | 延迟 | 适用场景 |
|---|---|---|---|
| select | O(n) | 高 | 连接数少(<1024) |
| epoll | O(1) | 低 | 大量连接,事件驱动 |
| io_uring | O(1) | 极低 | 高IOPS,零拷贝 |
epoll:当前主流
epoll用红黑树管理文件描述符,用回调机制通知事件。相比select的轮询,效率高得多。
// epoll使用示例
int epfd = epoll_create1(0);
struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET; // 边缘触发
ev.data.fd = sockfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);
// 事件循环
struct epoll_event events[MAX_EVENTS];
while (1) {
int nfds = epoll_wait(epfd, events, MAX_EVENTS, -1);
for (int i = 0; i < nfds; i++) {
// 处理事件
}
}
我个人习惯用边缘触发(ET)模式,配合非阻塞IO。水平触发(LT)虽然简单,但容易重复触发,增加不必要的系统调用。
io_uring:新一代利器
io_uring是Linux 5.1引入的异步IO框架。它用共享环形缓冲区在用户态和内核态之间传递请求和完成事件,避免了系统调用开销。
// io_uring初始化
struct io_uring ring;
io_uring_queue_init(1024, &ring, 0);
// 提交读请求
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_read(sqe, fd, buf, len, offset);
io_uring_submit(&ring);
// 等待完成
struct io_uring_cqe *cqe;
io_uring_wait_cqe(&ring, &cqe);
// 处理数据
io_uring_cqe_seen(&ring, cqe);
io_uring最厉害的地方是支持缓冲区和文件注册,能实现真正的零拷贝。我在做行情网关时,用io_uring替代epoll后,单核吞吐量提升了3倍。
知识体系总览
下面这张图总结了网络延迟优化的核心路径:
从物理层到应用层,每一层都有优化空间。但要注意,优化不是孤立的——物理层决定了天花板,Kernel bypass决定了路径,TCP和多路复用决定了实际效果。我建议先从最明显的瓶颈入手,逐步深入。
公众号:蓝海资料掘金营,微信deep3321