[关闭]
@liuhui0803 2017-09-15T13:07:33.000000Z 字数 10311 阅读 2380

Dropbox高吞吐量低延迟Web服务器优化之法(上篇)

nginx 优化 流量 Web服务器 测试


摘要:

本文从硬件和驱动等底层内容,Linux内核及TCP/IP栈,以及库和应用程序层面的调优等角度介绍了针对常规用途Web服务器,尤其是nginx进行性能优化的思路。文章共分为上下两篇,上篇主要介绍硬件、驱动方面的优化措施和建议;下篇主要介绍Linux系统以及应用程序等方面的措施和建议。

正文:
本文最初发布于Dropbox官方博客,原作者Alexey Ivanov,经授权由InfoQ中文站翻译并分享。

flame-graph.png-222.7kB

本文是基于我在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帧下推至内核层面(例如FreeBSDLinux)。

软件方面,建议选择邮件列表和用户社区活动都比较活跃的开源驱动。如果要对与驱动有关的问题进行调试(很可能需要这样做),这一点往往非常重要。

内存

根据经验来说,对延迟敏感的任务往往需要速度更快的内存,而对吞吐率敏感的任务往往需要容量更大的内存。

硬盘

主要取决于缓冲/缓存需求,但如果要对更多内容创建缓冲或缓存,则应选择基于闪存的存储设备。有些人甚至会使用针对闪存进行优化的特殊文件系统(通常是以日志为结构的),但这些文件系统并非总能胜过普通的ext4/xfs。

此外任何情况下都要注意避免因为忘了启用TRIM或更新固件而导致闪存被烧穿(Burn through)。

操作系统:底层

固件

为避免痛苦并且冗长的排错过程,应尽可能确保固件保持最新版本。请尽量确保CPU微码,以及主板、网卡、SSD的固件始终保持最新状态。但这并不是说上述内容有了新版本就要第一时间更新,一般来说,建议始终保持使用最新版固件的前一个版本,除非最新版修复了某些非常重要的Bug,并且版本也不要落后太多。

驱动

驱动的更新规则与固件的更新差不多:尽可能接近最新版就行了。不过此处需要注意一个问题:可能的情况下,尽量将内核升级和驱动更新分开进行。例如可以使用DKMS将驱动打包,或针对自己使用的所有内核版本进行预编译驱动。这样当更新内核后如果有什么东西无法按照预期正常运转,那么排错过程中也可以减少一个怀疑的目标。

CPU

这方面要善用内核代码库和自带的各种工具。你可以在Ubuntu/Debian中安装linux-tools包,其中提供了大量实用工具,但目前我们只需要使用cpupowerturbostatx86_energy_perf_policy。为了对与CPU有关的优化措施进行验证,可以使用自己熟悉的负载生成工具(例如Yandex使用的Yandex.Tank)对软件进行压力测试。最近的NginxConf大会上也有开发者针对nginx的负载测试最佳时间进行了演讲:“NGINX Performance testing”。

cpupower

这个工具的用法远比慢到爆的/proc/简单。若要查看有关处理器及其变频调速器(Frequency governor)的信息,可运行:

  1. $ cpupower frequency-info
  2. ...
  3. driver: intel_pstate
  4. ...
  5. available cpufreq governors: performance powersave
  6. ...
  7. The governor "performance" may decide which speed to use
  8. ...
  9. boost state support:
  10. Supported: yes
  11. Active: yes

请检查Turbo Boost是否已启用。对于Intel CPU,则要确保运行时使用了intel_pstate,而非acpi-cpufreqpcc-cpufreq。如果此时继续使用acpi-cpufreq,那么也许需要升级内核,如果无法升级,则需要需要使用performance调速器。如果配合intel_pstate运行,那么就算powersave调速器也能实现不错的性能,但这一点需要自行验证。

至于空载(Idling),为了了解CPU实际遇到的情况,可以使用turbostat直接查看处理器的MSR并获取能耗(Power)、频率(Frequency),以及空闲状态(Idle State)信息:

  1. # turbostat --debug -P
  2. ... Avg_MHz Busy% ... CPU%c1 CPU%c3 CPU%c6 ... Pkg%pc2 Pkg%pc3 Pkg%pc6 ...

在这里可以看到CPU的实际频率(没错,/proc/cpuinfo提供的信息并不准),以及内核/包空闲状态

如果就算使用intel_pstate驱动,CPU也在远多于预期的时间处于空闲状态,此时可以:

或者仅针对延迟问题极为敏感的任务,可以:

有关处理器能耗管理的常规信息和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

  1. usecs : count distribution
  2. 0 -> 1 : 819 | |
  3. 2 -> 3 : 58888 |****************************** |
  4. 4 -> 7 : 77984 |****************************************|
  5. 8 -> 15 : 10529 |***** |
  6. 16 -> 31 : 4853 |** |
  7. ...
  8. 4096 -> 8191 : 34 | |
  9. 8192 -> 16383 : 39 | |
  10. 16384 -> 32767 : 17 | |

如果在这里看到多个毫秒级别的延迟,可能意味着服务器上除了nginx本身,还运行了太多东西,此时亲缘性会增大,而不能降低延迟。

内存

所有内存调优通常都要针对工作流程的具体情况来进行,这方面可以提供下列几个建议:

NUMA

比较新的CPU实际上包含多个相互独立的CPU核心(die),多个核心之间使用非常快速的连接互联,并且会共享各种资源,从超线程核心的L1缓存直到整个处理器共用的L3缓存,直到整个插槽共用的内存和PCIe链路,这些资源都是共享的。而这也正是NUMA的主要目标:使用告诉互联连接并联执行和存储单元。

有关NUMA的全面介绍和相关影响,可参阅Frank Denneman撰写的“NUMA Deep Dive Series”文章。

长话短说,此时的选项包括:

进一步谈谈上面的第三个选项吧,毕竟前两个选项并没有太多可供优化的余地。

为了发挥NUMA的作用,你需要将每个NUMA节点视作一台单独的服务器,因此首先需要检查整个系统的拓扑,为此可运行numactl –hardware

  1. $ numactl --hardware
  2. available: 4 nodes (0-3)
  3. node 0 cpus: 0 1 2 3 16 17 18 19
  4. node 0 size: 32149 MB
  5. node 1 cpus: 4 5 6 7 20 21 22 23
  6. node 1 size: 32213 MB
  7. node 2 cpus: 8 9 10 11 24 25 26 27
  8. node 2 size: 0 MB
  9. node 3 cpus: 12 13 14 15 28 29 30 31
  10. node 3 size: 0 MB
  11. node distances:
  12. node 0 1 2 3
  13. 0: 10 16 16 16
  14. 1: 16 10 16 16
  15. 2: 16 16 10 16
  16. 3: 16 16 16 10

随后需要留意:

上面其实是个非常糟糕的例子,因为其中包含4个节点,其中有几个节点没有附加任何内存。除非牺牲掉系统中半数的处理器内核,否则无法将每个节点视作一台独立的服务器。

为了确认这一点,可以运行numastat

  1. $ numastat -n -c
  2. Node 0 Node 1 Node 2 Node 3 Total
  3. -------- -------- ------ ------ --------
  4. Numa_Hit 26833500 11885723 0 0 38719223
  5. Numa_Miss 18672 8561876 0 0 8580548
  6. Numa_Foreign 8561876 18672 0 0 8580548
  7. Interleave_Hit 392066 553771 0 0 945836
  8. Local_Node 8222745 11507968 0 0 19730712
  9. Other_Node 18629427 8939632 0 0 27569060

此外也可以让numastat/proc/meminfo的格式输出每个节点的内存使用量统计信息:

  1. $ numastat -m -c
  2. Node 0 Node 1 Node 2 Node 3 Total
  3. ------ ------ ------ ------ -----
  4. MemTotal 32150 32214 0 0 64363
  5. MemFree 462 5793 0 0 6255
  6. MemUsed 31688 26421 0 0 58109
  7. Active 16021 8588 0 0 24608
  8. Inactive 13436 16121 0 0 29557
  9. Active(anon) 1193 970 0 0 2163
  10. Inactive(anon) 121 108 0 0 229
  11. Active(file) 14828 7618 0 0 22446
  12. Inactive(file) 13315 16013 0 0 29327
  13. ...
  14. FilePages 28498 23957 0 0 52454
  15. Mapped 131 130 0 0 261
  16. AnonPages 962 757 0 0 1718
  17. Shmem 355 323 0 0 678
  18. KernelStack 10 5 0 0 16

接着用一个简单的拓扑作为例子一起来看看。

  1. $ numactl --hardware
  2. available: 2 nodes (0-1)
  3. node 0 cpus: 0 1 2 3 4 5 6 7 16 17 18 19 20 21 22 23
  4. node 0 size: 46967 MB
  5. node 1 cpus: 8 9 10 11 12 13 14 15 24 25 26 27 28 29 30 31
  6. node 1 size: 48355 MB

由于这些节点大部分都是对称的,因此可以使用numactl --cpunodebind=X --membind=X将应用程序的每个实例与一个NUMA节点绑定,随后将其暴露到不同端口,这样就可以更充分地利用每个节点,借此提高吞吐率,并通过更近的内存位置获得更短延迟。

此时可通过内存操作的延迟验证NUMA布局的效率,例如可以bcc的funclatency衡量内存密集型操作,如memmove的延迟。

在内核方面,我们可以使用perf stat测量效率,并查找相应的内存和调度器事件:

  1. # perf stat -e sched:sched_stick_numa,sched:sched_move_numa,sched:sched_swap_numa,migrate:mm_migrate_pages,minor-faults -p PID
  2. ...
  3. 1 sched:sched_stick_numa
  4. 3 sched:sched_move_numa
  5. 41 sched:sched_swap_numa
  6. 5,239 migrate:mm_migrate_pages
  7. 50,161 minor-faults

对于网络密集型工作负载,有关NUMA的最后一个优化建议是使用PCIe接口的网卡设备,并将每个设备与自己的NUMA节点绑定,这样在与网络通信时可降低部分CPU的延迟。在下文介绍网卡与CPU的亲缘性时,还将进一步介绍这种优化措施,不过首先还是来谈谈PCI-Express……

PCIe

通常来说,除非硬件操作方面遇到问题,否则并不需要深入了解有关PCIe的排错。因此一般这方面只需要投入最少量精力即可,为PCIe设备创建“链路带宽”、“链路速度”,甚至RxErr/BadTLP警报就够了。这些警报可以帮助我们大幅节约由于硬件故障或PCIe协商失败所造成问题的排错时间。为此可以使用lspci

  1. # lspci -s 0a:00.0 -vvv
  2. ...
  3. LnkCap: Port #0, Speed 8GT/s, Width x8, ASPM L1, Exit Latency L0s <2us, L1 <16us
  4. LnkSta: Speed 8GT/s, Width x8, TrErr- Train- SlotClk+ DLActive- BWMgmt- ABWMgmt-
  5. ...
  6. Capabilities: [100 v2] Advanced Error Reporting
  7. UESta: DLP- SDES- TLP- FCP- CmpltTO- CmpltAbrt- ...
  8. UEMsk: DLP- SDES- TLP- FCP- CmpltTO- CmpltAbrt- ...
  9. UESvrt: DLP+ SDES+ TLP- FCP+ CmpltTO- CmpltAbrt- ...
  10. CESta: RxErr- BadTLP- BadDLLP- Rollover- Timeout- NonFatalErr-
  11. CEMsk: RxErr- BadTLP- BadDLLP- Rollover- Timeout- NonFatalErr+

如果有多个高速设备争夺带宽(例如将高速网络连接到高速存储),那么PCIe也可能成为瓶颈,因此可能需要从物理上将PCIe设备划分给不同CPU,以获得最高吞吐率。

pcie-table.png-137.3kB
来源:https://en.wikipedia.org/wiki/PCI_Express#History_and_revisions

此外可参阅Mellanox网站上的“了解可获得最优性能的PCIe配置”这篇文章,此文较为深入地介绍了PCIe配置,如果发现网卡和操作系统之间存在丢包情况,这篇文章应该能提供一定的帮助。

Intel认为,有时候PCIe电源管理(ASPM)可能导致延迟提高,因进而导致丢包率增高。因此也可以为内核命令行参数添加pcie_aspm=off将其禁用。

网卡

继续之前,首先有必要注意,IntelMellanox都提供了自己的性能调优指南,无论你使用哪家供应商的产品,这两篇指南最好都读一下。此外驱动程序通常也提供了自己的说明文件,并提供了一系列实用的工具。

随后还有必要参阅操作系统配套的手册,例如Red Hat Enterprise Linux网络性能调优指南,其中介绍了下文涉及的大部分优化措施,此外还提供了更丰富的建议。

Cloudflare也通过自己的博客发布了一篇有关网络栈调优的精彩文章,不过这篇文章主要面向偏重于低延迟的用例。

网卡的优化工作少不了会用到ethtool

这方面有个小问题需要注意:如果在使用较新的内核(并且也很有必要这样做!),用户空间中运行的某些工具也有必要及时更新,例如对于网络操作,可能需要使用新版本的ethtooliproute2,甚至iptables/nftables包。

如果希望针对网卡正在进行的操作获得更深入的了解,可运行ethtool -S

  1. $ ethtool -S eth0 | egrep 'miss|over|drop|lost|fifo'
  2. rx_dropped: 0
  3. tx_dropped: 0
  4. port.rx_dropped: 0
  5. port.tx_dropped_link_down: 0
  6. port.rx_oversize: 0
  7. port.arq_overflows: 0

有关这些统计结果的详细介绍,请咨询网卡制造商,例如Mellanox为这些信息提供了一个详细的维基页面

在内核方面,我们有必要查看/proc/interrupts/proc/softirqs/proc/net/softnet_stat,这方面有两个很有用的bcc工具:hardirqssoftirqs。网络优化的目标在于对系统进行不断的调优,直到在不丢包的情况下将CPU使用率降至最低。

中断的亲缘性

这方面的调优通常首先会将中断分散到多个处理器上。具体怎么做取决于工作负载的需求:

供应商通常会提供执行这类操作的脚本,例如Intel就提供了set_irq_affinity

Ring缓冲区大小

网卡需要与内核交换信息。这通常是通过一种名为“Ring”的数据结构实现的,若要查看这种Ring的当前/最大大小,可使用ethtool -g

  1. $ ethtool -g eth0
  2. Ring parameters for eth0:
  3. Pre-set maximums:
  4. RX: 4096
  5. TX: 4096
  6. Current hardware settings:
  7. RX: 4096
  8. TX: 4096

我们可以在pre-set maximums中通过-G调整这些值。一般来说,这些值越大越好(尤其是在使用中断联合(Interrupt coalescing)的情况下),这样做可以面对爆发和内核中卡顿获得更好的保护,进而减少由于缓冲区无空间/错过中断造成的丢包。但也要注意:

联合(Coalescing)

中断联合可供我们推迟向内核通告新事件的操作,将多个事件汇总在一个中断中通知内核。该功能的当前设置可通过ethtool -c查看:

  1. $ ethtool -c eth0
  2. Coalesce parameters for eth0:
  3. ...
  4. rx-usecs: 50
  5. tx-usecs: 50

此处可以设置固定上限,对每内核每秒处理中断数量的最大值进行硬性限制,或针对特定硬件根据吞吐率自动调整中断速率

启用联合(使用-C)会增大延迟并可能导致丢包,因此对延迟敏感的工作可能需要避免这样做。另外,彻底禁用该功能可能导致中断受到节流限制,进而影响性能。

卸载

现代化网卡其实相当智能,可将大部分工作卸载给硬件,或在驱动中模仿这样的卸载。

若要查看所有可支持的卸载操作,可使用ethtool -k

  1. $ ethtool -k eth0
  2. Features for eth0:
  3. ...
  4. tcp-segmentation-offload: on
  5. generic-segmentation-offload: on
  6. generic-receive-offload: on
  7. large-receive-offload: off [fixed]

在上述输出结果中,所有不可调优的卸载操作都标注了[fixed]后缀。

关于这些操作有太多可说的东西,下文将列举一些从经验得来的规则:

从我们的生产环境来说,这方面有几个最佳实践:

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计数器。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注