让我们服务器卡死的缺失交换文件
9 月 24 日,我们的一台生产服务器停止了响应。负载均衡器将其标记为不健康,我们的远程命令工具也无法连接到它——但没有任何东西崩溃,也没有任何进程因内存占用过高而被杀掉。服务器陷入了颠簸:没有交换空间,而且容器被允许使用的内存超过了机器实际拥有的内存,Linux 把时间都花在驱逐并重新读取它运行所需的文件上,几乎没有取得任何进展。在每台服务器上添加一个 2 GB 的交换文件解决了问题。更有意思的教训则与健康检查有关。
「卡死」是什么样子
- 负载均衡器的健康检查在那台服务器上超时。
- 通过 AWS Systems Manager 发送的命令一直处于「Pending」状态,从未执行。
- 日志中没有内存溢出被杀的记录。没有任何进程退出。
一周前,我们将生产环境迁移到了更小的 ARM 服务器,每台有 2 GB 内存。我们的预发布服务器——比它还要小——在部署期间解压容器镜像时,也以同样的方式卡死了。
为什么没有内存错误?
Linux 只有在真正耗尽可回收的内存时才会杀死进程。在那之前,它会从页缓存中回收内存——也就是它保存在内存中的文件副本,包括每个正在运行的程序的代码。
在我们的服务器上,各容器的内存限制加起来约为 3.3 GB,而机器可用内存约为 1.8 GB。通常它们不会同时用满自己的限额。但一旦同时用满,内核就会不断从缓存中驱逐程序代码来腾出空间,而程序代码一旦再次运行,又要从磁盘读回来。这种循环——颠簸——可以让一台机器几乎完全失去响应,同时从技术上讲它并没有耗尽内存。因此没有任何东西被杀掉,也没有任何东西恢复。
没有交换空间时,内核无处存放已分配但很少使用的内存,只能从缓存中拿走。
修复
服务器的引导脚本现在会在每次服务器启动或修复时创建一个 2 GB 的交换文件,并将 swappiness 设置为较低的值(10)。较低的 swappiness 意味着内核仍然优先将工作内存保留在 RAM 中,但很少访问的页面可以移到磁盘上,而不是把程序代码从缓存中挤出。我们在两台生产服务器和预发布服务器上实时应用了这一设置,部署流水线也会在每次生产部署时重新发布引导脚本,因此被替换的服务器同样会获得它。
更让我们担心的部分:不健康的服务器一直留着
我们的服务器组使用的是 EC2 健康检查——「机器是否在运行?」——而不是负载均衡器健康检查——「它是否在响应请求?」。因此,对于服务器组来说,一台已被负载均衡器放弃的服务器仍然是健康的。它永远不会被替换。生产环境会悄悄地只依赖一台服务器运行,直到有人注意到并重启另一台。
把服务器组切换为负载均衡器健康检查听起来是显而易见的修复,但对我们来说并不简单:部署期间,服务器会刻意让它们的健康端点返回错误,以便负载均衡器将其排空(参见我们如何将部署降到零 502)。如果服务器组根据这个信号替换服务器,就会在每次部署的过程中替换它们。
因此,目前我们在部署检查清单中增加了一步:每次部署之后,不仅要看部署任务是否通过,还要在负载均衡器上检查每个目标的健康状况。如果发往某台服务器的命令一直处于「Pending」,那就是它在颠簸——重启它;反正它已经不在轮转之中了。
我们的收获
- 「没有崩溃」不等于「健康」。 颠簸不会产生错误,只会造成慢到机器停止响应的程度。
- 小服务器需要交换空间。 即使只有一点,配合较低的 swappiness,也能给内核一个存放冷内存的地方。
- 各内存限制加起来超过机器容量,就是一场赌博。 有时你会输。要清楚自己什么时候在下注。
- 要知道你的自动扩缩容信任哪种健康检查。 只问「虚拟机是否开机」的健康检查,不会替换已经停止工作的服务器。