背景
很多時候我們都不用特別的關心伺服器時間的問題,比如後臺管理系統,如果伺服器時間出錯頂多會在頁面獲取錯誤的時間而已,影響不大。但有些程式對時間非常敏感,不能出一丁點錯誤,今天要講的是去年發生在自己身邊的事:由於時間同步問題引發了部門級故障,造成非常嚴重的後果。因為事件發生還不到一年,並且就在自己身邊,所以記憶猶新。老規矩,在講故事之前先了解一下背景,故事中 X 系統後臺的簡化架構如下:
它包括 Client->Connector->Heartbeat 三個模組(當然實際有很多模組,這裡省去其他模組簡化架構,不影響問題的描述)- Client 是用來採集資料客戶端,安裝在公司所有的內部機器上,我們稱之為 Client 端。
- Connector 起到 Client 端與後臺橋樑的作用,主要用來進行 Client 連線管理、資料透傳等。
- Heartbeat 是心跳模組,用來管理 Client 上報的心跳資料。同時它兼職時間伺服器:為 Client 提供統一時間服務。
經過
上圖中 Client 模組機器數量 > 萬臺,Connector 模組機器數量 > 百臺,而 Heartbeat 模組機器卻只有一臺,這是引發慘案的根本原因。事情經過:
- 某天早上到公司,部門內部已經亂成一團。一番瞭解後才知道上述 Heartbeat 機器昨晚當機了,很多心跳資料上報不上來。這時候的問題還只是 Client 上報不了心跳資料而已,並不影響正的常資料採集。
- 運維開始在一臺新機器上部署 Heartbeat 模組,並啟動(能自動註冊並被發現)。
- 心跳資料陸續回升,但是隻到了正常數量的 60-70%。
- 各個部門開始向我們部門反映:自己部門的機器 頻繁產生 Client 的 core 檔案(C++ 程式意外終止時產生的現場檔案),而且是大面積的。
- 開發組老大和相關人獲取 core 檔案開始分析,一番定位後發現是因為 Client 出現了除零錯誤,Client 異常退出。異常處的程式碼大致如下:
int g_lastReportTime = sometime;
void report() {
int currentTime = getCurrentTime();
if (somevalue / (currentTime - g_lastReportTime ) > threshold) {
reportSomething();
g_lastReportTime = currentTime;
}
}
複製程式碼
從上面程式碼可以看出 currentTime - g_lastReportTime 在理論上是不會等於零的,但是由於新啟的機器時間並未與 X 系統內部保持一致。導致 Client 時間回退,也就出現了 currentTime - g_lastReportTime = 0 的情況。
- 運維立馬想到是新 Heartbeat 機器時間問題,由於我們的 X 系統後臺都是 Linux 機器,通過 ntpdate 命令將新機器時間統一,並將該命令加入開機啟動項。
- 心跳資料陸續恢復正常。
以上過程花了將近 40多分鐘,造成了非常大的影響。後來部門對事故相關責任人進行了問責和全部門通報。這次事故看似偶然,其實必然,從心跳機器單點那一刻。器當機造成的事故遠不及 Client core 造成的嚴重,可以看出本次事故的主要原因是由於時間不同步造成的,但它也不是孤立的,是眾多因素的統一作用結果。本標題伺服器時間同步引發的“慘案”並不誇張,希望能引起讀者這塊的注意和思考。
思考
在事故發生的時候我也沒有想太多,畢竟當時作為萌新也想不出來個所以然來。但是隨著工作經驗積累,慢慢的從這次事故中有了一些總結。這次事故主要引出以下四個問題:
1、監控問題
心跳機器當機,心跳資料上不來居然在第二天上班的時候才被發現,整整超過 8 小時了。幸虧該系統只是一個內部系統,如果是類似淘寶、天貓這種的,那造成的經濟損失和口碑影響可想而知了。聽說在半夜監控已經報出異常,但是一個新來的同事定位並未發現問題,才導致了最後的蝴蝶效應。不過好像即使當場發現,如果在切換新機器時沒做好時間同步也會出現 core 事故。對於系統監控我有以下幾點建議:
- 不要將重要模組的監控交給新人,很多情況下新人可能並不知道某個監控的重要性
- 重要模組的監控接收人不要出現“單點”,最好將主管也加入重要模組的監控通知接收人中。
- 多維度、多地點進行監控,一般一個系統都是一個整體,一旦某個模組出現問題,其他模組也會隨之出現問題。如果我們在多個維度、多個地方進行監控,即使某個地方發現不了,總有一個地方能被發現。
2、單點問題
這是一個老生常談的話題,網上有一堆的解決方案,這裡不講普適性的只介紹下針對 X 系統的。由於該系統的特殊性,Heartbeat 模組並不能進行多機器部署。所以只能單點,那我們只能祈禱單點機器不會發生故障了嗎?根據墨菲定律:如果你擔心某種情況發生,那麼它就更有可能發生。所以不要有僥倖心理,對於 X 系統 Heartbeat 的單點,雖然不能在短時間內重構,但我們還是可以做一些事情而不至於需要運維手動切換機器的。一種方案如下:
在服務註冊中心對 Heartbeat 模組機器進行監控,如果發現服務註冊中心沒有了心跳模組超過一定時間。則啟動備用機器上的 Heartbeat 併發出告警。這樣就能及時的切換到備用 Heartbeat 不至於太匆忙忘這忘那。簡單結構圖如下:
3、程式碼質量問題
事故中的錯誤程式碼是我簡化後的,一般不會出現出現這種低階錯誤,應該是一段邏輯處理後產生了類似的程式碼,只是不夠直接沒被發現。這種情況我們怎麼保證程式碼質量問題?我覺得可以從三個方面考慮:
- 程式設計師自測:程式設計師在寫完程式碼後一定要進行充分自測,雖然不能 100% 保證不出錯,但起碼能發現大部分的邏輯錯誤和低階錯誤。不要想著會有測試幫你,自己就可以偷懶,他們往往只在使用層面進行測試而不是在程式碼層面。如果不給你配備專門的測試,那就更應該自己動手、豐衣足食了。
- 程式碼評審(CodeReview):都說不識廬山真面目,只緣生在此山中。程式碼檢查也是,自己檢查自己的程式碼,很難發現一些隱藏的錯誤。這時就要其他人幫你檢查,也就是 CodeReview。當然,所有的程式碼都進行 CodeReview 是一件費時費力的事,所以要有選擇的進行。一些核心模組的程式碼,一定要一遍又一遍的自測、CodeReview。至於 CodeReview 的方案試情況而定。
- 增加專業測試人員:(1) 和 (2) 只能解決程式碼層面的錯誤,但是一些使用層面的、極端情況下的錯誤 (1) 和 (2) 並不一定能發現。所以需要專業的測試人員和測試平臺對上線前的程式碼進行充分測試。
4、分散式或者叢集時間同步問題
分散式或者叢集的時間同步也是分散式系統下需要解決的問題之一,拋開 X 系統事故中的場景不說。有很多場景是需要保持分散式系統中各個節點(機器)上的時間一致的的,比如銀行交易系統,不能讓後一筆交易的時間比前一筆交易的時間早,否則會給使用者造成困擾。不同的分散式系統有不同的解決方案,有的簡單有的複雜。簡單的像 X 系統一樣直接用 ntpdate 就可以實現,複雜的參考部落格園這篇文章《分散式系統----時鐘同步》實現自己的同步系統。
當然,以上只是泛泛而談,權當拋磚引玉。不知道你所維護的系統中是否也存在類似的問題呢?早一點把已知的問題暴露出來,比藏著掖著要好的多。在出事故之前發現問題能贏得老闆對你好感,說不定還能升職加薪,而如果在出事之後再來解決問題那就只能背鍋咯。記得關注公眾號哦,記錄著一個 C++ 程式設計師轉 Java 的學習之路。