警報只響一次,而非每部伺服器各響一次
我們其中一個營運警報(用於通知某個建立工具的已發佈應用程式「額度不足」)原本應該最多每天只送達我們一次。結果有一個下午,它送到了五次。原因是:我們 API 的每個副本各自在自己的記憶體中記住了已經發送過哪些警報,所以兩部伺服器各發送了一次;而每次部署都會清空那份記憶體,讓它們再次發送。同樣的模式也曾令一個後台工作重複執行。現在兩者都透過共享儲存來協調,而且如果該儲存無法使用,兩者都會以安全的一方為準來失效。
症狀
當有事情需要留意時,Mythex 會向團隊發送警報:服務停止運作、工作停滯,或者某個建立工具的額度餘額偏低而其應用程式仍已發佈。每個警報都有一個去重時間窗,即在該期間內,同一警報只會發送一次。對於「額度不足」警報,這個時間窗是 24 小時。
同一個下午收到同一警報的五份副本,代表這個時間窗沒有發揮作用。更糟的是,它們是成對出現的,每對之間大約相隔 11 分鐘。
為何會發生
我們的警報套件把去重資料保存在記憶體中:一張「警報鍵 → 上次發送時間」的表格,存在於運行中的程序內。這在只有一個永不重新啟動的程序時行得通。但正式環境兩者都不符合:
- 兩個 API 副本。 正式環境運行兩份 API。每份都有自己的表格,因此各自發送了一次該警報。這就是成對出現的原因。
- 部署。 每次部署都會以全新的程序和空白的表格取代兩個副本。部署之後,警報又再次發出,彷彿從未發送過一樣。
每對警報之間約 11 分鐘的間隔,來自觸發此警報的工作。它在兩個副本上都運行,並且會取得一個鎖,所以兩個副本會輪流取得這個鎖,而每個副本輪到時都會觸發一次警報。
同一段程式碼還有第二個較為隱蔽的缺陷。為了防止表格無限增長,當它存放的鍵達到 500 個時,程式就會自行修剪:刪除所有超過 15 分鐘的條目,無論它們的時間窗為何。在高負載下,一個 24 小時的去重時間窗可能只維持了四分之一小時。
修正方法
每個警報一份共享紀錄
在 API 發送警報之前,它現在會先在 Redis(一個兩個副本都會連接、且不會隨 API 重新啟動而重置的共享記憶體儲存)中認領該警報的去重鍵。這個認領是一個單一操作:「只在鍵不存在時設定它,並在時間窗結束後令其過期」。先認領的副本負責發送警報;另一個副本發現鍵已被佔用,便保持靜默。部署之後的新程序會發現該鍵仍然存在。
鍵和時間窗來自警報套件所用的同一條規則,現在兩者共用這條規則,因此兩個層次不會對「同一個警報」的定義產生分歧。
失敗時保持開放,而非關閉
最重要的決定,是當無法連接 Redis 時該怎麼辦。我們選擇仍然發送警報。如果 Redis 停止運作,或者在 1.5 秒內沒有回應,警報就照常發出。重複的警報只是噪音;遺漏的警報則代表有人沒有被通知。
最嚴重的警報 P0,則完全不會被去重機制攔截。
每個條目保留自己的時間窗
在警報套件自己的記憶體去重中,每個條目現在都會儲存其時間窗的結束時間,而修剪只會移除時間窗已結束的條目。一個 24 小時的鍵會保留 24 小時,無論有多少其他警報進來。
同一個缺陷,出現在別處
同樣的模式也令一項後台工作重複執行:一個對帳工作,負責讓我們記錄的每個工作區狀態與沙盒供應商保持一致。每次執行都會列出帳戶中的每個沙盒(正式環境有數千個,大約需要五秒),以及每個運行中或已暫停的專案。兩個副本都每兩分鐘執行一次。
它的寫入具有冪等性,即執行兩次與執行一次的結果相同,所以並沒有造成任何損壞,只是工作量加倍。現在,每個兩分鐘的時段都會以我們用量計量器認領時段的同一方式,在 Redis 中被認領,而且只有認領到的副本才會執行這次作業。如果無法連接 Redis,作業仍會執行:重複做同一件工作無害,而不做則不可接受。
我們如何測試
測試模擬了真實的設定。它們把警報模組載入兩次,作為兩個獨立的「副本」,兩者都指向同一個共享儲存,然後從每個副本各自觸發同一警報;接著再載入第三個全新的副本,代表「部署之後」,並再次觸發它。結果只會發送一則警報。其他測試則確認:不同建立工具的警報仍會發出、P0 會從兩個副本都發送,而且當儲存停止運作或超過逾時時,警報仍會發送。修剪的修正有自己的測試:一個 24 小時的鍵,能在 20 分鐘後另外 600 個警報進來時仍然存活。
對於對帳工作,兩個副本同時觸發,加上同一時段內的第三次觸發,最終只產生一次列表查詢。
我們從中學到的
- 記憶體中的狀態屬於單一程序。 任何必須在多部伺服器之間成立的事(「只發送一次」、「只執行一次」),都需要存放在它們共享的地方。
- 部署就是重新啟動。 只能維持到下一次重新啟動為止的狀態,撐不過一個正常的工作日。
- 決定失敗時的方向。 對於警報而言,共享儲存停止運作,應該意味著發送更多警報,而不是更少。
- 留意模式,而不只是個別實例。 那個警報是最明顯的案例;重複執行的對帳工作則是同一缺陷,只是悄悄地浪費了工作量。
以上這些在 Mythex 內部都看不見。如需了解我們如何讓平台保持穩定,請參閱我們如何把部署時的 502 錯誤降至零,以及缺失的交換檔案。如果你對同時運行多個服務副本的概念還不熟悉,我們的伺服器是什麼指南涵蓋了基本知識。