如何优化Web服务器以实现高吞吐量和低延迟

更新时间:2017-09-20 09:40:57 点击次数:2348次

这篇文章将讨论如何调优web服务器和代理服务器,并采用科学的方法,将它们逐一应用测量效果,终确定是否真的对所处的环境有用。

这不是一篇关于Linux性能的文章,尽管它大量引用了bcc工具、eBPF和perf,但不要误以为是使用性能分析工具的全面指南。如果你想了解更多关于他们的信息,可以通过Brendan Gregg的博客阅读。

这也不是一篇关于浏览器性能的文章。当介绍与延迟相关的优化时,会涉及到客户端的性能,但只是短暂的。如果你想知道更多这方面的,可以阅读Ilya Grigorik的高性能浏览器网络

后,这也不是关于TLS(传输层安全协议)的佳实践编译,尽管会提到TLS库及其设置,但那是为了评估每种方法的性能和安全性。可以使用Qualys SSL测试,以验证网络终端与当前的佳实践,如果想了解更多关于TLS的信息,请考虑阅读Feisty Duck Bulletproof TLS Newsletter

文章的结构

文章将讨论系统不同层级的效率/性能优化。从硬件和驱动程序的低级别开始:这些调优可以应用到几乎任何高负载服务器上。然后将转移到linux内核及其TCP/IP协议栈:这些是在任何一个TCP-heavy盒子上尝试的旋钮。后将讨论库和应用程序级别的调优,它们主要适用于一般的web服务器和nginx服务器。

对于每一个潜在的优化领域,将尝试提供一些关于延迟/吞吐量权衡(如果有的话)的背景知识,以及监控指导方针,后建议对不同的工作负载作调整。

硬件方面

CPU

对于良好的非对称RSA/EC性能,具有至少AVX2(AVX2 in /proc/ cpuinfo)支持的处理器,好是具有大型整数算术能力硬件(bmi和adx)的处理器。而对于对称的情况,应该找AES ciphers和AVX512的ChaCha+Poly。英特尔通过OpenSSL 1.0.2对不同的硬件产品进行了性能比较,说明了这些硬件卸载的影响。

CPU延迟敏感的用例,如路由选择,减少NUMA的节点和禁用HT会有帮助。拥有更多内核意味着高吞吐量的任务会处理得更好,并且将从超线程(除非它们是缓存绑定)中受益,而且通常不会过多地关心NUMA。

具体地说,如果走英特尔的路线,至少需要haswell或broadwell架构的处理器与合适的Skylake CPUs。如果采用AMD,那么EPYC相当不错。

NIC(网络适配器)

至少需要10G网卡,好是25G。如果想要通过TLS单个服务器达到更大的传输,这里所描述的调优是不够的,可能需要将TLS构建到内核级别(例如FreeBSDLinux)。

在软件方面,应该寻找具有活动邮件列表和用户社区的开源驱动程序。如果涉及到调试与驱动相关的问题,这将是非常重要的。

内存

选择的经验法则是,延迟敏感的任务需要更快的内存,而吞吐量敏感的任务需要更多的内存。

硬盘

这取决于缓冲/缓存的需求,如果要缓存大量的内容,应该选择基于flash的存储。有些甚至采用了专门的flash友好文件系统(通常是日志结构的),但是它们并不总是比普通的ext4/xfs性能更好。

无论如何,注意不要因为忘记启用TRIM或更新固件而烧穿了flash。

操作系统:低水平

固件

保持固件新,以避免痛苦和冗长的故障排除会话。尽量保持新的CPU微码,主板,NICs和ssd固件。这并不意味着要一直花钱,经验法则是把固件更新到上一个版本就行,除非它在新版本中修复了关键错误,否则只要不要太落后就行。

驱动

更新规则和固件更新差不多,尝试保持新。这里需要注意的是,如果可能的话,尝试将内核升级与驱动程序更新解耦。例如,可以用DKMS打包驱动程序,或者为使用的所有内核版本预编译驱动程序。这样当更新内核出现故障时不需要考虑驱动程序。

CPU

在Ubuntu/Debian中,可以使用一些实用工具安装linux工具包,但是现在只使用cpupowerturbostatx86_energy_perf_policy即可。为了验证cpu相关的优化,可以通过常用的负载生成工具对软件进行压力测试(例如,Yandex使用Yandex.Tank)。下面是有一份来自NginxConf开发者的演示,介绍了nginx加载测试的佳实践:“nginx性能测试”。

cpupower

使用这个工具比crawling /proc/更容易。要查看有关处理器及其频率调控器的信息,请运行以下代码。

$ 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。如果坚持使用acpic-cpufreq,那么应该升级内核,或者如果不能升级,确保使用的是performance调控器。在使用intel_pstate运行时,powersave调控器也应该运行良好,但这一步需要自己去验证。

想看看空载时CPU到底发生了什么,可以使用turbostat直接查看处理器的MSRs,获取电源、频率和空闲状态信息。

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

这里可以看到实际的CPU频率(是的,/proc/cpuinfo欺骗了你),以及核心/包空闲状态。 
如果使用intel_pstate驱动程序,CPU的空闲时间会比预想的要多,可以采取以下措施。

或者,对于非常延迟的关键任务,还可以这样做。

更多关于处理器电源管理的信息,可以在2015年的LinuxCon Europe上由“Linux内核”的Intel开源技术中心发表的“Linux内核中的平衡能力和性能”文章中了解到。

CPU亲和力

还可以通过在每个线程/流程上应用CPU关联性来降低延迟,例如nginx,它具有worker_cpu_affinity指令,可以自动将每个web服务器进程绑定到它自己的核心。这可以消除CPU迁移,减少缓存遗漏和页面错误,并稍微增加每个周期的指令数。所有这些都可以通过perf stat来验证。

遗憾的是,启用亲和性也会增加等待空闲CPU的时间,从而对性能产生负面影响。可以通过在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本身之外,服务器可能在处理其他很多的事,一旦关联将增加延迟,而不是减少。

内存

所有的mm/tunings通常都是非常具体的工作流,可以推荐的方法很少。

现代的CPU实际上包含多个独立的CPU,它们通过高速互连和共享资源,从HT核心的L1缓存开始,经过包内的L3高速缓存,再到内存和套接字内PCIe链路。这基本上就组成了NUMA,它具有快速互连的多个执行和存储单元。

对于NUMA的全面概述及其影响,可以参考Frank Denneman的“NUMA深入分析系列”。

对于NUMA长话短说,可以进行以下选择:

现在讨论第三种选择,因为前两种方法并没有太多的优化。

要合理地使用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

考虑的因素有:

这是一个非常糟糕的用例,因为它有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

也可以以/proc/meminfo格式要求numastat输出每个节点的内存使用统计信息。

$ 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

对network-heavy工作负载,后一点与numa相关的优化来自于一个事实,即网络适配器是一个PCIe设备,每个设备都绑定到自己的numa节点,因此在与网络通信时,一些cpu的延迟时间会较低。讨论NIC和CPU亲和力时将涉及到优化可以应用到哪些地方,现在先说一说PCIe。

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可能会成为一个瓶颈,如果存在多个高速设备竞争带宽(例如将快速网络与快速存储结合起来),那么可能需要在cpu之间物理地切分PCI总线设备以获得大的吞吐量。


image

图片来源:https://en.wikipedia.org/wiki/PCI_Express History_and_revisions

Mellanox网站上有篇文章“理解PCIe配置的大性能”,更深入的介绍了PCIe配置,对于高速传输中出现卡顿和操作系统之间的数据包丢失会有所帮助。

英特尔公司表示,有时PCIe的权力管理(ASPM)可能导致更高的延迟,从而导致更高的包损失,但可以通过向内核cmdline添加pcie_aspm=off禁用它。

NIC

开始之前需要说明的是,Intel和Mellanox都有自己的性能调优指南,无论选择哪种供应商,都有助于了解它们。同时,驱动程序通常也有自己的README和一套实用工具。

下一个需要阅读的是操作系统的手册,例如红帽企业版Linux系统网络性能调优指南,它提到了下面的大多数优化,甚至更多。

Cloudflare也提供了一篇关于在博客上调优网络堆栈的一部分的文章,尽管它主要是针对低延迟的用例。

在优化网络适配器时,ethtool将提供很好的帮助。

注意,如果正在使用一个新的内核,应该在userland中添加一些部分,比如对于网络操作,可能需要新的: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

与NIC制造商进行详细的统计描述,例如Mellanox为他们提供了一个专门的wiki页面

内核方面将看到/proc/interrupts、/proc/softirqs和/proc/net/softnet_stat。这里有两个有用的bcc工具,hardirqs和softirqs。优化网络的目标是在没有数据包丢失的情况下,对系统进行优化,直至达到小的CPU使用量。

中断亲和力

调优通常从处理器之间的扩展中断开始,具体怎么做取决于工作量。

供应商通常提供脚本以达到目的,例如英特尔有set_irq_affinity

环形缓冲区大小

网卡与内核交换信息一般是通过一种叫做“环”的数据结构来完成的,该数据结构借助ethtool -g可以查看该“环”的当前/大尺寸。

$ ethtool -g eth0
Ring parameters for eth0:
Pre-set maximums: RX: 4096 TX: 4096 Current hardware settings: RX: 4096 TX: 4096

可以在预先设置的大值与-G之间作调整,一般来说越大越好(特别是如果使用中断合并时),因为它将给予更多的保护来防止突发事件和内核内的间断,因此降低了由于没有缓冲区空间/错过中断而减少的数据包数量。但有几点需要说明:

合并

中断合并允许通过在一个中断中聚合多个事件来延迟通知内核的新事件。当前设置可以通过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]后缀标记。

关于它们有很多讨论,但这里有一些经验法则。

以下是生产中的一些佳实践。

导流器和ATR

启用导流器(或英特尔术语中的fdir)在应用程序的目标路由模式中是默认操作,该应用程序通过取样包和转向系统来实现aRFS,并将其控制在可能被处理的核心位置。它的统计数据也可以通过ethtool - s:$ ethtool - s eth0 |egrep “fdir” port.fdir_flush_cnt:0实现。

尽管英特尔声称fdir在某些情况下提高了性能,但外部研究表明它也引发了多1%的包重新排序,这对TCP性能有很大的损害。因此可以测试一下,看看FD是否对工作负载有帮助,同时要注意TCPOFOQueue计数器。

操作系统:网络栈

对于Linux网络堆栈的调优,有无数的书籍、视频和教程。可悲的是,大量的“sysctl.conf cargo-culting”随之而来,即使近的内核版本不需要像10年前那样多的调优,而且大多数新的TCP/IP特性在默认情况下都是启用和已调优的,人们仍然在复制旧的已经使用了2.6.18/2.6.32内核的sysctls.conf。

为了验证与网络相关的优化效果,需要:

对于关于网络优化的信息来源,个人喜欢由CDN-folks进行的会议讨论,因为他们通常知道在做什么,比如LinuxCon在澳大利亚的快速发展。听Linux内核开发人员关于网络的说法也是很有启发性的,例如netdevconf对话NETCONF记录

通过PackageCloud深入到Linux网络堆栈中时以下需要注意,特别是当侧重监视而不是盲目的调优时。

开始之前再次重申:请升级内核!有大量的新网络堆栈改进,比如关于新的热像:TSO自动调整,FQ,步测,TLP,和机架,但以后更多。通过升级到一个新的内核,将得到一大堆拓展性改进,如删除路由缓存、lockless侦听套接字、SO_REUSEPORT等等。

概述

近的Linux网络文章中非常显目的是“让Linux TCP快速运行”。它通过将Linux sender-side TCP堆栈分解为功能块,从而巩固了Linux内核的多年改进。


image

公平队列和pacing

公平队列负责改善TCP流之间的公平性和减少行阻塞,这对包的下降率有积极的影响。以拥塞控制的速度设定数据包,以同样的间隔时间间隔来设置数据包,从而进一步减少数据包损失,终提高吞吐量。

附注一点:在linux中,通过fq qdisc可以获得公平队列和pacing。不要误以为这些是BBR的要求,它们都可以与CUBIC一同使用以减少15-20%的数据包损失,从而提高CCs上损失的吞吐量。只是不要在旧的内核(低于3.19版本)中使用它,因为会结束pacing pure ACKs,并破坏uploads/RPCs。

TSO autosizing和TSQ

两种方法都负责限制TCP堆栈中的缓冲,从而降低延迟,且不会牺牲吞吐量。

拥塞控制

CC算法本身设计领域很广,近年来围绕它们进行了大量的活动。其中一些活动被编纂为:tcp_cdg(CAIA)tcp_nv(Facebook)tcp_bbr(谷歌)。这里不会太深入地讨论他们的内部工作,先假设所有的人都依赖于延迟的增加而不是数据包的减少。

BBR可以说是所有新的拥塞控制中,文档完整、可测试和实用的。基本思想是建立一个基于包传输速率的网络路径模型,然后执行控制循环,大限度地提高带宽,同时小化rtt。这正是代理堆栈中需要的。

来自BBR实验的初步数据显示,文件下载速度有提升。


image

在东京的BBR实验6小时:x轴表示时间,y轴表示客户端下载速度。

需要强调的是有观察到所有百分位数的速度都在增长,这不是后端修改引起。通常只会给p90+用户带来好处(互联网连接速度快的用户),因为其他所有人都是带宽限制的。网络级的调优,比如改变拥塞控制,或者启用FQ /pacing显示用户没有带宽限制,但是他们是“tcp - limited”。

如果想了解更多关于BBR的信息,APNIC有一个很好的入门级的BBR概述(它与基于损失的拥挤控制的对比)。关于BBR的更深入的信息,可以阅读bbr-dev邮件列表档案(它在顶部有许多有用的链接)。对拥塞控制感兴趣的,可以关注网络拥塞控制研究小组的有趣活动。

ACK处理和丢失检测

关于拥塞控制已经说了很多,下面再次运行新的内核讨论一下丢失检测。那些新的启发式方法,比如TLP和RACK,经常被添加到TCP,而FACK和ER等旧的东西正在被淘汰。一旦添加,它们是默认启用的,因此在升级之后不需要调整任何系统设置。

用户空间优先级和HOL

用户空间套接字api提供隐式缓冲,一旦被发送,就无法重新排序块,因此在多路复用场景中(例如HTTP/2),可能导致HOL阻塞,以及h2优先级反转。TCP_NOTSENT_LOWAT套接字选项(和相应的net.ipv4.tcp_notsent_lowat sysctl)被设计用来解决这个问题,它设置了一个阈值,在这个阈值中,套接字会认为自己是可写的(即epoll会对欺骗应用程序)。从而可以解决使用HTTP/2优先级的问题,但它也可能对吞吐量产生负面影响。

Sysctls

在谈到网络优化的时候,不能不提到关于sysctls的调优。先从不熟悉的开始。 
net.ipv4.tcp_tw_recycle=1——不要使用它——它已经被NAT的用户破坏了,但是如果升级内核将有所改善。 
net.ipv4.tcp_timestamps=0——不要禁用它们,除非已知所有的副作用,而且可以接受它们。例如,一个不明显的副作用是可以在syncookie上打开窗口缩放和SACK选项

对于sysctls需要使用:

同样值得注意的是,有一份由curl的作者Daniel Stenberg编写的RFC草案,命名为HTTP的TCP调优,它试图整理所有可能对HTTP有利的系统调优。

应用程序级别:中层

工具

就像内核一样,拥有新的用户空间是非常重要的。从升级工具开始,例如打包新版本的perf、bcc等。

一旦有了新的工具,就可以适当地调整和观察系统的行为。这一部分主要依赖于通过perf top、on-CPU flamegraphs和来自bcc funclatency的临时直方图进行on-cpu分析。


image

编译器工具链

如果想要编译硬件优化的组件,那么拥有一个现代的编译器工具链是必不可少的,这在许多web服务器通常使用的库中也是存在的。

除了性能之外,新的编译器具有新的安全特性(如-fstack-protector-strongSafeStack),这些特性是想在边缘网络中应用的。现代工具链的另一个用例是,当运行测试工具时,杀毒软件(例如AddressSanitizer和friends)编译的是二进制文件。

系统库

推荐升级系统库,比如glibc,因为在其他方面,可能会错过-lc、-lm、-lrt等低级功能中近存在的优化。

Zlib

通常,web服务器将负责压缩。根据多少数据将通过代理,偶尔会在perf top看到zlib的符号,例如:

# perf top ... 8.88%  nginx        [.] longest_match 8.29%  nginx        [.] deflate_slow 1.90%  nginx        [.] compress_block

在低级别上有一些优化的方法:英特尔和Cloudflare,以及一个独立的zlib-ng项目,都有他们的zlib forks,可以通过使用新的指令集提供更好的性能。

Malloc

在讨论优化之前,我们主要是面向cpu的,但是现在换一个角度,讨论与内存相关的优化。如果使用大量的Lua和FFI或第三方的重模块来执行它们自己的内存管理,可能会由于碎片化而增加内存使用。可以尝试通过切换到jemalloc或tcmalloc来解决这个问题。

使用自定义malloc有以下好处。

PCRE

如果在nginx configs中使用许多复杂的正则表达式,或者严重依赖Lua,perf top会显示与pcre相关的标志。可以通过使用JIT编译PCRE进行优化,也可以通过pcre_jit在nginx中启用它。

通过查看火焰图或者使用funclatency检查优化的结果。

# funclatency /srv/nginx-bazel/sbin/nginx:ngx_http_regex_exec -u ... usecs               : count     distribution 0 -> 1 : 1159 |**********                              | 2 -> 3 : 4468 |****************************************| 4 -> 7 : 622 |*****                                   | 8 -> 15 : 610 |*****                                   | 16 -> 31 : 209 |*                                       | 32 -> 63 : 91 |                                        |

TLS

如果在CDN前端的w/o边缘终止TLS,那么TLS性能优化的价值是非常可观的。在讨论调优时主要关注服务器端效率。

因此需要决定的件事是使用哪些TLS库:Vanilla OpenSSL、OpenBSD LibreSSL或谷歌的BoringSSL。在选择TLS库的偏好之后,需要适当地构建它,例如,OpenSSL有一堆构建时的启发式,可以根据构建环境进行优化;BoringSSL具有确定性的构建,但遗憾的是它更保守,并且在默认情况下禁用了一些优化。无论如何,在这里优先选择现代的CPU,大多数TLS库可以使用从AES-NI和SSE到ADX和AVX512的所有属性。可以使用带有TLS库的内置性能测试,例如在BoringSSL案例中,内置有bssl speed。

大多数性能不是来自已有的硬件,而是来自将要使用的cipher - suite,因此必须仔细地优化它们。也需要知道这里的变化会影响web服务器的安全性——快的密码套件不一定是好的。如果不确定要使用什么加密设置,Mozilla SSL配置生成器是一个很好的选择。

非对称加密

如果服务处于边缘,那么可能会观察到相当数量的TLS握手,因此CPU占用了大量的非对称密码,这使它成为优化的明显目标。

为了优化服务器端CPU,可以切换到ECDSA certs,其速度通常比RSA快10倍,而且它们的体积要小得多,因此在出现包丢失时能加速握手。但是ECDSA也严重依赖于系统的随机数生成器的质量,所以如果使用的是OpenSSL,那么一定要有足够的熵值(使用BoringSSL,不必担心这一点)。

附注一点,越大并不总是越好,例如使用4096个RSA证书会降低10倍的性能。

$ bssl speed
Did 1517 RSA 2048 signing ... (1507.3 ops/sec)
Did 160 RSA 4096 signing ... (153.4 ops/sec)

更糟糕的是,小的也不一定是好的选择:使用non-common p-224字段与更常见的p-256相比,会降低60%的性。

$ bssl speed
Did 7056 ECDSA P-224 signing ... (6831.1 ops/sec)
Did 17000 ECDSA P-256 signing ... (16885.3 ops/sec)

原则上常用的加密通常是优的加密。

在使用RSA certs运行适当优化的基于opentls的库时,可以在perf top:AVX2-capable看到以下的跟踪信息,但不能使用ADX-capable盒子(例如Haswell)。

 6.42% nginx [.] rsaz_1024_sqr_avx2 1.61% nginx [.] rsaz_1024_mul_avx2

更新的硬件应该使用通用的montgomery模乘算法和ADX codepath。

 7.08% nginx [.] sqrx8x_internal 2.30% nginx [.] mulx4x_internal

对称加密

如果有许多的批量传输,如视频、照片或更通用的文件,需要在分析器的输出中观察对称加密符号。只需要确保CPU具有AES-NI支持,并为AES-GCM ciphers设置服务器端的首选项。根据perf top信息适当的调整硬件。

 8.47% nginx [.] aesni_ctr32_ghash_6x

不只是服务器需要解决加密/解密问题,客户端在缺少可用的CPU时也会面临相同的负担。如果没有硬件加速,这可能非常具有挑战性,因此可以考虑使用一种致力于快速而没有硬件加速的算法,例如chacha20-poly1305。这将减少一些移动客户的TTLB。

BoringSSL是支持chacha20-poly1305的,而对于OpenSSL 1.0.2,可以考虑使用Cloudflare补丁。BoringSSL还支持“相等的首选密码组”,因此可以使用以下配置,让客户端根据其硬件功能来决定使用什么密码(从cloudflare/sslconfig中窃取)。

ssl_ciphers '[ECDHE-ECDSA-AES128-GCM-SHA256|ECDHE-ECDSA-CHACHA20-POLY1305|ECDHE-RSA-AES128-GCM-SHA256|ECDHE-RSA-CHACHA20-POLY1305]:ECDHE+AES128:RSA+AES128:ECDHE+AES256:RSA+AES256:ECDHE+3DES:RSA+3DES'; ssl_prefer_server_ciphers on;

应用程序级别:高标准的

为了分析高标准的优化效果,需要收集RUM数据。在浏览器中,可以使用导航定时api资源定时api收集。收集的主要指标是TTFB和TTV/TTI。拥有便于查询和图形化格式的数据将极大地简化迭代。

压缩

nginx的压缩从mime.types文件开始,它定义了文件扩展和响应MIME类型之间的默认通信。然后需要定义传递给压缩器的类型。如果想要完整的列表,可以使用mime-db来自动生成mime.types并添加.compressible==true到gzip_types。

在启用gzip时,两个方面要注意。

附注,http压缩不仅限于gzip,nginx有第三方ngx_brotli模块,与gzip相比改善后的压缩率高达30%。

至于压缩设置本身,讨论两个单独的用例:静态和动态数据。

在代理中缓存会极大地影响web服务器性能,特别是在延迟方面。nginx代理模块有不同的缓冲开关,它们在每个位置上都是可转换的。可以通过proxy_request_bufferingproxy_buffering对两个方向的缓冲进行单独控制。如果缓冲是启用了内存消耗上限的话,则由client_body_buffer_sizeproxy_buffer设置,达到请求/响应的临界值后,将缓冲到磁盘。对于响应的临界值,可以通过将proxy_max_temp_file_size设置为0来禁用。

常见的缓冲方法有:

无论你选择哪种方法,都不要忘记测试它对TTFB和TTLB的影响。另外,正如前面提到的,缓冲可以影响IO的使用,甚至是后端使用率,因此也要密切关注它。

TLS

现在我们将讨论TLS的高级方面和延迟改进,可以通过正确配置nginx来实现。文章提到的大多数优化都包含在高性能浏览器网络的“优化TLS”部分,并在nginx.conf 2014上让HTTPS更快的讨论。此部分中提到的调优将会影响web服务器的性能和安全性,如果不确定,请参考Mozilla服务器端TLS指南和/或与安全团队协商。

如何验证优化结果。

会话重用

像DBA常说的那样,“快的查询是从来没有做过的。”TLS同样如此,如果你缓存了握手的结果,可以减少一个RTT的延迟。有两种方法缓存握手结果。

附注一点,如果使用session ticket方法,那么值得使用3个键而不是一个键值,例如:

ssl_session_tickets on;
ssl_session_timeout 1h;
ssl_session_ticket_key /run/nginx-ephemeral/nginx_session_ticket_curr;
ssl_session_ticket_key /run/nginx-ephemeral/nginx_session_ticket_prev;
ssl_session_ticket_key /run/nginx-ephemeral/nginx_session_ticket_next;

即便始终使用当前的密钥进行会话加密,也会接受使用之前和之后密钥加密的会话。

OCSP Stapling

需要对OCSP响应作staple,否则:

要staple OCSP响应,可以定期从证书颁发机构获取它,将结果分发给web服务器,并使用ssl_stapling_file指令来调用。

ssl_stapling_file /var/cache/nginx/ocsp/www.der;

TLS记录大小

TLS将数据分解成记录块,在完全接收到它之前,无法对其进行验证和解密。可以从网络堆栈和应用程序的角度来度量这一延迟。

默认的nginx使用16k块,甚至不适合IW10拥塞窗口,因此需要额外的往返。nginx提供了一种通过ssl_buffer_size指令设置记录大小的方法:

静态调优的两个问题。

还有一种替代方法:动态记录大小调整。来自Cloudflare的nginx补丁为动态记录大小提供了支持。初对它进行配置可能是一种痛苦,但一旦配置完成,它就会运行得很好。

TLS 1.3

TLS 1.3的功能确实很不错,但是除非有足够的资源来解决TLS的问题,否则不建议启用它,因为以下原因。

避免Eventloop Stalls

Nginx是一个基于事件循环的web服务器,意味着它只能一次只做一件事。尽管它似乎同时做了所有这些事情,比如在分时复用中,所有的nginx都只是在事件之间快速切换,处理一个接一个。这一切都有效,因为处理每个事件只需几微秒。但如果它开始花费太多时间,例如,因为它需要转到旋转盘上,延迟就会飙升。

如果注意到nginx在ngx_process_events_and_timer函数中花费了很多时间,并且分布是双向的,那么可能会受到eventloop stalls的影响。

# funclatency '/srv/nginx-bazel/sbin/nginx:ngx_process_events_and_timers' -m msecs               : count     distribution 0 -> 1 : 3799 |****************************************| 2 -> 3 : 0 |                                        | 4 -> 7 : 0 |                                        | 8 -> 15 : 0 |                                        | 16 -> 31 : 409 |****                                    | 32 -> 63 : 313 |***                                     | 64 -> 127 : 128 |*                                       |

AIO和Threadpools

因为AIO和Threadpools是eventloop的主要来源,特别是在旋转磁盘上的IO,所以应该优先查看,还可以通过运行文件记录来观察受它影响的程度。

# fileslower 10 Tracing sync read/writes slower than 10 ms
TIME(s)  COMM           TID    D BYTES   LAT(ms) FILENAME 2.642 nginx 69097 R 5242880 12.18 0002121812 4.760 nginx 69754 W 8192 42.08 0002121598 4.760 nginx 69435 W 2852 42.39 0002121845 4.760 nginx 69088 W 2852 41.83 0002121854

为了解决这个问题,nginx支持将IO卸载到一个threadpool(它也支持AIO,但是Unixes的本地AIO很不友好,所以好避免它),基本的设置很简单。

aio threads;
aio_write on;

对于更复杂的情况可以设置自定义[线程池](http://nginx.org/en/docs/ngx_core_module.html # thread_pool)的预留磁盘,如果一个驱动器失效,它不会影响其他的请求。线程池可以极大地减少处于D状态的nginx进程的数量,从而提高延迟和吞吐量。但是它不会完全消除eventloop,因为并不是所有IO操作都被卸载。

日志

记录日志也会花费相当多的时间,因为它在读写磁盘。可以通过运行ext4slower检查是否存在日志,并查找access/error日志引用。

# ext4slower 10 TIME COMM PID T BYTES OFF_KB LAT(ms) FILENAME 06:26:03 nginx 69094 W 163070  634126     18.78 access.log 06:26:08 nginx 69094 W 151     126029     37.35 error.log 06:26:13 nginx 69082 W 153168  638728    159.96 access.log

通过使用access_log指令的缓冲区参数,在编写它们之前,通过在内存中对访问日志进行欺骗,可以解决这个问题。再使用gzip参数,将日志写入磁盘之前压缩日志,从而减少IO压力。

但是要在日志中完全消除IO档位,只能通过syslog编写日志,这样日志将与nginx事件循环完全集成。

打开文件缓存

因为open(2)调用本质上是阻塞的,而web服务器通常是打开/读取/关闭文件,因此缓存打开的文件可能是有益的。通过查看ngx_open_cached_file函数延迟,可以看到便利所在。

# funclatency /srv/nginx-bazel/sbin/nginx:ngx_open_cached_file -u usecs               : count     distribution 0 -> 1 : 10219 |****************************************| 2 -> 3 : 21 |                                        | 4 -> 7 : 3 |                                        | 8 -> 15 : 1 |                                        |

如果看到有太多的开放调用,或者有一些花费太多时间的调用,可以启用文件缓存。

open_file_cache max=10000;
open_file_cache_min_uses 2;
open_file_cache_errors on;

在启用open_file_cache之后,可以通过查看opensnoop来观察所有的缓存遗漏,终决定是否需要调整缓存限制

# opensnoop -n nginx PID    COMM               FD ERR PATH 69435 nginx 311 0 /srv/site/assets/serviceworker.js 69086 nginx 158 0 /srv/site/error/404.html ...

总结

本文描述的所有优化都是基于本地的Web服务器。其中一些提高了拓展性和性能。另外一些与小延迟或更快地将字节传送给客户机是相关的。但在以往的体验中,大量用户可见的性能来自于更高级的优化,这些优化会影响到整个Dropbox 边缘网络的行为,比如ingress/egress流量工程和智能的内部负载平衡。这些问题处于知识的边缘,而行业才刚刚开始接近它们。


本站文章版权归原作者及原出处所有 。内容为作者个人观点, 并不代表本站赞同其观点和对其真实性负责,本站只提供参考并不构成任何投资及应用建议。本站是一个个人学习交流的平台,网站上部分文章为转载,并不用于任何商业目的,我们已经尽可能的对作者和来源进行了通告,但是能力有限或疏忽,造成漏登,请及时联系我们,我们将根据著作权人的要求,立即更正或者删除有关内容。本站拥有对此声明的最终解释权。

回到顶部
嘿,我来帮您!