更新我们的服务所依赖的底层系统(可以是操作系统、虚拟机、核心库、数据库或其他组件)是我们系统生命周期的常规部分。在这篇文章中,我们将讨论在更新到更新的Erlang OTP版本时,我们是如何发现性能下降的,我们采取了哪些步骤来调查它,以及我们是如何解决手头的具体问题的。
对于我们在Erlang上运行的大部分核心技术,几年前我们就确定了在倒数第二个OTP版本上运行的策略。这背后的原因是为了避免受到最新版本中可能仍然存在的错误或性能问题的影响。这个想法也是为了等新功能稳定下来后再急于采用它们。这不是板上钉钉的,但这是我们努力尊重的规则。
遵循这一工作流程,几个月前,随着OTP23的发布,我们首次尝试从OTP21迁移到OTP22。但令我们沮丧的是,这并不像我们希望的那样顺利。
作为发布过程的一部分,我们将新版本与之前的版本并行运行,并寻找任何可能影响我们的异常或性能下降。在这种情况下,很明显,我们在OTP22上运行时遇到了一些问题,因为该服务的生产吞吐量较低。显著降低:我们的系统运行速度是以前的0.75倍。
切换到OTP22(黄色)时的吞吐量与我们运行OTP21(蓝色)和控制组(紫色)的生产系统上的吞吐量相比,我们尝试的第一件事是查看发行说明。有没有什么变化可能会对我们的系统产生这种意想不到的影响?我们正在寻找我们认为可以产生这种重大影响的东西,比如…。
与文字术语相关的更改(我们有大量数据在启动时在PERSISTEN_TERM中进行预处理和备份)。
然后,我们查看了系统的实际运行情况,很明显,它在系统CPU上花费的时间比以前多得多:
[ec2-user@ip-172-26-77-44~]$mpstat-P all 5Linux 4.14.177-107.254.amzn1.x86_64(ip-172-26-77-44)06/08/2020_x86_64_(16 CPU)07:44:51 PM CPU%usr%nIC%sys%iowait%irq%soft%stepe%gues%idle07:44:56 PM all 62.28 0.00 16.24 0.00 0.00 0.61。.4607:44:56 PM 0 21.22 0.00 4.69 0.00 0.00 0.00 0.61 0.00 73.4707:44:56 PM 1 62.37 0.00 17.71 0.00 0.00 4.63 0.40 0.00 14.8907:44:56 PM 2 65.09 0.00 17.25 0.00 0.00 0.00 17.2507:44:56 PM 3 65.58 0.00 16.50 0.00 0。.00 0.00 0.41 0.00 17.5207:44:56 PM 4 66.94 0.00 15.70 0.00 0.00 0.00 16.9407:44:56 PM 5 66.67 0.00 16.36 0.00 0.00 0.00 16.5607:44:56 PM 6 65.79 0.00 16.70 0.00 0.00 0.00 17.1007:44:56 PM 6 65.79 0.00 16.70 0.00 0.00 0.00 0.40 0.00 17.1007:44:56 PM 6 65.79 0.00 16.70 0.00 0.00 0.00 17.1007:44:56 PM 7 68。.78 0.00 14.69 0.00 0.00 0.00 16.3307:44:56 PM 8 64.62 0.00 15.95 0.00 0.00 0.00 0.61 0.00 18.8107:44:56 PM 9 67.69 0.00 14.93 0.00 0.00 0.00 0.41 0.00 16.9707:44:56 PM 10 53.54 0.00 24.65 0.00 0.00 4.85 0.40 0。.00 16.5707:44:56 PM 11 61.89 0.00 19.47 0.00 0.00 0.00 0.41 0.00 18.2407:44:56 PM 12 62.68 0.00 19.38 0.00 0.00 0.00 0.62 0.00 17.3207:44:56 PM 13 64.21 0.00 17.79 0.00 0.00 0.00 0.61 0.00 17.3807:44:56 PM 14 74.49 0.00 10.93 0。14.1707:44:56 PM 15 64.24 0.00 17.67 0.00 0.00 0.00 0.42 0.00 17.67
从%sys列可以清楚地看出,超过15%的CPU时间在所有内核的内核任务中(内核0与我们在这里的讨论无关,因为我们的Erlang系统不使用它;它是为其他短暂的、时间敏感的任务保留的)。该系统的空闲时间百分比也很高,因此很明显它在等待什么。
由于内核任务正在运行,因此我们比较了两个系统之间的vmstat输出。在OTP 22…上,中断和上下文切换的数量几乎翻了一番。
[ec2-user@ip-172-26-77-44~]$vmstat 5pros-内存-交换-io-系统-cpu-r b swpd空闲缓冲区缓存si so bi bo in cs us sy id wa st24 0 0 24724364 63472 1242540 0 0 0 12 57 1466 981 59 9 31 0 119 0。62547 74 12 13 0 015 0 0 24930996 63488 1246924 0 0 0 359 248553 61832 74 12 13 0 015 0 0 24840760 63496 1247752 0 0 0 1405 250937 48011 74 12 13 013 0 0 24858236 63504 1249876 0 0 664 247081 48183 75 12 12 0 017 0 0 24855240 63512 1251484 0 0 35 252037 47864 12 12 0019 0 24911248 635。
[ec2-user@ip-172-26-77-50~]$vmstat 5pros-内存-交换-io-系统-cpu-r b swpd空闲缓冲区缓存si so bi bo in cs us sy id wa st16 0 0 22068060 63404 1410956 0 0 0 9 54 1218513 70 8 21 0217 0。27720 84 7 9 0 015 0 0 22120716 63412 1414376 0 0 0 267 119251 26972 84 7 10 0 0 6 0 0 22107168 63420 1415960 0 0 0602 117711 25838 85 7 8 017 0 0 22124960 63428 1418760 0 0 1528 121784 26970 83 7 10 0 012 0 0 22149468 63436 1420380 0 0 0 18 124217 30018 82 7 11 0 015 0 22135184 63444。
接下来,我们来看一下网络活动:套接字写入/读取模式会不会有所不同?即使发送和接收的数据相同,我们使用的IP数据包是否比以前更多?这些中断是网络中断吗?所有这些问题的答案都是否定的。问题不在IO端。
相反,正如系统活动报告告诉我们的那样,我们有大量的页面错误:
$sudo sar-B 10 10Linux 4.14.177-107.254.amzn1.x86_64(ip-172-26-68-64)08/03/2020_x86_64_(16cpu)09:21:55 PM pgpgin/s pgpgout/s故障/s主要/s pgfree/s pgscank/s pgscand/s pgstead/s%vmeff09:22:05 PM 0.00 243.64 140891.62 0.00 128650.61 0.00。00 103.24 135143.62 0.00 129283.20 0.00 0.00 0.00。
请注意,这些都是较小的页面错误。我们没有在磁盘上翻来覆去,我们的记忆力也不差。
有了这个新的信息,我们回去查看变更日志和发布说明,这一次,我们对我们正在寻找的东西有了更多的了解。我们正在寻找可能导致我们看到的分配器设置/工作方式的任何变化。本文最下面的“内存优化”一节引起了我们的注意,我们认为我们有了赢家。与释放内存有关的更改似乎很有可能与我们不断增加的分页错误有关。
在这一点上,我们匆忙制作了一个快速而肮脏的补丁来禁用这种新行为(或者我们是这么想的)。在深入研究如何以更文明的方式解决这个问题之前,我们重新进行了测试,以确认这就是原因所在。更让我们沮丧的是,这并没有对性能下降造成影响,所以我们认为这个补丁可能不是我们的问题,并继续在其他地方挖掘。
在早期对此进行调查时,我们遇到的一个问题是,我们有好的、快速的方法来基于给定的(固定的)OTP版本生成系统的新版本。但是,使用不同的OTP版本生成版本是一个痛苦而乏味的过程。这仅仅是我们的开发基础设施没有优化过的东西。我们不得不更改几个配置和打包文件,以及重新部署我们的CI管道。
到目前为止,这还不是问题,因为切换OTP版本通常是一年一次的事情,但现在它放慢了我们的脚步,因为我们想尝试不同的OTP版本(例如,我们尝试了OTP22系列的其他次要版本)以及光束补丁。因此,我们继续解决了这个问题,使我们能够轻松快速地构建和测试部署具有不同底层OTP版本的发行版,甚至是并行的。
有了这个新工具,我们采用了强力方法:将OTP21和OTP23之间的更改一分为二,直到我们找到引入问题的地方。调查速度之快令人惊讶,一天下午后,我们看到了看起来像是确凿证据的东西。令人惊讶的是,它让我们回到了OTP的公关#2046。这有点令人困惑;我们不是已经排除了这一点吗?
查看我们在较新的光束上执行的与内存相关的系统调用,我们看到:
$sudo strace-f-e TRACE=Memory-c-p$PID%时间秒数使用/调用错误sys-call。
与OTP21上的相同跟踪相比,不同之处在于OTP21上没有mise调用,而内存分配的数量大致相同。
MAdise调用的快照,仅放大一个调度程序。在这一点上,我们非常确定#2046与我们的问题有关,所以我们做了我们在第一次怀疑它时就应该做的事情:我们试图理解补丁在做什么,mAdise的实际含义是什么。
此PR允许操作系统回收与池运营商中的空闲块相关的物理内存,从而降低长期笨拙分配的影响。一小块分配的区块仍然可以让一个巨大的运营商存活下来,但运营商的未使用部分现在将可供操作系统使用。
这里我们不会解释内存分配是如何在BEAM上构造的。有关这一点,请参考allc框架文档和运营商迁移文档。在那里,您可以找到有关合用运营商的详细说明。
Mise在这里的使用方式是向操作系统提示给定内存块的使用情况。特别是,BEAM(从OTP22开始)调用该函数来告诉操作系统在不久的将来可能不需要某些内存块。
既然我们了解了我们正在查看的内容,那么我们的性能测量就有意义了。我们的系统过于频繁地将运营商放回池中,然后在不久之后再次挑选他们。当运营商回到池中时,OTP22上的新行为应该是在向操作系统暗示,预计不会很快再次需要那里的空闲内存。
问题是,为什么使用mise(MADV_FREE)会导致页面错误?这应该是对操作系统的一个提示,所以在内存紧张的情况下,它应该选择取消该内存与其他内存的映射。但我们从未承受过记忆压力。像往常一样,Lukas非常有帮助,建议我们检查MADV_FREE在我们的系统上是否确实可用。我们在AWS上运行,我们仍然在使用不支持MADV_FREE的Amazon Linux的旧版本。因此,BEAM退回到使用MADV_DONTNEED,在这种情况下,操作系统非常渴望从我们的进程中取消这些内存块的映射,即使在其他地方不需要内存的情况下也是如此。
如果您想检查您的环境是否支持MADV_FREE,您可以在操作系统提供的.h头文件上对其进行grep。或者,以一种可能更文明的方式,您可以编译并运行测试程序,如下所示:
#include<;sys/mman.h>;#clude<;stdio.h>;int main(){#ifdef MADV_free printf(";恭喜,MADV_free标志可用\n";);#Else printf(";:(\n";);#endif return 0;}
为什么我们会有这样一种反复的模式,不断地离开和挑选承运商?这是一个有趣的问题,实际上也是问题的根源。这意味着我们的分配设置不合适。设置几年来没有改变,而系统在这段时间里有了很大的发展和增长。当然,在OTP21上运行时也存在这种对运营商池的误用。只是那里的税赋较低。
解决方案是去改进它们,这件事说起来容易做起来难:)(如果有疑问,一个很好的起点是使用erts_alloc_config获取初始配置并从那里迭代)。
我们进行了几次迭代来调整我们的设置,并对内存处理和性能进行了相当大的改进,但无法完全消除开销,因为运营商仍然经常被抛弃。这是我们需要进一步调查的事情。航空公司不应该以这样的速度被推入和推出泳池。作为允许我们更新到OTP22的临时解决办法,我们完全禁用了运营商迁移(这是通过将+M<;S&>系统标志设置为0,将<;S&>替换为您想要调整的分配器来实现的)。
正如前面提到的,在开始调查时,我们确实认为mise更改是导致问题的一个可能原因。那么,当我们最初对此持怀疑态度时,为什么要排除这一可能性呢?
事实证明,上面讨论的禁用它的快速而肮脏的小补丁是错误的,它并没有真正禁用它。这是一起令人遗憾的事件,暴露了我们方面的两个错误,都是因为匆忙推进调查,而没有花时间询问事情为什么会发展到现在的地步:
在没有完全理解手头的代码的情况下,我们匆忙做了一个快速补丁来测试这个假设。我们禁用了一条调用mise的代码路径,但错过了另一条代码路径。
当假设失败时,我们再次匆忙进行另一项工作。我们没有花时间分析为什么我们的理论是错误的。我们没有质疑这个实验的有效性,这会让我们很早就意识到我们的补丁是错误的。
这里有一个更广泛的教训:当调查任何复杂的问题时,很容易被要考虑的活动部件的数量所淹没,但关键是不要仓促得出结论。你应该花时间去了解手头的问题,以及每一步是如何让你更接近解决问题的。知道什么不是问题的原因是很好的进步!