千萬別強制停機!我嘴都氣歪了!
你知道強制停機的后果有多嚴重嗎!
有一天,我正在愉快地寫技術文章,結果電腦啪地一下就藍屏了!

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

我盲猜很多同學都有這種體驗,可能因為一些突發(fā)意外,導致自己的電腦強制停機了,丟失了自己當前的工作。
同樣,對于企業(yè),所有的網(wǎng)站、應用、數(shù)據(jù)、服務都是掛在服務器上的,一旦意外發(fā)生,比如被挖斷了電線、遭遇了自然災害,會導致服務器被強制停機,使得機器上 所有進行中的程序被強制中斷,后果不堪設想!
有同學就笑了,不就是程序被強制中斷么,我們自己偶爾也會用任務管理器或者 kill -9
命令殺個進程啊,抓緊重新啟動程序不就好了,有啥大不了的?

的確,我以前也是通過強殺進程來下線和升級服務的,干脆利落爽。但直到后來有一次,因為強殺進程導致了線上事故,造成了經(jīng)濟損失和加班,把我嘴都氣歪了!我才意識到自己之前太粗暴、想法太簡單了。
其實,一個程序被強制中斷,除了無法提供服務外,還有很多嚴重的后果!
1. 請求丟失
對于一個 web 服務器,比如 Java Web 開發(fā)中主流的 Tomcat。當接受到請求時,會開啟一個線程來處理該請求。而如果請求數(shù)較多,線程處理不過來,就會將此請求放入等待隊列中,排隊等待空閑線程。

假設 web 服務進程突然中斷,會導致所有在內存隊列中等待執(zhí)行的請求丟失,等了半天,等了個空!
2. 業(yè)務中斷
一旦進程中斷,會導致 所有 正在執(zhí)行的業(yè)務中斷,會導致很多意想不到的后果。
比如有一個檢查數(shù)據(jù)的任務,要檢查所有數(shù)據(jù)庫中狀態(tài)為 0 的數(shù)據(jù)是否正確,代碼流程如下:
//?開始檢查,數(shù)據(jù)狀態(tài)由?0?置為?1
startCheck();
//?檢查
doCheck();
//?結束檢查,將正確的數(shù)據(jù)狀態(tài)置為?2
endCheck();
假設剛把數(shù)據(jù)的狀態(tài)置為 1,表示正在檢查中。然后程序就中斷了,會導致以后這條數(shù)據(jù)的狀態(tài)始終為 1,再也不會被檢查。
同理,如果已經(jīng)檢查完,并且數(shù)據(jù)正確,本來應該將數(shù)據(jù)狀態(tài)置為 2,但這時程序中斷,也會導致 數(shù)據(jù)的狀態(tài)和預期不一致。
以上只是一個簡單的例子,但實際的業(yè)務場景中,業(yè)務中斷可能直接影響收益,尤其是涉及交易的支付轉賬業(yè)務,如果用戶已經(jīng)付款,卻因為程序的中斷,沒有存儲付款記錄,那這個支付業(yè)務不是真要涼涼?
3. 事務中斷
數(shù)據(jù)庫事務是指對數(shù)據(jù)庫的一系列 不可分割 的操作,具有一致性,每次執(zhí)行必須使數(shù)據(jù)庫從一個一致性狀態(tài)變到另一個一致性狀態(tài)。
比如轉賬業(yè)務中,用戶 A 要給用戶 B 轉賬 1 元,用戶 A 扣除 1 元,用戶 B 就要增加 1 元。

但如果用戶 A 已扣除 1元后,應用程序或者數(shù)據(jù)庫系統(tǒng)突然掛了,導致事務尚未完成就被迫中斷,結果用戶 B 的總金額并沒有變化。這時數(shù)據(jù)庫就處于不一致狀態(tài)。同理,即使在程序中設計了回滾,回滾過程也可能會被中斷!
除了數(shù)據(jù)不一致外,事務中斷還可能導致鎖行、鎖表,使得這部分 數(shù)據(jù)的可用性受到影響。
4. 文件損壞
假設程序正在向一個文件進行寫操作,還未完成,就被中斷了,可能會導致文件的不完整、甚至損壞。
這讓我想起小時候,電腦配置不高,有時玩游戲會卡住,然后我就強制殺了進程,結果導致游戲文件損壞,只能重新下載游戲。

5. 任務丟失
我們在編寫業(yè)務代碼時,經(jīng)常會將比較耗時的任務異步化,將任務提交到線程池后立即返回成功。線程池會從任務隊列中依次讀取并執(zhí)行任務。
而一旦程序中斷,線程池中的任務就會丟失,好像他從來沒有被提交過一樣。這種感覺就像你答應別人要做一件事,別人對你很放心,但你最后卻放了鴿子跑路了。

6. 數(shù)據(jù)丟失
有時,我們會先將數(shù)據(jù)臨時放在內存中,然后定期、定時、或者分批地持久化到數(shù)據(jù)庫或本地磁盤中。
比如 Redis 數(shù)據(jù)庫的 RDB 機制,每隔一段時間,會將內存中的數(shù)據(jù)進行本地備份,從而降低大量數(shù)據(jù)并發(fā)寫入時的負載,提升性能。
但就像上面提到的任務丟失一樣,一旦程序中斷,可能會導致很多 未持久化的數(shù)據(jù)丟失,比如緩存、分批提交數(shù)據(jù)等。

7. 消息丟失
在分布式系統(tǒng)中,各個節(jié)點間經(jīng)常通過消息來進行交互和協(xié)作,而程序的中斷可能會在不同情況下導致消息丟失。
1. 消息未發(fā)出
假設某支付業(yè)務中,已經(jīng)扣除了用戶的賬戶余額,并更新了數(shù)據(jù)庫,接下來要向客戶端返回應答消息。
但是消息正在發(fā)送隊列中排隊等待發(fā)送時,由于進程被強制退出導致消息未發(fā)出,從而導致應答消息丟失??蛻舳司镁媒邮詹坏较⒑螅赡軙l(fā)起重試,導致重復更新。

2. 消息未確認
比如說某段業(yè)務代碼從消息隊列中取出了一個消息,進行消費處理,代碼流程如下:
//?獲取下一個消息
Message?msg?=?getNextMsg();
//?處理消息
int?res?=?handleMsg(msg);
//?處理成功?
if(res?==?0)?{
?//?確認消息
??ack();
}?else?{
??//?拒絕確認消息
??nack();
}
無論消息處理成功與否,都必須要給消息隊列一個回復!如果處理成功,要告訴他這條消息已經(jīng)被我處理完成啦,請給我下一條消息;即使處理失敗,也要告訴消息隊列,請給我重發(fā)本條消息。
而一旦程序中斷,這條消息的處理結果便無人知曉,可能導致消息隊列的 阻塞或者無限重發(fā)(根據(jù)具體消息隊列來決定)。
8. 資源占用
程序的強制中斷可能會導致很多資源的占用未被釋放。比如:
空間占用:如已分配的內存未回收,臨時文件未被刪除等。
端口占用:會導致這個端口無法被其他應用程序使用。很多同學在本地調試時,應該也會遇到因為強退導致的 3000、8080 端口未被釋放的問題。
連接占用:比如和遠程的服務建立了 Http 連接,由于連接未被釋放,會浪費一個連接數(shù),就像買了電影票卻不去一樣。
####9. 服務未下線
在微服務場景下,服務通常由集中的注冊中心進行統(tǒng)一的服務發(fā)現(xiàn)和管理。
比如 Eureka 注冊中心,服務生產(chǎn)者向注冊中心注冊服務,服務消費者從注冊中心獲取服務地址,然后遠程調用:

而一旦某個服務進程還沒有即時通知注冊中心它要下線,就中斷了,會導致服務消費者仍能獲取到該服務的路由,從而調用失敗。
此外,服務下線時如果未向上游(該服務調用方)通知,還可能導致上游的持續(xù)調用,嚴重時會產(chǎn)生雪崩效應,整條服務鏈路中斷!
尤其是在分布式場景下,出現(xiàn)進程強制中斷對集群的影響(比如數(shù)據(jù)一致性)非常大。正如 FLP不可能定理 的描述:在異步通信場景,即使只有一個進程失敗,也沒有任何算法能保證非失敗進程達到一致性。

其實,相比起這些問題,更可怕的是,如果沒有完善的數(shù)據(jù)監(jiān)控和檢測機制,你甚至完全不知道在強制停機后有沒有出現(xiàn)問題?出現(xiàn)了哪些問題?哪些數(shù)據(jù)丟失?哪些數(shù)據(jù)不一致?哪些任務需要補償?看不見的危險才最可怕??!
因此,預防大于治療。一方面要養(yǎng)成良好習慣,無論是對自己的電腦還是服務器,都千萬不要再主動強制停機了;另一方面,也要在程序設計時,做好應對意外停機的防控措施。不要等到失去了,才追悔莫及。