千萬彆強制停機!我嘴都氣歪了!
你知道強制停機的後果有多嚴重嗎!
有一天,我正在愉快地寫技術文章,結果電腦啪地一下就藍屏了!
哦豁,完蛋,寫了幾千字,忘了保存!
我盲猜很多同學都有這種體驗,可能因為一些突發意外,導致自己的電腦強制停機了,丟失了自己當前的工作。
同樣,對於企業,所有的網站、應用、數據、服務都是掛在伺服器上的,一旦意外發生,比如被挖斷了電線、遭遇了自然災害,會導致伺服器被強制停機,使得機器上 所有進行中的程式被強制中斷,後果不堪設想!
有同學就笑了,不就是程式被強制中斷么,我們自己偶爾也會用任務管理器或者 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. 資源佔用
程式的強制中斷可能會導致很多資源的佔用未被釋放。比如:
- 空間佔用:如已分配的記憶體未回收,臨時文件未被刪除等。
- 埠佔用:會導致這個埠無法被其他應用程式使用。很多同學在本地調試時,應該也會遇到因為強退導致的 3000、8080 埠未被釋放的問題。
- 連接佔用:比如和遠程的服務建立了 Http 連接,由於連接未被釋放,會浪費一個連接數,就像買了電影票卻不去一樣。
9. 服務未下線
在微服務場景下,服務通常由集中的註冊中心進行統一的服務發現和管理。
比如 Eureka 註冊中心,服務生產者向註冊中心註冊服務,服務消費者從註冊中心獲取服務地址,然後遠程調用:
而一旦某個服務進程還沒有即時通知註冊中心它要下線,就中斷了,會導致服務消費者仍能獲取到該服務的路由,從而調用失敗。
此外,服務下線時如果未向上游(該服務調用方)通知,還可能導致上游的持續調用,嚴重時會產生雪崩效應,整條服務鏈路中斷!
尤其是在分散式場景下,出現進程強制中斷對集群的影響(比如數據一致性)非常大。正如 FLP不可能定理 的描述:在非同步通訊場景,即使只有一個進程失敗,也沒有任何演算法能保證非失敗進程達到一致性。
其實,相比起這些問題,更可怕的是,如果沒有完善的數據監控和檢測機制,你甚至完全不知道在強制停機後有沒有出現問題?出現了哪些問題?哪些數據丟失?哪些數據不一致?哪些任務需要補償?看不見的危險才最可怕啊!
因此,預防大於治療。一方面要養成良好習慣,無論是對自己的電腦還是伺服器,都千萬不要再主動強制停機了;另一方面,也要在程式設計時,做好應對意外停機的防控措施。不要等到失去了,才追悔莫及。