凍結我們伺服器的那個缺少的交換檔
9 月 24 日,我們的一台正式環境伺服器停止回應。負載平衡器將它標記為不健康,我們的遠端命令工具也無法連上它——但沒有任何東西當機,也沒有因為記憶體用量過高而被終止。伺服器正在抖動(thrashing):沒有交換空間,而且容器被允許使用的記憶體總和超過機器實際擁有的量,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,也能讓核心有地方放置冷門的記憶體。
- 加總超過機器容量的記憶體上限是一場賭注。 有時你會輸。要知道你什麼時候下了這個賭注。
- 要知道你的自動擴展信任哪一種健康檢查。 只問「VM 是否開機?」的健康檢查,不會替換已經停止運作的伺服器。