威利·塔罗(Willy Tarreau) | 2021年4月8日
在单个Arm实例上,软件负载平衡器首次超过200万RPS。
几周前,当我处理与线程锁定争用有关的HAProxy问题时,我发现自己在实验室中装有8核,16线程Intel Xeon W2145处理器的服务器上运行了一些测试。尽管我的目的不是对代理进行基准测试,但我观察到HAProxy每秒可达到103万个HTTP请求。我突然回忆起我曾经告诉周围的人的所有时间:“那天我们突破了每秒百万个请求的障碍,我会写这篇文章。” 因此,我必须信守诺言!
我想看看它将如何扩展到更多的内核上。我可以访问一些新的基于Arm的AWS Graviton2实例,这些实例最多提供64个内核。为了让您对它们的设计有所了解,每个内核都使用自己的L2缓存,并且所有内核共享一个L3缓存。如果您lscpu在以下其中一台计算机上运行,则可以自己看到,这将显示内核数以及如何共享缓存:
$ lscpu -e
CPU NODE SOCKET CORE L1d:L1i:L2:L3 ONLINE
0 0 0 0 0:0:0:0 yes
1 0 0 1 1:1:1:0 yes
2 0 0 2 2:2:2:0 yes
3 0 0 3 3:3:3:0 yes
...
60 0 0 60 60:60:60:0 yes
61 0 0 61 61:61:61:0 yes
62 0 0 62 62:62:62:0 yes
63 0 0 63 63:63:63:0 yes
查看原始他们给我留下了深刻的印象,特别是当我们受到
@AGSaidi的鼓励时,改用了新的Arm Large System Extensions(LSE)原子指令,对此我们已经有了一些可用的代码,但是从未进行过如此大规模的测试,并且完全释放了这些机器的真正力量。因此,这似乎是一个绝佳的机会,可以将所有内容结合起来并将我们的HAProxy基准推向更高的水平!如果要使用gcc 9.3.0编译HAProxy,请包括该标志-march=armv8.1-a以启用LSE原子指令。在gcc版本10.2.0中,默认情况下启用LSE。
概要
是的,您已经阅读了此博客文章的标题。在基于Arm的AWS Graviton2实例上进行测试时,HAProxy 2.3版每秒可达到204万个请求!
仍在开发中的HAProxy 2.4超过了它,每秒达到2.07至208万个请求。
如果您对测试的方法,系统的调整方式以及所汲取的教训感到好奇,请继续阅读。
与NGINX团队的一些成员合作启动了一个名为dpbench的新项目,该项目捕获了一些基准测试代理的最佳实践。它为想要进行类似实验而又不被某些常见误解所困扰的任何人提供指导。请注意,该项目欢迎可以为各种环境提供更多调整/报告脚本,配置,结果和提示的贡献者。
方法
在这些基准测试中,我们正在测试两件事:
- 每秒HTTP请求
- 端到端请求延迟达到或超过99.99个百分点
尾部等待时间(影响少量请求的等待时间)是非常重要的,不可忽视,特别是在服务网格中,来自用户的一个请求可能会在后端导致许多微服务请求。在这些环境中,等待时间尖峰会产生放大因子。
例如,如果用户的请求转换为20个后端请求,则较大的延迟仅影响10,000个后端请求中的1个,将影响500个用户请求中的1个。如果该用户在访问期间提出了50个请求,则该请求更有可能体现出来。如果延迟增加很少,或者非常罕见,那么问题就不会那么严重,但是当它既频繁又频繁时,这就是一个问题。因此,重要的是我们在基准测试期间对此进行测量。
实例大小
我们使用了AWS的c6gn.16xlarge虚拟机实例,这些实例是c6gn系列的64核机器,可访问100 Gbps的网络带宽。它们使用Graviton2处理器进行了计算优化,专为需要高网络带宽的应用而构建。此实例大小用于客户端,代理和服务器。
我首先以16核开始,然后以32核开始,以为这样就足够了。从客户端到服务器的直接测试(中间没有HAProxy)使我感到失望,每秒只有800k数据包,或者比常规的c6g数据包少一点。然后,我决定使用完整的内核:专用主机上的64个内核。这次我得到了每秒415万个数据包,并认为这已经足够了,因为我实际上只需要200万个数据就可以每秒实现100万个请求,占一个请求数据包和一个响应数据包。
所有计算机均安装了Ubuntu 20.04,这是默认情况下建议的安装之一。
IRQ亲和力
我花了一些时间弄清楚如何完全稳定平台,因为在进行虚拟化时,仍然有32个中断(又名IRQ)分配给网络队列,传递给32个内核!这可能解释了内核数量较少时性能较低的情况。对于某些读者来说,32个队列听起来可能很多,但我们这里所说的是100 Gbps网络和每秒数百万个数据包。在这样的数据包速率下,您绝对不希望任何用户级进程在测试过程中碰到这些核心之一,这就是发生的事情,因为这些核心或多或少地随机分配给了这些进程。
将中断移至32个上层内核后,剩下的32个下层内核仍未使用,从而大大简化了设置。但是,我想知道网络堆栈是否仅支持在16个内核上运行,每个内核有2个中断,这将为HAProxy和系统的其余部分留下48个内核。我尝试了一下,发现它是最佳情况:网络在每个方向上以每秒约415万个数据包的速度饱和,而网络专用的核心通常以100%的速度出现,表明中断被迫排队(ksoftirqd)。
在此级别上,无论何时系统上发生任何事情(例如,snapd守护程序每60秒唤醒一次,crond每60秒检查一次其文件),它都必须与网络或HAProxy共享一个核心,这使得可见一毫秒或更长的延迟峰值。我更喜欢停止这些服务,以限制测试期间在后台发生随机事件的风险:查看原始
$ sudo systemctl stop irqbalance
$ sudo systemctl stop snapd
$ sudo systemctl stop cron
这绝对是寄生活动的一个很好的例子,证明为剩余活动留出了一些核心是合理的。因此,我认为我将保留2个内核,并在那里绑定每个与负载无关的活动。
简而言之,测试是在绑定到所有计算机上的内核48-63的网络中断(总共16个内核),在内核2-47上启用HAProxy /客户端/服务器(总共46个内核)以及剩余0-1个内核可用的网络中断下运行的用于其他所有内容(sshd以及监视工具)。
使用前面提到的dpbench项目中的set-irq.sh工具,使用此命令将IRQ绑定到16个上层内核:
$ sudo ~/dpbench/scripts/set-irq.sh ens5 16
查看原始
网络拓扑结构
整个设置保持相当简单,看起来像这样:
服务器软件是httpterm,在46核上启动。该ulimit命令增加了可用的打开文件描述符(即用于连接的插槽)的数量:
$ ulimit -n 100000
$ for i in {2..47}; do taskset -c 2-47 httpterm -D -L :8000;done
查看原始
HAProxy是2.3版:
$ ./haproxy -v
HA-Proxy version 2.3.7 2021/03/16 - https://haproxy.org/
Status: stable branch - will stop receiving fixes around Q1 2022.
Known bugs: http://www.haproxy.org/bugs/bugs-2.3.7.html
Running on: Linux 5.4.0-1038-aws #40-Ubuntu SMP Fri Feb 5 23:53:34 UTC 2021 aarch64
查看原始
其配置文件test.cfg基于dpbench项目的基本转发器示例:
查看原始
defaults
mode http
timeout client 60s
timeout server 60s
timeout connect 1s
listen px
bind :8000
balance random
server s1 172.31.36.194:8000
我们使用以下命令运行它:
$ ulimit -n 100000
$ taskset -c 2-47 haproxy -D -f test.cfg
查看原始
客户端软件(在图中显示为Runner)是具有46个线程的h1load,它提供实时统计信息,使您可以验证一切是否按预期进行。并发连接数设置为1150,因为这是46的整数倍,它与链中的线程/进程数匹配。这些测试的HTTP响应被配置为零字节主体。可以通过更改URL来配置,以指定通过传递/?s = <size>所需的响应大小。
$ ulimit -n 100000
$ taskset -c 2-47 h1load -e -ll -P -t 46 -s 30 -d 120 -c 1150 http://172.31.37.79:8000/ | tee cli-run.dat
查看原始
第二个客户充当该实验的控件。如图中的Ref所示,它在专用内核的同一客户端计算机上以每秒1000个请求的极低负载运行。它的目的是测量客户端和服务器之间的直接通信,测量整个链上负载对网络堆栈的影响,以更准确地测量遍历负载平衡器的成本。它不会测量客户端软件本身所经历的部分,但是它已经确定了最低值。查看原始测试以30秒的加速运行了两分钟。加速对于纯粹的性能并不重要,但是在收集定时信息时是必不可少的,因为所有计算机上的操作系统都花费大量时间来增长文件描述符和套接字所使用的结构。由于其自身的工作,很容易看到客户端上的等待时间非常长。加速方法可以解决此问题,并减少客户端上不良测量的次数。使用阶梯也可以检测到一些意外的边界,通常是被遗忘的文件描述符限制(即您忘记运行ulimit),这会导致突然增加。
$ ulimit -n 100000
$ taskset -c 1 h1load -e -ll -P -t 1 -s 30 -d 120 -R 1000 -c 128 http://172.31.36.194:8000/ | tee cli-ref.dat
测试以30秒的加速运行了两分钟。加速对于纯粹的性能并不重要,但是在收集定时信息时是必不可少的,因为所有计算机上的操作系统都花费大量时间来增长文件描述符和套接字所使用的结构。由于其自身的工作,很容易看到客户端上的等待时间非常长。加速方法可以解决此问题,并减少客户端上不良测量的次数。使用阶梯也可以检测到一些意外的边界,通常是被遗忘的文件描述符限制(即您忘记运行ulimit),这会导致突然增加。
dpbench项目中也记录了每台计算机上的CPU使用率和网络流量。
结果
尽管每秒进行一次测量,但下面绘制的结果显示的曲线看起来非常干净且非常稳定。绑定了IRQ并在所有计算机上停止了以上守护程序之后,几乎所有测试都像此测试一样干净。
HAProxy 2.3
性能几乎保持不变,为每秒2.04至205万个请求。从客户端到服务器的直接通信显示160毫秒的响应时间,而添加HAProxy则将其增加到560。因此,负载平衡器的插入以及添加额外网络跃点的效果平均最多增加了400毫秒。超过半毫秒。
百分位图显示99.9%的延迟为5毫秒,这表明千分之一的请求所花费的时间超过5毫秒。但是,它持续增长,达到百万分之1的请求的68毫秒。事实是,计算机已完全饱和,因此,我们正在测量链中每个阶段(网络缓冲区和HAProxy)的排队效果。
HAProxy 2.4
这些结果适用于HAProxy 2.3版。随着时间的推移,2.4开发分支已经获得了很多改进,对其运行相同的测试甚至可以得到更好的结果。HAProxy 2.4每秒可在2.07至208万个请求之间振荡。从客户端到服务器的直接通信显示,在添加HAProxy时响应时间为163微秒,而额外的网络跃点将其增加到552。因此,平均起来,总计为389微秒,或略大于三分之一毫秒。
此处的最大延迟达到1.87毫秒,其中直接通信已经经历了0.68毫秒。因此,HAProxy的相加时间永远不会超过1.19毫秒。
在下面显示的CPU使用率报告中可见,HAProxy不再饱和。它平均使用46个分配的CPU中的42个。但是,网络层在其下面可见,并且ksoftirq线程通常在100%CPU上弹出。
top - 05:43:31 up 29 min, 4 users, load average: 16.62, 12.76, 8.62
Tasks: 639 total, 2 running, 637 sleeping, 0 stopped, 0 zombie
%Cpu(s): 44.5 us, 33.2 sy, 0.0 ni, 22.2 id, 0.0 wa, 0.0 hi, 0.1 si, 0.0 st
MiB Mem : 126544.6 total, 125538.5 free, 568.9 used, 437.1 buff/cache
MiB Swap: 0.0 total, 0.0 free, 0.0 used. 124966.6 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
2044 ubuntu 20 0 3356916 27312 6260 R 4194 0.0 20:09.40 haproxy+
336 root 20 0 0 0 0 S 8.9 0.0 0:24.27 ksoftir+
355 root 20 0 0 0 0 S 5.9 0.0 0:23.12 ksoftir+
361 root 20 0 0 0 0 S 5.9 0.0 0:19.64 ksoftir+
409 root 20 0 0 0 0 S 5.9 0.0 0:23.90 ksoftir+
373 root 20 0 0 0 0 S 5.0 0.0 0:22.96 ksoftir+
379 root 20 0 0 0 0 S 5.0 0.0 0:18.61 ksoftir+
391 root 20 0 0 0 0 S 5.0 0.0 0:22.93 ksoftir+
415 root 20 0 0 0 0 S 5.0 0.0 0:17.42 ksoftir+
385 root 20 0 0 0 0 S 4.0 0.0 0:18.74 ksoftir+
397 root 20 0 0 0 0 S 4.0 0.0 0:15.33 ksoftir+
432 root 20 0 0 0 0 S 4.0 0.0 0:15.12 ksoftir+
450 root 20 0 0 0 0 S 4.0 0.0 0:17.04 ksoftir+
367 root 20 0 0 0 0 S 3.0 0.0 0:16.48 ksoftir+
403 root 20 0 0 0 0 S 3.0 0.0 0:17.66 ksoftir+
444 root 20 0 0 0 0 S 3.0 0.0 0:12.70 ksoftir+
查看原始
这是资源的理想组合,HAProxy完全具有所需的资源,而网络堆栈也具有完全所需的资源。不可能超出这个范围,因为我们在两个方向上都达到了每秒415万个数据包的限制,这是我们之前作为网络接口的硬性限制所测得的。
TLS免费
一些读者可能想知道我们是否无法在HAProxy中使用这些多余的CPU周期免费进行TLS终止。让我们尝试一下。
结果表明,通过TLS,我们每秒只有1.99到201万个请求。162个直接响应的平均响应时间为575微秒,因此基于TLS的HAProxy最多可将链发增加413微秒。对于这些测试,我使用了RSA 2048位证书,并且连接是通过TLSv1.3和TLS_AES_256_GCM_SHA384建立的。
延迟百分位数显示,对于直接最大数为1.2的情况,每10万个请求中有不到1个请求高于1.6毫秒,并且从未超过6.25毫秒。终止TLS时,HAProxy的添加时间不会超过5毫秒。
现在我们已经饱和了CPU:
top - 06:42:08 up 1:28, 4 users, load average: 40.05, 20.99, 12.57
Tasks: 639 total, 2 running, 637 sleeping, 0 stopped, 0 zombie
%Cpu(s): 54.9 us, 31.3 sy, 0.0 ni, 13.7 id, 0.0 wa, 0.0 hi, 0.1 si, 0.0 st
MiB Mem : 126544.6 total, 125279.7 free, 626.9 used, 638.0 buff/cache
MiB Swap: 0.0 total, 0.0 free, 0.0 used. 124893.0 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
5433 ubuntu 20 0 3357232 39948 6428 R 4597 0.0 260:36.56 haproxy
373 root 20 0 0 0 0 S 8.9 0.0 1:32.24 ksoftir+
391 root 20 0 0 0 0 S 6.9 0.0 1:29.67 ksoftir+
409 root 20 0 0 0 0 S 5.9 0.0 1:26.27 ksoftir+
379 root 20 0 0 0 0 S 5.0 0.0 1:34.07 ksoftir+
415 root 20 0 0 0 0 S 5.0 0.0 1:20.57 ksoftir+
438 root 20 0 0 0 0 S 5.0 0.0 1:16.82 ksoftir+
355 root 20 0 0 0 0 S 4.0 0.0 1:27.25 ksoftir+
361 root 20 0 0 0 0 S 4.0 0.0 1:24.02 ksoftir+
385 root 20 0 0 0 0 S 4.0 0.0 1:19.53 ksoftir+
432 root 20 0 0 0 0 S 4.0 0.0 1:08.21 ksoftir+
450 root 20 0 0 0 0 S 4.0 0.0 1:13.43 ksoftir+
336 root 20 0 0 0 0 S 3.0 0.0 1:31.32 ksoftir+
367 root 20 0 0 0 0 S 3.0 0.0 1:20.86 ksoftir+
403 root 20 0 0 0 0 S 3.0 0.0 1:12.48 ksoftir+
397 root 20 0 0 0 0 S 2.0 0.0 1:10.06 ksoftir+
查看原始
分析
如果网络有限,为什么还要分配这么多内核?只是为了证明这是可能的。另外,某些用户可能希望在HAProxy中使用大量规则,多线程Lua模块(仅适用于2.4)或大量TLS流量来运行繁重的处理。另外,弄清楚网络部分处理给定负载需要多少个内核非常有用,因为一旦这些内核被认为是“保留”的,就可以轻松地将剩余的内核分配给各种任务(这是为完成这些任务而完成的)。负载生成器)。
但是,坦率地说,没有人需要在一台计算机上达到如此疯狂的性能水平。可以将这种设置大大缩减为非常便宜的机器,并且仍然可以获得出色的响应时间。
对于低得多的负载(例如100个并发连接,尽管对于许多用户来说仍然很高),即使是一个免费的t4g.micro实例(带有2个vCPU和1 GB的RAM),对于非TLS流量,每秒可处理102,000个请求,对于75,000个实例,则可以达到75,000个。要求TLS。虽然,这种适度的计算机实例在并发连接数为1,000时下降到大约56,000 TLS和45,000 TLS。
但是,需要在HAProxy,网络堆栈和工具之间共享2个vCPU,这会导致可见的延迟峰值比每秒25,000个TLS请求高出99.9999%。但是我们已经在谈论一个免费实例和高负载了!对于更低的负载,还可以在您没有注意到它的任何现有计算机上免费安装HAProxy!
结论
使用基于Arm的AWS Graviton2实例,可以同时具有计算性能和网络性能。Graviton2机器拥有64个真正的内核,并且具有令人印象深刻的可扩展性,绝对给力。他们对LSE原子指令的支持已完全解决了困扰armv8.1-a之前的CPU的可伸缩性问题。我不明白为什么我们仍然看不到带有这种CPU的主板。它们对开发人员而言非常棒,它们提供对所有64个内核的完全统一的内存访问,并且它们已经帮助我们利用了HAProxy 2.4上的一些争用点。即使只是缩小比例的16核和较低频率的核,也将是很棒的……请🙂
同样清晰可见,还有一些余量,但是我们已经达到了当前虚拟化层每秒400万个数据包的限制。但是,在那里达到100 Gbps是微不足道的。我可以验证HAProxy在请求量仅为30 kB的情况下达到了HTTP负载的92 Gbps!我真的很想知道这样的机器可以在裸机上做什么,但是很可能我们正在观察当今最便宜的位移动器之一!如果亚马逊已经将这种硬件用于自己的基础设施,我不会感到惊讶。
在HAProxy方面,当前的2.4开发分支最近获得了许多可伸缩性优化,从而进一步降低了峰值延迟并提高了请求之间的公平性。我们竭尽所能地在运行时测量每个功能的CPU时间和每个功能的延迟,以发现违规者并将精力集中在代码的某些区域。Runtime APIshow profiling displays为我们命令这些指标:
> show profiling
Tasks activity:
function calls cpu_tot cpu_avg lat_tot lat_avg
h1_io_cb 3667960 9.089s 2.477us 12.88s 3.510us
si_cs_io_cb 3487273 4.971s 1.425us 12.44m 214.0us
process_stream 2330329 10.20s 4.378us 21.52m 554.1us
ssl_sock_io_cb 2134538 2.612m 73.43us 6.315h 10.65ms
h1_timeout_task 1553472 - - 6.881m 265.8us
accept_queue_process 320615 13.72m 2.568ms 8.094m 1.515ms
task_run_applet 58 3.897ms 67.19us 3.658ms 63.07us
session_expire_embryonic 1 - - 294.0ns 294.0ns
查看原始
该命令是长期测试工作的一部分,该工作使开发人员能够不断地验证自己是否不会降低代码库中的任何内容。了解什么可能是造成现场处理欠佳的原因也是非常有用的。这些机器上经常使用“性能”工具,并且确定了一些小的争用点,这些争用点一旦得到解决,便可以使我们获得更多的百分点。
我们正处在一个时代,您不仅可以免费获得世界上最快的负载均衡器,而且还可以在廉价的计算机实例上运行它并仍然获得惊人的结果,其运行成本也几乎降低到了零。