本文最初发布于 Dropbox 官方博客,原作者 Alexey Ivanov,经授权由 InfoQ 中文站翻译并分享。
本文是基于我在 2017 年 9 月 6 日举办的 NginxConf 2017 大会上所做演讲的延展。作为 Dropbox Traffic 团队的 SRE,我主要负责我们 Edge 网络的可靠性、性能和效率。 Dropbox Edge 网络是一种基于 nginx 的代理层,主要用于处理延迟敏感型元数据事务和高吞吐率的数据传输。对于这样一个每秒需要处理数十 GB 数据,并需要同时处理数万笔延迟敏感型事务的系统,需要对整个代理栈的效率和性能进行调优,从驱动到中断,从 TCP/IP 和内核到库,再到应用程序层,无不需要进行优化。
声明
本文将介绍大量用于对 Web 服务器和代理进行性能调优的方法。但请不要在没有深入了解背后动机的情况下盲目模仿。为了尽可能严谨地借鉴,可以逐一尝试并衡量效果,进而确定每种方法在你的环境中是否有效。
本文并不准备深入探讨 Linux 性能调优,不过相关方法会大量使用 Bcc 工具、 eBPF 以及perf
,但这并不意味着本文是为了告诉你如何使用各类性能分析工具。如果希望进一步了解这些工具,那么推荐阅读 Brendan Gregg 的博客文章。
本文也不准备涉及浏览器性能。有关性能延迟的优化可能会涉及客户端性能相关的问题,但只是简要介绍不会过于深入。如果想要进一步了解这方面的知识,建议阅读 Ilya Grigorik 撰写的 High Performance Browser Networking 一文。
同时本文也不打算详细探讨有关 TLS 的最佳实践。虽然下文将多次提及 TLS 库及其设置,但你和你的安全团队应当评估每种选项对性能和安全性的影响。此时可以使用 Qualys SSL Test 来确认端点是否与当前的最佳实践要求匹配。如果想进一步了解 TLS 的常规信息,可考虑订阅 Feisty Duck Bulletproof TLS 新闻邮件。
本文内容安排
本文将探讨整个系统不同层面的效率 / 性能优化问题。首先从硬件和驱动等最底层的内容着手,这些调优措施几乎可以适用于任何高负载服务器。随后将转向 Linux 内核及其 TCP/IP 栈,任何以 TCP 事务为主的系统均可使用这些措施。最后我们将介绍库以及应用程序层面的调优,这些措施主要适用于常规的 Web 服务器,尤其是 nginx。
对于每个可优化的领域,我会尽可能提供一些有关延迟 / 吞吐率等方面如何进行取舍、监视指南等内容的背景信息,同时也会针对不同工作负载提供适合的建议。
硬件
CPU
为了获得优异的非对称 RSA/EC 性能,建议至少采用能够支持 AVX2(/proc/cpuinfo 中显示可支持 avx2)的处理器,同时最好能选择支持大整数运算(Bmi 和Adx)的硬件。对于对称式架构,则应选择为AES 密码运算使用AES-NI,为ChaCha+Poly 使用AVX512。Intel 针对自家不同世代硬件的OpenSSL 1.0.2 性能提供了性能对比,从中可以了解到通过不同硬件进行卸载(Offload)所能获得的效果。
对于路由等延迟敏感的用例,禁用超线程(HT)的情况下NUMA 节点数越少越好。对于高吞吐率任务,内核数量越多越好,同时这类任务也能从超线程中获益(除非受制于缓存),此类任务通常并不会受到NUMA 节点数量影响。
特别需要指出一点,如果选择Intel 的处理器,至少要使用Haswell/Broadwell 家族产品,如果能使用Skylake CPU 就更好了。如果选择AMD 处理器,EPYC 的性能就很棒。
网卡
至少需要选择10G 产品,25G 的产品就更好了。如果需要由一台服务器通过TLS 处理更高吞吐率的工作负载,单凭本文介绍的调优方法是不够的,此时可能需要将TLS 帧下推至内核层面(例如 FreeBSD 、 Linux )。
软件方面,建议选择邮件列表和用户社区活动都比较活跃的开源驱动。如果要对与驱动有关的问题进行调试(很可能需要这样做),这一点往往非常重要。
内存
根据经验来说,对延迟敏感的任务往往需要速度更快的内存,而对吞吐率敏感的任务往往需要容量更大的内存。
硬盘
主要取决于缓冲 / 缓存需求,但如果要对更多内容创建缓冲或缓存,则应选择基于闪存的存储设备。有些人甚至会使用针对闪存进行优化的特殊文件系统(通常是以日志为结构的),但这些文件系统并非总能胜过普通的 ext4/xfs。
此外任何情况下都要注意避免因为忘了启用 TRIM 或更新固件而导致闪存被烧穿(Burn through)。
操作系统:底层
固件
为避免痛苦并且冗长的排错过程,应尽可能确保固件保持最新版本。请尽量确保 CPU 微码,以及主板、网卡、SSD 的固件始终保持最新状态。但这并不是说上述内容有了新版本就要第一时间更新,一般来说,建议始终保持使用最新版固件的前一个版本,除非最新版修复了某些非常重要的 Bug,并且版本也不要落后太多。
驱动
驱动的更新规则与固件的更新差不多:尽可能接近最新版就行了。不过此处需要注意一个问题:可能的情况下,尽量将内核升级和驱动更新分开进行。例如可以使用 DKMS 将驱动打包,或针对自己使用的所有内核版本进行预编译驱动。这样当更新内核后如果有什么东西无法按照预期正常运转,那么排错过程中也可以减少一个怀疑的目标。
CPU
这方面要善用内核代码库和自带的各种工具。你可以在 Ubuntu/Debian 中安装linux-tools
包,其中提供了大量实用工具,但目前我们只需要使用cpupower
、turbostat
和x86_energy_perf_policy
。为了对与 CPU 有关的优化措施进行验证,可以使用自己熟悉的负载生成工具(例如 Yandex 使用的 Yandex.Tank )对软件进行压力测试。最近的 NginxConf 大会上也有开发者针对 nginx 的负载测试最佳时间进行了演讲:“ NGINX Performance testing ”。
cpupower
这个工具的用法远比慢到爆的/proc/
简单。若要查看有关处理器及其变频调速器(Frequency governor)的信息,可运行:
$ cpupower frequency-info ... driver: intel_pstate ... available cpufreq governors: performance powersave ... The governor "performance" may decide which speed to use ... boost state support: Supported: yes Active: yes
请检查 Turbo Boost 是否已启用。对于 Intel CPU,则要确保运行时使用了intel_pstate
,而非acpi-cpufreq
或pcc-cpufreq
。如果此时继续使用acpi-cpufreq
,那么也许需要升级内核,如果无法升级,则需要需要使用performance
调速器。如果配合intel_pstate
运行,那么就算powersave
调速器也能实现不错的性能,但这一点需要自行验证。
至于空载(Idling),为了了解 CPU 实际遇到的情况,可以使用turbostat
直接查看处理器的 MSR 并获取能耗(Power)、频率(Frequency),以及空闲状态(Idle State)信息:
# turbostat --debug -P ... Avg_MHz Busy% ... CPU%c1 CPU%c3 CPU%c6 ... Pkg%pc2 Pkg%pc3 Pkg%pc6 ...
在这里可以看到 CPU 的实际频率(没错,/proc/cpuinfo 提供的信息并不准),以及内核 / 包空闲状态。
如果就算使用 intel_pstate 驱动,CPU 也在远多于预期的时间处于空闲状态,此时可以:
- 将调速器设置为
performance
。 - 将
x86_energy_perf_policy
设置为performance
。
或者仅针对延迟问题极为敏感的任务,可以:
- 使用 /dev/cpu_dma_latency 接口。
- 为 UDP 流量使用 busy-polling 。
有关处理器能耗管理的常规信息和 P-state 的相关信息,可参阅 Intel 开源技术中心在 LinuxCon Europe 2015 大会上提供的演示文档“ Balancing Power and Performance in the Linux Kernel ”。
CPU 亲缘性
为了进一步降低延迟,还可为每个线程 / 进程应用 CPU 亲缘性(Affinity),例如 nginx 就提供了worker_cpu_affinity
指令,可以自动将 Web 服务器的每个进程绑定至自己的内核。这样可以避免 CPU 迁移,减少缓存未命中和页面错误的情况,并可略微提高每个周期内执行的指令数量。这些效果都可通过perf stat
验证。
然而启用亲缘性也可能对性能产生消极影响,因为这会增加进程等待可用 CPU 过程中的等待时间。为了监视这一情况,可针对 nginx 工作进程的某一个 PID 运行 runqlat :
usecs : count distribution 0 -> 1 : 819 | | 2 -> 3 : 58888 |****************************** | 4 -> 7 : 77984 |****************************************| 8 -> 15 : 10529 |***** | 16 -> 31 : 4853 |** | ... 4096 -> 8191 : 34 | | 8192 -> 16383 : 39 | | 16384 -> 32767 : 17 | |
如果在这里看到多个毫秒级别的延迟,可能意味着服务器上除了 nginx 本身,还运行了太多东西,此时亲缘性会增大,而不能降低延迟。
内存
所有内存调优通常都要针对工作流程的具体情况来进行,这方面可以提供下列几个建议:
- 将 THP 设置为
madvise
并只在确定这样做能够获益的情况下启用,否则可能仅仅让延迟提升 20% 的情况下,会导致性能出现数量级的降低。 - 除非只使用了一个 NUMA 节点,否则应当将
vm.zone_reclaim_mode
设置为 0。
NUMA
比较新的 CPU 实际上包含多个相互独立的 CPU 核心(die),多个核心之间使用非常快速的连接互联,并且会共享各种资源,从超线程核心的 L1 缓存直到整个处理器共用的 L3 缓存,直到整个插槽共用的内存和 PCIe 链路,这些资源都是共享的。而这也正是 NUMA 的主要目标:使用告诉互联连接并联执行和存储单元。
有关 NUMA 的全面介绍和相关影响,可参阅 Frank Denneman 撰写的“ NUMA Deep Dive Series ”文章。
长话短说,此时的选项包括:
- 忽略,可在BIOS 中禁用,或使用
numactl --interleave=all
参数运行软件,这样可以获得不是很高,但始终如一的性能。 - 拒绝,可使用单节点服务器,就如同 Facebook 针对 OCP Yosemite 平台的做法。
- 接受,借此可优化用户空间和内核空间的 CPU/ 内存布局。
进一步谈谈上面的第三个选项吧,毕竟前两个选项并没有太多可供优化的余地。
为了发挥 NUMA 的作用,你需要将每个 NUMA 节点视作一台单独的服务器,因此首先需要检查整个系统的拓扑,为此可运行numactl –hardware
:
$ numactl --hardware available: 4 nodes (0-3) node 0 cpus: 0 1 2 3 16 17 18 19 node 0 size: 32149 MB node 1 cpus: 4 5 6 7 20 21 22 23 node 1 size: 32213 MB node 2 cpus: 8 9 10 11 24 25 26 27 node 2 size: 0 MB node 3 cpus: 12 13 14 15 28 29 30 31 node 3 size: 0 MB node distances: node 0 1 2 3 0: 10 16 16 16 1: 16 10 16 16 2: 16 16 10 16 3: 16 16 16 10
随后需要留意:
- 节点的数量
- 每个节点的内存数
- 每个节点的 CPU 数
- 节点之间的距离
上面其实是个非常糟糕的例子,因为其中包含 4 个节点,其中有几个节点没有附加任何内存。除非牺牲掉系统中半数的处理器内核,否则无法将每个节点视作一台独立的服务器。
为了确认这一点,可以运行numastat
:
$ numastat -n -c Node 0 Node 1 Node 2 Node 3 Total -------- -------- ------ ------ -------- Numa_Hit 26833500 11885723 0 0 38719223 Numa_Miss 18672 8561876 0 0 8580548 Numa_Foreign 8561876 18672 0 0 8580548 Interleave_Hit 392066 553771 0 0 945836 Local_Node 8222745 11507968 0 0 19730712 Other_Node 18629427 8939632 0 0 27569060
此外也可以让numastat
以/proc/meminfo
的格式输出每个节点的内存使用量统计信息:
$ numastat -m -c Node 0 Node 1 Node 2 Node 3 Total ------ ------ ------ ------ ----- MemTotal 32150 32214 0 0 64363 MemFree 462 5793 0 0 6255 MemUsed 31688 26421 0 0 58109 Active 16021 8588 0 0 24608 Inactive 13436 16121 0 0 29557 Active(anon) 1193 970 0 0 2163 Inactive(anon) 121 108 0 0 229 Active(file) 14828 7618 0 0 22446 Inactive(file) 13315 16013 0 0 29327 ... FilePages 28498 23957 0 0 52454 Mapped 131 130 0 0 261 AnonPages 962 757 0 0 1718 Shmem 355 323 0 0 678 KernelStack 10 5 0 0 16
接着用一个简单的拓扑作为例子一起来看看。
$ numactl --hardware available: 2 nodes (0-1) node 0 cpus: 0 1 2 3 4 5 6 7 16 17 18 19 20 21 22 23 node 0 size: 46967 MB node 1 cpus: 8 9 10 11 12 13 14 15 24 25 26 27 28 29 30 31 node 1 size: 48355 MB
由于这些节点大部分都是对称的,因此可以使用numactl --cpunodebind=X --membind=X
将应用程序的每个实例与一个 NUMA 节点绑定,随后将其暴露到不同端口,这样就可以更充分地利用每个节点,借此提高吞吐率,并通过更近的内存位置获得更短延迟。
此时可通过内存操作的延迟验证 NUMA 布局的效率,例如可以 bcc 的funclatency
衡量内存密集型操作,如memmove
的延迟。
在内核方面,我们可以使用perf stat
测量效率,并查找相应的内存和调度器事件:
# perf stat -e sched:sched_stick_numa,sched:sched_move_numa,sched:sched_swap_numa,migrate:mm_migrate_pages,minor-faults -p PID ... 1 sched:sched_stick_numa 3 sched:sched_move_numa 41 sched:sched_swap_numa 5,239 migrate:mm_migrate_pages 50,161 minor-faults
对于网络密集型工作负载,有关 NUMA 的最后一个优化建议是使用 PCIe 接口的网卡设备,并将每个设备与自己的 NUMA 节点绑定,这样在与网络通信时可降低部分 CPU 的延迟。在下文介绍网卡与 CPU 的亲缘性时,还将进一步介绍这种优化措施,不过首先还是来谈谈 PCI-Express……
PCIe
通常来说,除非硬件操作方面遇到问题,否则并不需要深入了解有关 PCIe 的排错。因此一般这方面只需要投入最少量精力即可,为 PCIe 设备创建“链路带宽”、“链路速度”,甚至 RxErr/BadTLP 警报就够了。这些警报可以帮助我们大幅节约由于硬件故障或 PCIe 协商失败所造成问题的排错时间。为此可以使用lspci
:
# lspci -s 0a:00.0 -vvv ... LnkCap: Port #0, Speed 8GT/s, Width x8, ASPM L1, Exit Latency L0s <2us, L1 <16us LnkSta: Speed 8GT/s, Width x8, TrErr- Train- SlotClk+ DLActive- BWMgmt- ABWMgmt- ... Capabilities: [100 v2] Advanced Error Reporting UESta: DLP- SDES- TLP- FCP- CmpltTO- CmpltAbrt- ... UEMsk: DLP- SDES- TLP- FCP- CmpltTO- CmpltAbrt- ... UESvrt: DLP+ SDES+ TLP- FCP+ CmpltTO- CmpltAbrt- ... CESta: RxErr- BadTLP- BadDLLP- Rollover- Timeout- NonFatalErr- CEMsk: RxErr- BadTLP- BadDLLP- Rollover- Timeout- NonFatalErr+
如果有多个高速设备争夺带宽(例如将高速网络连接到高速存储),那么 PCIe 也可能成为瓶颈,因此可能需要从物理上将 PCIe 设备划分给不同 CPU,以获得最高吞吐率。
来源: https://en.wikipedia.org/wiki/PCI_Express#History_and_revisions
此外可参阅 Mellanox 网站上的“了解可获得最优性能的PCIe 配置”这篇文章,此文较为深入地介绍了PCIe 配置,如果发现网卡和操作系统之间存在丢包情况,这篇文章应该能提供一定的帮助。
Intel 认为,有时候 PCIe 电源管理(ASPM)可能导致延迟提高,因进而导致丢包率增高。因此也可以为内核命令行参数添加pcie_aspm=off
将其禁用。
网卡
继续之前,首先有必要注意, Intel 和 Mellanox 都提供了自己的性能调优指南,无论你使用哪家供应商的产品,这两篇指南最好都读一下。此外驱动程序通常也提供了自己的说明文件,并提供了一系列实用的工具。
随后还有必要参阅操作系统配套的手册,例如 Red Hat Enterprise Linux 网络性能调优指南,其中介绍了下文涉及的大部分优化措施,此外还提供了更丰富的建议。
Cloudflare 也通过自己的博客发布了一篇有关网络栈调优的精彩文章,不过这篇文章主要面向偏重于低延迟的用例。
网卡的优化工作少不了会用到 ethtool
。
这方面有个小问题需要注意:如果在使用较新的内核(并且也很有必要这样做!),用户空间中运行的某些工具也有必要及时更新,例如对于网络操作,可能需要使用新版本的ethtool
、iproute2
,甚至iptables/nftables
包。
如果希望针对网卡正在进行的操作获得更深入的了解,可运行ethtool -S
:
$ ethtool -S eth0 | egrep 'miss|over|drop|lost|fifo' rx_dropped: 0 tx_dropped: 0 port.rx_dropped: 0 port.tx_dropped_link_down: 0 port.rx_oversize: 0 port.arq_overflows: 0
有关这些统计结果的详细介绍,请咨询网卡制造商,例如 Mellanox 为这些信息提供了一个详细的维基页面。
在内核方面,我们有必要查看 /proc/interrupts
、/proc/softirqs
和/proc/net/softnet_stat
,这方面有两个很有用的 bcc 工具:hardirqs
和softirqs
。网络优化的目标在于对系统进行不断的调优,直到在不丢包的情况下将 CPU 使用率降至最低。
中断的亲缘性
这方面的调优通常首先会将中断分散到多个处理器上。具体怎么做取决于工作负载的需求:
- 为了获得最大吞吐率,可将中断分散到系统中所有 NUMA 节点上。
- 为了尽可能降低延迟,可将中断限制在一个 NUMA 节点上。为此可能需要减小队列数量,使其可以适合一个节点处理(通常这意味着需要使用
ethtool -L
将队列数量减半)。
供应商通常会提供执行这类操作的脚本,例如 Intel 就提供了set_irq_affinity
。
Ring 缓冲区大小
网卡需要与内核交换信息。这通常是通过一种名为“Ring”的数据结构实现的,若要查看这种 Ring 的当前 / 最大大小,可使用ethtool -g
:
$ ethtool -g eth0 Ring parameters for eth0: Pre-set maximums: RX: 4096 TX: 4096 Current hardware settings: RX: 4096 TX: 4096
我们可以在pre-set maximums
中通过-G
调整这些值。一般来说,这些值越大越好(尤其是在使用中断联合(Interrupt coalescing)的情况下),这样做可以面对爆发和内核中卡顿获得更好的保护,进而减少由于缓冲区无空间 / 错过中断造成的丢包。但也要注意:
联合(Coalescing)
中断联合可供我们推迟向内核通告新事件的操作,将多个事件汇总在一个中断中通知内核。该功能的当前设置可通过ethtool -c
查看:
$ ethtool -c eth0 Coalesce parameters for eth0: ... rx-usecs: 50 tx-usecs: 50
此处可以设置固定上限,对每内核每秒处理中断数量的最大值进行硬性限制,或针对特定硬件根据吞吐率自动调整中断速率。
启用联合(使用 -C
)会增大延迟并可能导致丢包,因此对延迟敏感的工作可能需要避免这样做。另外,彻底禁用该功能可能导致中断受到节流限制,进而影响性能。
卸载
现代化网卡其实相当智能,可将大部分工作卸载给硬件,或在驱动中模仿这样的卸载。
若要查看所有可支持的卸载操作,可使用ethtool -k
:
$ ethtool -k eth0 Features for eth0: ... tcp-segmentation-offload: on generic-segmentation-offload: on generic-receive-offload: on large-receive-offload: off [fixed]
在上述输出结果中,所有不可调优的卸载操作都标注了 [fixed] 后缀。
关于这些操作有太多可说的东西,下文将列举一些从经验得来的规则:
- 不要启用 LRO,请改为使用 GRO。
- 对于 TSO 一定要小心,该操作严重受制于驱动 / 固件本身的质量。
- 不要对老版本内核启用 TSO/GSO,这可能导致层出不穷的缓冲区过满问题。** 数据包操控(Packet Steering)** 所有现代化网卡均针对多核心硬件进行了优化,因此可以在内部将数据包拆分到不同的虚拟队列中,通常每个 CPU 一个队列。当这一切是通过硬件完成时,这个过程称之为 RSS;当由操作系统负责通过多个 CPU 对数据包进行负载均衡时,这个过程可称之为 RPS(对应的 TX 技术也叫做 XPS)。当操作系统也希望智能地将通信流路由到正在处理当前 Socket 的 CPU 时,这一过程叫做 RFS。硬件执行该操作的过程可叫做“加速的 RFS”,或简称为 aRFS。
从我们的生产环境来说,这方面有几个最佳实践:
- 如果使用较新的 25G+ 硬件,可能已经有了足够的队列,并能通过一个足够大的间接表跨越所有节点进行RSS。一些老的网卡可能存在局限,只能使用前 16 个 CPU。
- 如果符合下列情况,可以试着启用RPS:
- CPU 数量超过硬件队列数量,希望牺牲延迟换来更高吞吐率。
- 所用的内部隧道(例如 GRE/IPinIP)使得网卡无法支持 RSS。
- 如果 CPU 相当老并且不支持 x2APIC,请不要启用RPS。
- 通过 _XPS*_ 将每个 CPU 绑定到自己的 TX 队列,通常来说是推荐的做法。
- RFS的效果严重取决于工作负载,以及是否对其应用了 CPU 亲缘性。
Flow Director 和 ATR
启用 Application Targeting Routing 模式默认使用的Flow Director(即 Intel 所谓的 fdir),可对数据包进行采样并将通信流引导至按照推测负责执行处理任务的内核,借此实现aRFS。该功能的统计信息也可通过ethtool -S:$ ethtool -S eth0 | egrep ‘fdir’ port.fdir_flush_cnt: 0
查看。
虽然 Intel 宣称 fdir可在某些情况下改善性能,但外界的研究发现该功能也可能导致最多 1% 的数据包重排序,这会对 TCP 性能产生相当大的影响。因此请尝试自行测试以确定 FD 是否能让你的工作负载获益,同时需要密切关注 TCPOFOQueue 计数器。
感谢丁晓昀对本文的审校。
评论