警示只發一次,而不是每台伺服器各發一次
我們的一項營運警示「低點數」(針對建置者已發布的應用程式)原本只會每天送出最多一次,但有一天下午卻來了五次。原因是:每一份 API 副本都把已送出的警示記在自己的記憶體裡,因此兩台伺服器各送了一次;而每次部署都會清空這些記憶體,讓它們再送一次。同樣的模式也讓一個背景工作重複執行。現在兩者都改為透過共享的儲存空間協調,而且當該儲存空間無法使用時,都會偏向安全的一方。
症狀
Mythex 會在需要關注的時候發送團隊警示:服務停擺、工作卡住,或建置者已發布的應用程式點數即將用完。每個警示都有一個去重時間窗:在這段期間內,相同的警示只會送出一次。對於「低點數」警示來說,這個時間窗是 24 小時。
一個下午收到同一警示五次,表示時間窗沒有發揮作用。更糟的是,這些警示是成對出現的,每對之間大約相隔 11 分鐘。
為什麼會這樣
我們的警示套件把去重資料存在記憶體中:一張「警示鍵 → 上次送出的時間」的表,位於正在執行的程序內。這在只有一個程序且從不重新啟動的情況下沒有問題。但正式環境兩者都不符合:
- **兩個 API 副本。**正式環境執行兩份 API。每份都有自己的表,因此各自送出一次警示,這就是成對出現的原因。
- **部署。**每次部署都會用全新的程序和空白的表取代兩個副本。部署之後,警示又會再送一次,彷彿從未送出過。
每對警示之間約 11 分鐘的間隔,來自觸發此警示的工作。它在兩個副本上都會執行,並且會取得一把鎖,因此兩個副本會輪流取得鎖,各自輪到時就觸發警示。
同一段程式碼裡還有第二個較不起眼的錯誤。為了避免表無限制地增長,它在存了 500 個鍵之後會自動修剪:刪除所有超過 15 分鐘的項目,不論它們的時間窗為何。在高負載下,一個原本應持續 24 小時的去重,實際可能只維持四分之一小時。
修正方式
每個警示只有一筆共享紀錄
現在,API 在送出警示之前,會先在 Redis 中認領該警示的去重鍵。Redis 是一種共享的記憶體儲存,兩個副本都會連線到它,而且 API 重新啟動時它不會跟著重置。這項認領是一個單一操作:「只有在鍵不存在時才設定,並在時間窗結束後讓它過期」。哪個副本先認領,就由它送出警示;另一個副本發現鍵已被占用,就保持安靜。部署之後的新程序也能看到這個鍵仍然存在。
鍵與時間窗來自警示套件使用的同一套規則,現在由兩者共用,因此兩個層次對「同一個警示」的判斷不會出現分歧。
失敗時放行,而非封鎖
最重要的決定,是 Redis 無法連線時該怎麼辦。我們選擇仍然送出警示。如果 Redis 停擺,或在 1.5 秒內沒有回應,警示就會照常送出。重複的警示只是雜訊;而漏掉的警示,則代表有人沒有被通知。
P0 等級的最嚴重警示則完全不受去重機制影響。
每個項目保有自己的時間窗
在警示套件本身的記憶體去重中,現在每個項目都會記下它的時間窗何時結束,修剪時只移除時間窗已經結束的項目。一個 24 小時的鍵會保留 24 小時,無論有多少其他警示進來。
同樣的錯誤,出現在別處
同樣的模式也讓一項背景工作重複執行了:一個對帳工作,會讓我們記錄的每個工作區狀態,與沙盒供應商的狀態保持一致。每一輪它都會列出帳號中的每一個沙盒(正式環境中有數千個,大約需要五秒),以及每個執行中或已暫停的專案。兩個副本都每兩分鐘執行一次。
它的寫入具有冪等性,也就是說執行一次和執行兩次的結果相同,因此沒有造成任何損壞。只是白白多做了一倍的工作。現在,每個兩分鐘的時間窗會以我們的用量計量器已經在使用的相同方式,在 Redis 中被認領,只有認領成功的副本會執行這一輪。如果連不上 Redis,這一輪仍會執行:重複做同一件事是無害的,但不做則不行。
我們如何測試
測試模擬了真實的環境。它會把警示模組載入兩次,當作兩個獨立的「副本」,兩者都指向同一個共享儲存空間,各自觸發同一個警示;接著再載入第三個全新的副本,模擬「部署之後」的情況,並再次觸發。最後應該只送出一次警示。其他測試則確認:另一位建置者的警示仍會送出;P0 警示在兩個副本上都會送出;當儲存空間停擺或超過逾時時間而沒有回應時,警示仍然會送出。修剪的修正也有自己的測試:一個 24 小時的鍵,能在 20 分鐘後到達的 600 個其他警示之後依然存在。
至於對帳工作,測試讓兩個副本同時觸發,再加上同一時間窗內的第三次觸發,結果只產生一次列表。
我們的收穫
- **記憶體中的狀態只屬於單一程序。**任何必須在多台伺服器之間成立的事(「只送出一次」、「只執行一次」),都必須存放在它們共享的地方。
- **部署就是重新啟動。**只能撐到下一次重新啟動的狀態,無法撐過一個正常的工作日。
- **決定失敗時的方向。**對警示而言,共享儲存空間停擺,應該意味著送出更多警示,而不是更少。
- **尋找模式,而不只是個別事件。**警示是最明顯的案例;重複執行的對帳工作則是同一個錯誤,只是悄悄地浪費了運算資源。
這些在 Mythex 內部是看不見的。想了解我們如何讓平台保持穩定,可參閱我們如何把部署的 502 錯誤降到零,以及缺少的交換檔。如果你對「執行多個服務副本」的概念還不熟悉,我們的什麼是伺服器指南可以提供基礎說明。