令伺服器凍結的缺失交換檔案
九月二十四日,我們的一台正式伺服器停止回應。負載平衡器把它標示為不健康,而我們的遠端命令工具也無法連接到它——但沒有任何東西崩潰,也沒有因為用量過多的記憶體而被終止。伺服器是在抖動:由於沒有交換空間,而容器又獲准使用超出機器所擁有的記憶體,Linux 把時間都花在逐出並重新讀取運行所需的檔案上,幾乎沒有任何進展。在每台伺服器上加入一個 2 GB 的交換檔案,便解決了問題。更有意思的教訓則與健康檢查有關。
「卡住」是什麼樣子
- 負載平衡器對該伺服器的健康檢查逾時。
- 透過 AWS Systems Manager 發送的命令停留在「Pending」,從未運行。
- 日誌中沒有記憶體不足的終止紀錄,沒有任何程序死亡。
一星期前,我們已把正式環境遷移到較小的 ARM 伺服器,每台有 2 GB 記憶體。我們的預發佈伺服器——比它更小——在部署期間解壓縮容器映像檔時,也以同樣的方式凍結了。
為什麼沒有記憶體錯誤?
Linux 只有在真的耗盡可回收的記憶體時,才會終止程序。在那之前,它會從頁面快取中回收記憶體——即它保留在 RAM 中的檔案副本,包括每個運行中程序的程式碼。
在我們的伺服器上,各容器的記憶體上限加起來約為 3.3 GB,而機器實際可用的約為 1.8 GB。平時它們不會同時用到上限。但當它們同時使用時,核心會不斷從快取中逐出程序程式碼以騰出空間,然後程式碼一運行,又要從磁碟重新讀回。這種循環——抖動——可以令機器幾乎完全無回應,而技術上從未耗盡記憶體。所以什麼都沒有被終止,也沒有任何東西能夠恢復。
沒有交換空間,核心就沒有地方放置已分配但很少使用的記憶體,因此只能從快取中把它拿走。
修復方法
伺服器的啟動腳本現在會在每次伺服器開機或修復時,建立一個 2 GB 交換檔案,並把 swappiness 設為較低的 10。低 swappiness 意味著核心仍然傾向於把工作記憶體保留在 RAM 中,但很少被觸及的頁面可以移到磁碟上,而不是迫使程序程式碼被逐出快取。我們已在正式環境的兩台伺服器和預發佈環境上即時套用,而且部署流程在每次正式部署時都會重新發佈啟動腳本,因此被替換的伺服器也會取得它。
更令我們擔心的:不健康的伺服器仍然留著
我們的伺服器群組使用的是 EC2 健康檢查——「機器是否在運行?」——而不是負載平衡器健康檢查——「它是否在回應請求?」。因此,在群組看來,一台負載平衡器已經放棄的伺服器仍然是健康的。它永遠不會被替換。正式環境會悄悄地只靠一台伺服器運行,直到有人注意到並重新啟動另一台為止。
把群組改為使用負載平衡器健康檢查聽起來是顯而易見的修復,但對我們來說並不簡單:在部署期間,伺服器會刻意以錯誤回應其健康端點,好讓負載平衡器把它們排空(見我們如何把部署降至零 502)。如果群組依據這個信號來替換伺服器,它會在每次部署的中途替換它們。
因此,目前我們在部署檢查清單中加入了一個步驟:每次部署後,檢查負載平衡器上每個目標的健康狀態,而不只是確認部署工作是否通過。如果發給某台伺服器的命令停留在「Pending」,那就是它在抖動——重新啟動它;它本來就已經脫離輪替。
我們的心得
- 「沒有崩潰」不等於「健康」。 抖動不會產生錯誤,只會造成慢到機器停止回應的程度。
- 小型伺服器需要交換空間。 即使只有一點點,搭配低 swappiness,也能讓核心有地方存放冷門的記憶體。
- 記憶體上限加起來超過機器本身,就是一場賭博。 有時你會輸。知道自己何時下了這個注。
- 知道你的自動擴展信任哪一種健康檢查。 一個只問「虛擬機是否開著?」的健康檢查,不會替換一台已停止運作的伺服器。