最近作了一些系统和网络调优相关的测试,达到了指望的效果,有些感悟。同时,我也发现知乎上对Linux服务器低延迟技术的讨论比较欠缺(满嘴高并发现象);或者对现今cpu + 网卡的低延迟潜力认识不足(动辄FPGA现象),好比一篇知乎高赞的介绍FPGA的文章写到“从延迟上讲,网卡把数据包收到 CPU,CPU 再发给网卡,即便使用 DPDK 这样高性能的数据包处理框架,延迟也有 4~5 微秒。更严重的问题是,通用 CPU 的延迟不够稳定。例如当负载较高时,转发延迟可能升到几十微秒甚至更高”,恰好我前几天作过相似的性能测试,发现一个tcp或udp的echo server能够把网卡到网卡的延迟稳定在1微秒之内,不会比FPGA方案慢不少吧?linux
所以,我以为有必要分享下本身的看法。总的来讲,我打算分两篇文章讨论相关低延迟技术:ios
1)系统调优(本文):一些低延迟相关的Linux系统设置,和一些原则。缓存
这里不打算介绍用户空间的延迟优化,由于太普遍了,另外我以前的文章也分享一些解决某类问题的低延迟类库。服务器
说到低延迟,关键点不在低,而在稳定,稳定便可预期,可掌控,其对于诸如高频交易领域来讲尤其重要。 而说到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."。tcp
至此,经过修改系统设置,咱们可以把中断频率下降成每秒一次,这已经不错了。若是想作的更完美些,让关键线程长时间(好比几个小时)不收到任何中断,只能修改内核延长中断的发送周期。不一样kernel版本相关代码有所差别,这里就不深刻讨论。不过你们可能会顾虑:这样改变系统运行方式会不会致使什么问题呢?个人经验是,这有可能会影响某些功能的正常运转(如内核文档提到的那些),但我还没有发现程序和系统发生任何异常,说明这项内核修改至少不会影响我须要的功能,我会继续使用。高并发
两个原则:
1)若是一件事情能够被delay一段时间,那它每每可以被delay的更久,由于它没那么重要。
2)不要为不使用的东西付费,对于性能优化来讲尤其如此。
如何检测中断屏蔽的效果呢?能够watch/proc/interrupts文件的变化 。更好的方法是用简单的测试程序来验证延迟的稳定性:
#include 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。