低延迟文章引用

Linux获取纳秒时间戳的正确方式 - 知乎

Linux低延迟服务器系统调优 - 知乎

  最近做了一些系统和网络调优相关的测试,达到了期望的效果,有些感悟。同时,我也发现知乎上对Linux服务器低延迟技术的讨论比较欠缺(满嘴高并发现象);或者对现今cpu + 网卡的低延迟潜力认识不足(动辄FPGA现象),比如一篇知乎高赞的介绍FPGA的文章写到“从延迟上讲,网卡把数据包收到 CPU,CPU 再发给网卡,即使使用 DPDK 这样高性能的数据包处理框架,延迟也有 4~5 微秒。更严重的问题是,通用 CPU 的延迟不够稳定。例如当负载较高时,转发延迟可能升到几十微秒甚至更高”,刚好我前几天做过类似的性能测试,发现一个tcp或udp的echo server可以把网卡到网卡的延迟稳定在1微秒以内,不会比FPGA方案慢很多吧?

因此,我觉得有必要分享下自己的见解。总的来说,我打算分两篇文章讨论相关低延迟技术:

1)系统调优(本文):一些低延迟相关的Linux系统设置,和一些原则。

2)网络调优:使用solarflare网卡降低网络IO延迟

这里不打算介绍用户空间的延迟优化,因为太广泛了,另外我之前的文章也分享一些解决某类问题的低延迟类库。

说到低延迟,关键点不在低,而在稳定,稳定即可预期,可掌控,其对于诸如高频交易领域来说尤为重要。 而说到Linux的低延迟技术,一个不能不提的词是"kernel bypass",也就是绕过内核,为什么呢?因为内核处理不仅慢而且延迟不稳定。可以把操作系统想象成一个庞大的框架,它和其他软件框架并没有什么本质的不同,只不过更加底层更加复杂而已。既然是框架,就要考虑到通用性,需要满足各种对类型用户的需求,有时你只需要20%的功能,却只能take all。

因此我认为一个延迟要求很高(比如个位数微秒级延迟)的实时任务是不能触碰内核的,(当然在程序的启动初始化和停止阶段没有个要求,That's how linux works)。 这里的避免触碰是一个比bypass更高的要求:不能以任何方式进入内核,不能直接或间接的执行系统调用(trap),不能出现page fault(exception),不能被中断(interrupt)。trap和exception是主动进入内核的方式,可以在用户程序中避免,这里不深入讨论(比如在程序初始化阶段分配好所有需要的内存并keep的物理内存中;让其他非实时线程写日志文件等)。本文的关键点在于避免关键线程被中断,这是个比较难达到的要求,但是gain却不小,因为它是延迟稳定的关键点。即使中断发生时线程是空闲的,但重新回到用户态后cpu缓存被污染了,下一次处理请求的延迟也会变得不稳定。

不幸的是Linux并没有提供一个简单的选项让用户完全关闭中断,也不可能这么做(That's how linux works),我们只能想法设法避免让关键任务收到中断。我们知道,中断是cpu core收到的,我们可以让关键线程绑定在某个core上,然后避免各种中断源(IRQ)向这个core发送中断。绑定可以通过taskset或 sched_setaffinity实现,这里不赘述。 避免IRQ向某个core发中断可以通过改写/proc/irq/*/smp_affinity来实现。例如整个系统有一块cpu共8个核,我们想对core 4~7屏蔽中断,只需把屏蔽中断的core(0 ~ 3)的mask "f"写入smp_affinity文件。这个操作对硬件中断(比如硬盘和网卡)都是有效的,但对软中断无效(比如local timer interrupt和work queue),对于work queue的屏蔽可以通过改写/sys/devices/virtual/workqueue/*/cpumask来实现,本例中还是写入"f"。

那么剩下的主要就是local timer interrupt(LOC in /proc/interrupts)了。Linux的scheduler time slice是通过LOC实现的,如果我们让线程独占一个core,就不需要scheduler在这个core上切换线程了,这是可以做到的:通过isolcpus系统启动选项隔离一些核,让它们只能被绑定的线程使用,同时,为了减少独占线程收到的LOC频率,我们还需要使用"adaptive-ticks"模式,这可以通过nohz_fullrcu_nocbs启动选项实现。本例中需要在系统启动选项加入isolcpus=4,5,6,7 nohz_full=4,5,6,7 rcu_nocbs=4,5,6,7 来使得4~7核变成adaptive-ticks。adaptive-ticks的效果是:如果core上的running task只有一个时,系统向其发送LOC的频率会降低成每秒一次,内核文档解释了不能完全屏蔽LOC的原因:"Some process-handling operations still require the occasional scheduling-clock tick. These operations include calculating CPU load, maintaining sched average, computing CFS entity vruntime, computing avenrun, and carrying out load balancing. They are currently accommodated by scheduling-clock tick every second or so. On-going work will eliminate the need even for these infrequent scheduling-clock ticks."。

至此,通过修改系统设置,我们能够把中断频率降低成每秒一次,这已经不错了。如果想做的更完美些,让关键线程长时间(比如几个小时)不收到任何中断,只能修改内核延长中断的发送周期。不同kernel版本相关代码有所差异,这里就不深入讨论。不过大家可能会顾虑:这样改变系统运行方式会不会导致什么问题呢?我的经验是,这有可能会影响某些功能的正常运转(如内核文档提到的那些),但我尚未发现程序和系统发生任何异常,说明这项内核修改至少不会影响我需要的功能,我会继续使用。

两个原则:

1)如果一件事情可以被delay一段时间,那它往往能够被delay的更久,因为它没那么重要。

2)不要为不使用的东西付费,对于性能优化来说尤为如此。

如何检测中断屏蔽的效果呢?可以watch/proc/interrupts文件的变化 。更好的方法是用简单的测试程序来验证延迟的稳定性:

#include <iostream>

uint64_t now() {
   return __builtin_ia32_rdtsc();
}

int main() {
  uint64_t last = now();
  while (true) {
    uint64_t cur = now();
    uint64_t diff = cur - last;
    if (diff > 300) {
      std::cout << "latency: " << diff << " cycles" << std::endl;
      cur = now();
    }
    last = cur;
  }
  return 0;
}

通过taskset绑定一个核运行程序,每进入一次内核会打印一条信息。

最后,除了进入内核以外,影响延迟稳定性的因素还有cache misstlb miss

对于减少cache miss,一方面需要优化程序,minimize memory footprint,或者说减少一个操作访问cache line的个数,一个缓存友好例子是一种能高速查找的自适应哈希表文章中的哈希表的实现方式。另一方面,可以通过分(lang)配(fei)硬件资源让关键线程占有更多的缓存,比如系统有两块CPU,每块8核,我们可以把第二块CPU的所有核都隔离掉,然后把关键线程绑定到其中的部分核上,可能系统只有一两个关键线程,但它们却能拥有整块CPU的L3 cache。

对于减少tlb miss,可以使用huge pages。

使用solarflare网卡降低网络IO延迟 - 知乎

这篇文章承接上篇Linux低延迟服务器系统调优,主要谈谈Linux网络IO的低延迟方案。由于本人经验所限,只使用过solarflare的软硬件方案,没用过其他的kernel bypass框架(如DPDK)或网卡,所以本文只局限于solarflare相关的使用经验。

首先声明下,本人和solarflare公司没有利益关系,本文尽可能客观的分享个人的使用感受。另外我也不是solarflare产品的专家,如有不准确之处还望指正。

solarflare的拳头产品是它的高性能网卡。我对solareflare网卡的第一感觉是“贵”,和一块高端服务器CPU的价格差不多了。因此如果只把它当成一块支持10G/25G网络的普通网卡来用是暴殄天物了,这里说的当成普通网卡指的是不使用它的软件方案,而是使用Linux内核实现网络编程,这样的话它并不会比普通网卡快多少。如上篇文章所言“一个对延迟要求很高(比如个位数微秒级延迟)的实时任务是不能触碰内核的”,solarflare网卡提供了配套的kernel bypass软件解决方案,还不止一个:Onload, ef_vi和Tcpdirect(关于这三个stack的用法这里就不细说了,官方文档说的很详细)。首先我们关心的是它们的性能怎么样?下图是官方文档中的一个测试结果:

Latency test results

测试使用了两台直接连接的软硬件配置相同的服务器进行pingpong测试,这样RTT的一半可以认为是一台机器从收到发的网卡到网卡的延迟。

从测试结果看来这个数字还挺吸引人的:10G网络可以低至800多ns。不过我认为这个测试方法有些脱离实际应用:首先测试中每次发送的数据都是完全一样的(payload全0),在ef_vi udp的测试代码中甚至出现了这种优化:初始化阶段就准备好了要发送的frame数据(包括ethernet头,ip头,udp头和payload),只要程序一收到数据就发送这个相同的frame出去,有点作弊的成分。另外,pingpong测试是不间断的收发,一秒钟会处理几十万个包,实际场景一般不会出现这么高频的情况。

因此,我决定实现一个更贴合实际的测试:echo测试,echo client和echo server程序分别在两台服务器上,echo client每次发送不同的数据,echo server收到后原样转发给client,这里echo server是under test的机器。echo server的收和发使用的是不同的协议:收udp multicast(模仿接收行情),发送tcp数据(模仿发送订单),因此echo server会先tcp连接到echo client。echo client控制发送频率:每秒1000个包,5秒结束。

通过测试不同的收发stack组合,以及各种配置选项,我所能达到的最佳延迟是960ns +/- 90ns(这里除去了第一个echo的延迟,因为这个简单的测试没做cache warmup,第一个延迟会高出5000ns左右)。最佳方案使用了最新的X2系列网卡(支持ctpio,比非ctpio快200ns),ef_vi接收udp,tcpdirect发送tcp,以及上篇文章提到的屏蔽中断的内核。这个测试主要使用16字节payload的小包进行测试,对于更长的包可以用1 byte = 1.5ns来近似(亲测)。下面简单谈谈我对3个stack的使用感受。

onload

这是solarflare最经典的kernel bypass stack,属于transparent kernel bypass,因为它提供了兼容socket网络编程的接口,用户不需要修改自己的代码,只需preload libonload.so即可使用(类似使用tcmalloc)。由于其特别的易用性,十分适合新人入坑(先要买块网卡),是运动手表界的iwatch。

关于性能,在我的测试中onload比使用kernel的传统方法快了6000多ns,比ef_vi/tcpdirect慢300多ns,算是不错了。

另外提一下onload的配置,onload提供了非常多的配置选项,我在测试中测试了大部分相关的配置,最后发现对绝大多数配置来说使用默认值就好了,例外是设置EF_TCP_FASTSTART_INIT=0 EF_TCP_FASTSTART_IDLE=0 会稍微降低些延迟,这也和推荐的latency profile一致。

ef_vi

ef_vi是一个level 2的底层API,可以收发原始的ethernet frame,但没有提供上层协议的支持(不过能支持基于IP,proto,port的接收端filter)。除此之外ef_vi最大的特点是zero-copy:它要求用户预先分配一些recv buffer提供给ef_vi使用,网卡收到包后会直接写入这些buffer,用户通过eventq poll接口获得已填充数据的buffer id即可开始处理接收数据,处理完后再把buffer交还ef_vi循环使用。相比而言,onload无法做到这样的zero-copy,因为socket API只在用户开始接收数据时才提供buffer。

ef_vi API使用起来比较复杂,且缺乏上层协议支持,但能提供最好的性能,是专业用户的选择。我建议只使用ef_vi做udp的接收端,因为经filter后用户对包头的解析工作不多,这还有一个好处是,可以在用户程序中抓包(比如用于记录行情,后面会提到通用的本地抓包方法并不合适):通过一个ring buffer和一个写pcap文件的非关键线程,可以做到处理网络数据和抓包同时进行,而且抓包对处理数据的关键线程几乎零影响,这充分利用了ef_vi底层和zero-copy的特性。

tcpdirect

tcpdirect是基于ef_vi并实现了上层协议的stack,提供了类似socket的API:zocket,能让用户读写tcp/udp的payload数据,同时也继承了ef_vi zero-copy的特性。另外,tcpdirct要求使用huge pages。

我的建议是,对于udp的发送端和整个tcp使用tcpdirect。

最后,谈一个比较重要的话题:如何测量网卡到网卡的延迟?在这个echo测试中,由于echo server和echo client是不对等的的测试者(从收发的协议上),而且由于条件所限,两台机器的硬件配置有一定差异,也没有通过网线直连(中间经过了交换机),所以光看RTT不可靠,只能使用通用的网络延迟测试方法:

1)让交换机把echo server的收发链路数据镜像到另一个抓包设备。这种方法比较依赖交换机性能,精确度不高,比如抓包结果会出现因果乱序的现象。

2)通过tap或分光设备把echo server的网口数据分发到另一个抓包设备。由于条件所限,暂时无法使用...

3)在echo server本地抓包。这个比较依赖抓包工具,我进行过尝试,最后发现目前已有的工具都有其局限性,后面会详细讨论。

4)在程序中使用onload/ef_vi/tcpdirect提供的API获得包收发的硬件时间戳(网卡时间戳),这是我主要使用的方法,同时也参考了从echo client获得的RTT,用于double check前者的结果,另外可以测量各种抓包工具或者获取硬件时间戳造成的额外延迟。

本人经验有限,如果有更好的方法欢迎在评论区探讨。最后谈谈我使用过的本地记录网络包时间戳的方法:

tcpdump

这是大家用过的工具,通过kernel抓包,但对kernel bypass的方案无效,而且记录的是软件时间(系统时间戳),不等同网卡时间。tcpdump带来的额外延迟约为2000ns。

onload_tcpdump

顾名思义,可以对使用onload方案的网络数据进行抓包,但对其他无效,也无法获得硬件时间戳。onload_tcpdump带来的额外延迟约为1500ns。

solar_capture

solarflare自己软件抓包工具,需要额外购买license,不便宜,和网卡本身的价格差不多了,而且license绑定网卡。官方宣传solar_capture性能很高,可以记录收发包的网卡时间戳。我们曾经寄予厚望买了一个license试用,但发现局限性很大,基本个玩具。

首先,solar_capture不支持solarflare自己的最先进的X2系列网卡,只支持较老的7000/8000系列。关于这个问题我前几天有机会问了solarflare的一个system architect,他说更新solar_capture是一个较低优先级的事情...看来这工具本身在公司内部就是个边缘产品(官网上找solarCapture看到的都是硬件设备,这个软件工具的信息很难找到)。

另外,solar_capture不支持"ultra-low-latency"的网卡模式。这又是个硬伤,因为"ultra-low-latency"比其他模式快100ns左右(亲测),是我们默认会使用的模式。

最后,solar_capture对于一个网口的收和发的数据是通过两个任务来记录的,pcap文件中可能出现因果乱序现象(但时间戳是准确的),需要分析结果的用户自己处理。

solar_capture带来的额外延迟约为50ns。

通过ef_vi/tcpdirect API获取网卡时间戳

现在ef_vi和tcpdirect提供了获取接收和发送时间戳的API(获取发送时间戳是OpenOnload 201811新增加的功能),这样就可以在用户程序中直接测量网卡到网卡的延时了。获取两个硬件时间戳带来的额外延迟约为50ns,看上去和获取系统时间戳的开销差不多(相关文章:Linux获取纳秒时间戳的正确方式)。

sfptpd

这其实是solarflare提供的一个同步网卡时间源的工具,可以和网络上的其他设备同步,不过我主要用来和本地系统时间同步,这样可以测量网卡到用户程序,和用户程序到网卡的分段延迟。使用sfptpd的一个不方便的地方是这个服务必须一直运行才能保证同步的比较准确。

原则:所有的时间测量方式都会造成额外延迟,尽量避免在生产环境中过多使用。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值