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_full
和rcu_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 miss和tlb miss。
对于减少cache miss,一方面需要优化程序,minimize memory footprint,或者说减少一个操作访问cache line的个数,一个缓存友好例子是文章中的哈希表的实现方式。另一方面,可以通过分(lang)配(fei)硬件资源让关键线程占有更多的缓存,比如系统有两块cpu,每块8核,我们可以把第二块cpu的所有核都隔离掉,然后把关键线程绑定到其中的部分核上,可能系统只有一两个关键线程,但它们却能拥有整块cpu的l3 cache。
对于减少tlb miss,可以使用huge pages。
链接:https://pan.baidu.com/s/1v5gm7n0l7tgyejcmqrmh2g 提取码:x2p5
免费分享,但是x度限制严重,如若链接失效点击链接或搜索加群 群号744933466。