千萬彆強制停機!我嘴都氣歪了!

你知道強制停機的後果有多嚴重嗎!

有一天,我正在愉快地寫技術文章,結果電腦啪地一下就藍屏了!

哦豁,完蛋,寫了幾千字,忘了保存!

我盲猜很多同學都有這種體驗,可能因為一些突發意外,導致自己的電腦強制停機了,丟失了自己當前的工作。

同樣,對於企業,所有的網站、應用、數據、服務都是掛在伺服器上的,一旦意外發生,比如被挖斷了電線、遭遇了自然災害,會導致伺服器被強制停機,使得機器上 所有進行中的程式被強制中斷,後果不堪設想!

有同學就笑了,不就是程式被強制中斷么,我們自己偶爾也會用任務管理器或者 kill -9 命令殺個進程啊,抓緊重新啟動程式不就好了,有啥大不了的?

的確,我以前也是通過強殺進程來下線和升級服務的,乾脆利落爽。但直到後來有一次,因為強殺進程導致了線上事故,造成了經濟損失和加班,把我嘴都氣歪了!我才意識到自己之前太粗暴、想法太簡單了。

其實,一個程式被強制中斷,除了無法提供服務外,還有很多嚴重的後果!

1. 請求丟失

對於一個 web 伺服器,比如 Java Web 開發中主流的 Tomcat。當接受到請求時,會開啟一個執行緒來處理該請求。而如果請求數較多,執行緒處理不過來,就會將此請求放入等待隊列中,排隊等待空閑執行緒。

等待隊列

假設 web 服務進程突然中斷,會導致所有在記憶體隊列中等待執行的請求丟失,等了半天,等了個空!

2. 業務中斷

一旦進程中斷,會導致 所有 正在執行的業務中斷,會導致很多意想不到的後果。

比如有一個檢查數據的任務,要檢查所有資料庫中狀態為 0 的數據是否正確,程式碼流程如下:

// 開始檢查,數據狀態由 0 置為 1
startCheck();
// 檢查
doCheck();
// 結束檢查,將正確的數據狀態置為 2
endCheck();

假設剛把數據的狀態置為 1,表示正在檢查中。然後程式就中斷了,會導致以後這條數據的狀態始終為 1,再也不會被檢查。

同理,如果已經檢查完,並且數據正確,本來應該將數據狀態置為 2,但這時程式中斷,也會導致 數據的狀態和預期不一致

以上只是一個簡單的例子,但實際的業務場景中,業務中斷可能直接影響收益,尤其是涉及交易的支付轉賬業務,如果用戶已經付款,卻因為程式的中斷,沒有存儲付款記錄,那這個支付業務不是真要涼涼?

3. 事務中斷

資料庫事務是指對資料庫的一系列 不可分割 的操作,具有一致性,每次執行必須使資料庫從一個一致性狀態變到另一個一致性狀態。

比如轉賬業務中,用戶 A 要給用戶 B 轉賬 1 元,用戶 A 扣除 1 元,用戶 B 就要增加 1 元。

但如果用戶 A 已扣除 1元後,應用程式或者資料庫系統突然掛了,導致事務尚未完成就被迫中斷,結果用戶 B 的總金額並沒有變化。這時資料庫就處於不一致狀態。同理,即使在程式中設計了回滾,回滾過程也可能會被中斷!

除了數據不一致外,事務中斷還可能導致鎖行、鎖表,使得這部分 數據的可用性受到影響

4. 文件損壞

假設程式正在向一個文件進行寫操作,還未完成,就被中斷了,可能會導致文件的不完整、甚至損壞。

這讓我想起小時候,電腦配置不高,有時玩遊戲會卡住,然後我就強制殺了進程,結果導致遊戲文件損壞,只能重新下載遊戲。

文件損壞

5. 任務丟失

我們在編寫業務程式碼時,經常會將比較耗時的任務非同步化,將任務提交到執行緒池後立即返回成功。執行緒池會從任務隊列中依次讀取並執行任務。

而一旦程式中斷,執行緒池中的任務就會丟失,好像他從來沒有被提交過一樣。這種感覺就像你答應別人要做一件事,別人對你很放心,但你最後卻放了鴿子跑路了。

6. 數據丟失

有時,我們會先將數據臨時放在記憶體中,然後定期、定時、或者分批地持久化到資料庫或本地磁碟中。

比如 Redis 資料庫的 RDB 機制,每隔一段時間,會將記憶體中的數據進行本地備份,從而降低大量數據並發寫入時的負載,提升性能。

但就像上面提到的任務丟失一樣,一旦程式中斷,可能會導致很多 未持久化的數據丟失,比如快取、分批提交數據等。

7. 消息丟失

在分散式系統中,各個節點間經常通過消息來進行交互和協作,而程式的中斷可能會在不同情況下導致消息丟失。

1. 消息未發出

假設某支付業務中,已經扣除了用戶的賬戶餘額,並更新了資料庫,接下來要向客戶端返回應答消息。

但是消息正在發送隊列中排隊等待發送時,由於進程被強制退出導致消息未發出,從而導致應答消息丟失。客戶端久久接收不到消息後,可能會發起重試,導致重複更新。

消息未發出

2. 消息未確認

比如說某段業務程式碼從消息隊列中取出了一個消息,進行消費處理,程式碼流程如下:

// 獲取下一個消息
Message msg = getNextMsg();
// 處理消息
int res = handleMsg(msg);
// 處理成功?
if(res == 0) {
	// 確認消息
  ack();
} else {
  // 拒絕確認消息
  nack();
}

無論消息處理成功與否,都必須要給消息隊列一個回復!如果處理成功,要告訴他這條消息已經被我處理完成啦,請給我下一條消息;即使處理失敗,也要告訴消息隊列,請給我重發本條消息。

而一旦程式中斷,這條消息的處理結果便無人知曉,可能導致消息隊列的 阻塞或者無限重發(根據具體消息隊列來決定)。

8. 資源佔用

程式的強制中斷可能會導致很多資源的佔用未被釋放。比如:

  1. 空間佔用:如已分配的記憶體未回收,臨時文件未被刪除等。
  2. 埠佔用:會導致這個埠無法被其他應用程式使用。很多同學在本地調試時,應該也會遇到因為強退導致的 3000、8080 埠未被釋放的問題。
  3. 連接佔用:比如和遠程的服務建立了 Http 連接,由於連接未被釋放,會浪費一個連接數,就像買了電影票卻不去一樣。

9. 服務未下線

在微服務場景下,服務通常由集中的註冊中心進行統一的服務發現和管理。

比如 Eureka 註冊中心,服務生產者向註冊中心註冊服務,服務消費者從註冊中心獲取服務地址,然後遠程調用:

Eureka 註冊中心

而一旦某個服務進程還沒有即時通知註冊中心它要下線,就中斷了,會導致服務消費者仍能獲取到該服務的路由,從而調用失敗。

此外,服務下線時如果未向上游(該服務調用方)通知,還可能導致上游的持續調用,嚴重時會產生雪崩效應,整條服務鏈路中斷!

尤其是在分散式場景下,出現進程強制中斷對集群的影響(比如數據一致性)非常大。正如 FLP不可能定理 的描述:在非同步通訊場景,即使只有一個進程失敗,也沒有任何演算法能保證非失敗進程達到一致性。


其實,相比起這些問題,更可怕的是,如果沒有完善的數據監控和檢測機制,你甚至完全不知道在強制停機後有沒有出現問題?出現了哪些問題?哪些數據丟失?哪些數據不一致?哪些任務需要補償?看不見的危險才最可怕啊!

因此,預防大於治療。一方面要養成良好習慣,無論是對自己的電腦還是伺服器,都千萬不要再主動強制停機了;另一方面,也要在程式設計時,做好應對意外停機的防控措施。不要等到失去了,才追悔莫及。