3. 网络延迟优化(下):协议层——UDP vs TCP、内核旁路(Kernel Bypass)、Solarflare网卡与OpenOnload

上一节我们聊了网卡和驱动层的优化,说白了那是「硬件加速」。但真正让量化交易员头疼的,往往是协议栈这一层。你想想看,数据从网卡到了内核,要经过TCP/IP协议栈的层层处理,这中间有多少次内存拷贝、多少次上下文切换?我见过太多团队,网卡换成了最好的,光纤也拉了直连,但延迟就是降不下来。问题出在哪?就出在协议栈上。

UDP vs TCP:交易场景下的生死抉择

很多刚入行的朋友问我:「老师,为什么交易所都用UDP,不用TCP?TCP不是更可靠吗?」

嗯,这个问题问得好。我们先看看TCP干了什么——三次握手、拥塞控制、丢包重传、滑动窗口……这些机制在文件传输、网页浏览场景下是福音,但在交易场景下,它们就是灾难。

我举个例子。你在做高频套利,行情数据每秒过来几万笔。TCP如果丢了一个包,它会停下来等重传。这一等,可能就是几毫秒。几毫秒在交易世界里是什么概念?足够价格跳好几个tick了。你的策略可能就因为这一个丢包,错过了最佳入场点。

UDP就不一样了。它只管发,不管收没收到。丢了?丢了就丢了。反正下一笔行情马上就来。你想想看,在行情数据这种「连续流」场景下,丢一个包根本无所谓,因为下一秒的数据会覆盖它。但如果你用TCP,为了等那个丢掉的包,后面的所有数据都得排队等着——这才是真正的灾难。

核心结论: 交易场景下,UDP的「尽力而为」比TCP的「绝对可靠」更可靠。因为TCP的可靠性是以延迟为代价换来的,而这个代价在交易中不可接受。

我个人习惯是:行情数据全部走UDP,订单指令走TCP。为什么?因为订单不能丢。你下了一笔买入指令,如果丢了,可能就买不到了。但行情数据丢了,下一笔马上就来。这个原则我用了很多年,没出过问题。

内核旁路(Kernel Bypass):绕过那个「堵车」的内核

好,现在我们决定用UDP了。但问题还没完全解决。即使你用UDP,数据到了内核还是要经过协议栈处理。这个处理过程有多慢?我给你们算一笔账:

操作 耗时(微秒)
网卡收到数据 0.1
DMA到内核缓冲区 0.3
内核协议栈处理 1-5
上下文切换(内核→用户态) 1-3
数据拷贝到用户空间 0.5-2
总计 3-10+

看到没有?光是内核协议栈处理和上下文切换,就占了大部分时间。而且这还是理想情况。如果系统负载高,这个数字还会翻倍。

内核旁路(Kernel Bypass)的思路很简单:不让内核碰数据。数据从网卡进来,直接通过DMA放到用户空间的内存里,应用程序直接读。内核?让它一边凉快去。

具体怎么实现?主要有两种方式:

  • DPDK(Data Plane Development Kit):Intel主导的开源方案。它把网卡的控制权从内核手里抢过来,交给用户态的驱动。应用程序通过轮询(Polling)的方式从网卡收数据,而不是靠中断通知。轮询听起来很傻对吧?但在高吞吐场景下,轮询比中断快得多。因为中断有开销,每次中断都要保存上下文、恢复上下文。而轮询就是死循环检查,CPU占用率高,但延迟极低。
  • OpenOnload:Solarflare网卡的专属方案。它更聪明——不是完全绕过内核,而是把TCP/UDP协议栈搬到用户态。应用程序调用标准的socket API,但底层走的不是内核协议栈,而是用户态的协议栈。这样既保留了socket编程的便利性,又获得了接近DPDK的性能。
我的经验: 如果你团队有底层开发能力,DPDK是首选。它灵活、可控,而且免费。但如果你需要快速上线,或者团队对socket编程更熟悉,OpenOnload是更好的选择。我曾在某家做市商团队,就是用OpenOnload,两周就把延迟从15微秒降到了3微秒。

Solarflare网卡与OpenOnload:软硬一体的极致方案

说到Solarflare,我得好好聊聊。这家公司做网卡做了十几年,专门针对金融交易场景优化。他们的网卡有几个特点:

  • 硬件时间戳:网卡自己带高精度时钟,能在数据包进出网卡的瞬间打上时间戳。精度能达到纳秒级。这对做延迟分析、性能调优太重要了。我以前用普通网卡,想测延迟只能靠软件打时间戳,结果测出来的数据本身就包含了软件开销,根本不准。
  • TCP卸载引擎(TOE):把TCP协议栈的处理放到网卡硬件上。CPU不用管TCP的拆包、重组、校验这些脏活累活。这对订单指令场景特别有用——既保留了TCP的可靠性,又避免了CPU的负担。
  • OpenOnload:这是Solarflare的杀手锏。它本质上是一个用户态的协议栈,但和DPDK那种「完全绕过内核」不同,OpenOnload走的是「加速内核」的路线。应用程序调用socket API,OpenOnload拦截这些调用,在用户态完成协议处理,然后直接通过网卡硬件收发数据。

我给你们画个图,看看传统方式和OpenOnload的区别:

传统方式 应用程序 内核协议栈 网卡驱动 网卡硬件 OpenOnload方式 应用程序 OpenOnload (用户态协议栈) Solarflare驱动 Solarflare网卡 延迟对比 传统方式: 5-15微秒 OpenOnload: 1-3微秒

看到区别了吗?传统方式要经过四层,每层都有开销。OpenOnload把内核协议栈替换成了用户态的,省掉了内核态和用户态之间的切换开销。而且OpenOnload还用了「零拷贝」技术——数据从网卡直接到用户空间,不需要在内核缓冲区里倒一手。

注意: OpenOnload不是万能的。它只支持Solarflare自家的网卡。而且它的许可证是收费的。如果你用的是Mellanox或者Intel的网卡,那只能用DPDK或者RDMA。我曾经有个客户,买了Solarflare网卡但没买OpenOnload许可证,结果性能还不如普通网卡。所以,预算要算清楚。

实战:如何配置OpenOnload

好,理论说完了,我们看看实际怎么用。假设你已经装好了Solarflare网卡和OpenOnload驱动。配置其实很简单:

# 1. 检查网卡状态
onload --status

# 2. 启动应用程序时,用onload命令包裹
onload ./my_trading_app

# 3. 如果需要指定网卡和CPU亲和性
onload --net=sf0 --cpu=0,1 ./my_trading_app

# 4. 查看OpenOnload的统计信息
onload --profile=latency ./my_trading_app

就这么简单。你的应用程序不需要改一行代码。OpenOnload会自动拦截socket调用,走用户态协议栈。

但要注意几个坑:

  • CPU亲和性一定要绑。OpenOnload的轮询线程和你的业务线程最好绑在同一个CPU核心上,避免跨核通信的开销。
  • 中断合并要关掉。Solarflare网卡默认会合并中断,这对吞吐有好处,但对延迟是灾难。用sfcaffinity工具把中断绑到特定核心,或者干脆用轮询模式。
  • 内存要锁住。用mlockall()防止内存被换出。页面错误(Page Fault)在交易系统中是不可接受的。
避坑指南: 我曾经在一个项目中,OpenOnload配置好了,但延迟就是降不下来。查了两天,最后发现是BIOS里把CPU的C-State打开了。CPU在空闲时会进入省电模式,醒来需要几十微秒。交易系统里,CPU必须一直跑在最高频率,C-State、Turbo Boost这些统统关掉。

总结

网络延迟优化到了协议层,其实就是在和内核「抢时间」。UDP比TCP快,但不可靠;内核旁路比传统协议栈快,但需要专门的硬件和软件配合。Solarflare + OpenOnload这套组合,是目前金融交易领域最成熟的方案之一。它把延迟从10微秒级别降到了1-3微秒,而且对应用程序几乎透明。

但记住,没有银弹。DPDK、RDMA、OpenOnload各有优劣。选择哪个,取决于你的场景、预算和团队能力。我个人的建议是:如果条件允许,先用OpenOnload快速上线,后续再根据性能瓶颈做针对性优化。毕竟,在交易世界里,时间就是金钱。


专注资料整理