操作失敗和程序員的失誤

2018-02-24 16:17 更新

把錯(cuò)誤分成兩大類很有用[腳注3]:

  • 操作失敗?是正確編寫的程序在運(yùn)行時(shí)產(chǎn)生的錯(cuò)誤。它并不是程序的Bug,反而經(jīng)常是其它問題:系統(tǒng)本身(內(nèi)存不足或者打開文件數(shù)過多),系統(tǒng)配置(沒有到達(dá)遠(yuǎn)程主機(jī)的路由),網(wǎng)絡(luò)問題(端口掛起),遠(yuǎn)程服務(wù)(500錯(cuò)誤,連接失?。?。例子如下:
  • 連接不到服務(wù)器
  • 無法解析主機(jī)名
  • 無效的用戶輸入
  • 請求超時(shí)
  • 服務(wù)器返回500
  • 套接字被掛起
  • 系統(tǒng)內(nèi)存不足

  • 程序員失誤?是程序里的Bug。這些錯(cuò)誤往往可以通過修改代碼避免。它們永遠(yuǎn)都沒法被有效的處理。

  • 讀取 undefined 的一個(gè)屬性

  • 調(diào)用異步函數(shù)沒有指定回調(diào)

  • 該傳對象的時(shí)候傳了一個(gè)字符串

  • 該傳IP地址的時(shí)候傳了一個(gè)對象

人們把操作失敗和程序員的失誤都稱為“錯(cuò)誤”,但其實(shí)它們很不一樣。操作失敗是所有正確的程序應(yīng)該處理的錯(cuò)誤情形,只要被妥善處理它們不一定會(huì)預(yù)示著Bug或是嚴(yán)重的問題?!拔募也坏健笔且粋€(gè)操作失敗,但是它并不一定意味著哪里出錯(cuò)了。它可能只是代表著程序如果想用一個(gè)文件得事先創(chuàng)建它。

與之相反,程序員失誤是徹徹底底的Bug。這些情形下你會(huì)犯錯(cuò):忘記驗(yàn)證用戶輸入,敲錯(cuò)了變量名,諸如此類。這樣的錯(cuò)誤根本就沒法被處理,如果可以,那就意味著你用處理錯(cuò)誤的代碼代替了出錯(cuò)的代碼。

這樣的區(qū)分很重要:操作失敗是程序正常操作的一部分。而由程序員的失誤則是Bug。

有的時(shí)候,你會(huì)在一個(gè)Root問題里同時(shí)遇到操作失敗和程序員的失誤。HTTP服務(wù)器訪問了未定義的變量時(shí)奔潰了,這是程序員的失誤。當(dāng)前連接著的客戶端會(huì)在程序崩潰的同時(shí)看到一個(gè)ECONNRESET錯(cuò)誤,在NodeJS里通常會(huì)被報(bào)成“Socket Hang-up”。對客戶端來說,這是一個(gè)不相關(guān)的操作失敗, 那是因?yàn)檎_的客戶端必須處理服務(wù)器宕機(jī)或者網(wǎng)絡(luò)中斷的情況。

類似的,如果不處理好操作失敗, 這本身就是一個(gè)失誤。舉個(gè)例子,如果程序想要連接服務(wù)器,但是得到一個(gè)ECONNREFUSED錯(cuò)誤,而這個(gè)程序沒有監(jiān)聽套接字上的?error事件,然后程序崩潰了,這是程序員的失誤。連接斷開是操作失?。ㄒ?yàn)檫@是任何一個(gè)正確的程序在系統(tǒng)的網(wǎng)絡(luò)或者其它模塊出問題時(shí)都會(huì)經(jīng)歷的),如果它不被正確處理,那它就是一個(gè)失誤。

理解操作失敗和程序員失誤的不同, 是搞清怎么傳遞異常和處理異常的基礎(chǔ)。明白了這點(diǎn)再繼續(xù)往下讀。

處理操作失敗

就像性能和安全問題一樣,錯(cuò)誤處理并不是可以憑空加到一個(gè)沒有任何錯(cuò)誤處理的程序中的。你沒有辦法在一個(gè)集中的地方處理所有的異常,就像你不能在一個(gè)集中的地方解決所有的性能問題。你得考慮任何會(huì)導(dǎo)致失敗的代碼(比如打開文件,連接服務(wù)器,F(xiàn)ork子進(jìn)程等)可能產(chǎn)生的結(jié)果。包括為什么出錯(cuò),錯(cuò)誤背后的原因。之后會(huì)提及,但是關(guān)鍵在于錯(cuò)誤處理的粒度要細(xì),因?yàn)槟睦锍鲥e(cuò)和為什么出錯(cuò)決定了影響大小和對策。

你可能會(huì)發(fā)現(xiàn)在棧的某幾層不斷地處理相同的錯(cuò)誤。這是因?yàn)榈讓映讼蛏蠈觽鬟f錯(cuò)誤,上層再向它的上層傳遞錯(cuò)誤以外,底層沒有做任何有意義的事情。通常,只有頂層的調(diào)用者知道正確的應(yīng)對是什么,是重試操作,報(bào)告給用戶還是其它。但是那并不意味著,你應(yīng)該把所有的錯(cuò)誤全都丟給頂層的回調(diào)函數(shù)。因?yàn)椋攲拥幕卣{(diào)函數(shù)不知道發(fā)生錯(cuò)誤的上下文,不知道哪些操作已經(jīng)成功執(zhí)行,哪些操作實(shí)際上失敗了。

我們來更具體一些。對于一個(gè)給定的錯(cuò)誤,你可以做這些事情:

  • 直接處理。有的時(shí)候該做什么很清楚。如果你在嘗試打開日志文件的時(shí)候得到了一個(gè)ENOENT錯(cuò)誤,很有可能你是第一次打開這個(gè)文件,你要做的就是首先創(chuàng)建它。更有意思的例子是,你維護(hù)著到服務(wù)器(比如數(shù)據(jù)庫)的持久連接,然后遇到了一個(gè)“socket hang-up”的異常。這通常意味著要么遠(yuǎn)端要么本地的網(wǎng)絡(luò)失敗了。很多時(shí)候這種錯(cuò)誤是暫時(shí)的,所以大部分情況下你得重新連接來解決問題。(這和接下來的重試不大一樣,因?yàn)樵谀愕玫竭@個(gè)錯(cuò)誤的時(shí)候不一定有操作正在進(jìn)行)

  • 把出錯(cuò)擴(kuò)散到客戶端。如果你不知道怎么處理這個(gè)異常,最簡單的方式就是放棄你正在執(zhí)行的操作,清理所有開始的,然后把錯(cuò)誤傳遞給客戶端。(怎么傳遞異常是另外一回事了,接下來會(huì)討論)。這種方式適合錯(cuò)誤短時(shí)間內(nèi)無法解決的情形。比如,用戶提交了不正確的JSON,你再解析一次是沒什么幫助的。

  • 重試操作。對于那些來自網(wǎng)絡(luò)和遠(yuǎn)程服務(wù)的錯(cuò)誤,有的時(shí)候重試操作就可以解決問題。比如,遠(yuǎn)程服務(wù)返回了503(服務(wù)不可用錯(cuò)誤),你可能會(huì)在幾秒種后重試。如果確定要重試,你應(yīng)該清晰的用文檔記錄下將會(huì)多次重試,重試多少次直到失敗,以及兩次重試的間隔。?另外,不要每次都假設(shè)需要重試。如果在棧中很深的地方(比如,被一個(gè)客戶端調(diào)用,而那個(gè)客戶端被另外一個(gè)由用戶操作的客戶端控制),這種情形下快速失敗讓客戶端去重試會(huì)更好。如果棧中的每一層都覺得需要重試,用戶最終會(huì)等待更長的時(shí)間,因?yàn)槊恳粚佣紱]有意識到下層同時(shí)也在嘗試。

  • 直接崩潰。對于那些本不可能發(fā)生的錯(cuò)誤,或者由程序員失誤導(dǎo)致的錯(cuò)誤(比如無法連接到同一程序里的本地套接字),可以記錄一個(gè)錯(cuò)誤日志然后直接崩潰。其它的比如內(nèi)存不足這種錯(cuò)誤,是JavaScript這樣的腳本語言無法處理的,崩潰是十分合理的。(即便如此,在child_process.exec這樣的分離的操作里,得到ENOMEM錯(cuò)誤,或者那些你可以合理處理的錯(cuò)誤時(shí),你應(yīng)該考慮這么做)。在你無計(jì)可施需要讓管理員做修復(fù)的時(shí)候,你也可以直接崩潰。如果你用光了所有的文件描述符或者沒有訪問配置文件的權(quán)限,這種情況下你???么都做不了,只能等某個(gè)用戶登錄系統(tǒng)把東西修好。

  • 記錄錯(cuò)誤,其他什么都不做。有的時(shí)候你什么都做不了,沒有操作可以重試或者放棄,沒有任何理由崩潰掉應(yīng)用程序。舉個(gè)例子吧,你用DNS跟蹤了一組遠(yuǎn)程服務(wù),結(jié)果有一個(gè)DNS失敗了。除了記錄一條日志并且繼續(xù)使用剩下的服務(wù)以外,你什么都做不了。但是,你至少得記錄點(diǎn)什么(凡事都有例外。如果這種情況每秒發(fā)生幾千次,而你又沒法處理,那每次發(fā)生都記錄可能就不值得了,但是要周期性的記錄)。

(沒有辦法)處理程序員的失誤

對于程序員的失誤沒有什么好做的。從定義上看,一段本該工作的代碼壞掉了(比如變量名敲錯(cuò)),你不能用更多的代碼再去修復(fù)它。一旦你這樣做了,你就使用錯(cuò)誤處理的代碼代替了出錯(cuò)的代碼。

有些人贊成從程序員的失誤中恢復(fù),也就是讓當(dāng)前的操作失敗,但是繼續(xù)處理請求。這種做法不推薦。考慮這樣的情況:原始代碼里有一個(gè)失誤是沒考慮到某種特殊情況。你怎么確定這個(gè)問題不會(huì)影響其他請求呢?如果其它的請求共享了某個(gè)狀態(tài)(服務(wù)器,套接字,數(shù)據(jù)庫連接池等),有極大的可能其他請求會(huì)不正常。

典型的例子是REST服務(wù)器(比如用Restify搭的),如果有一個(gè)請求處理函數(shù)拋出了一個(gè)ReferenceError(比如,變量名打錯(cuò))。繼續(xù)運(yùn)行下去很有肯能會(huì)導(dǎo)致嚴(yán)重的Bug,而且極其難發(fā)現(xiàn)。例如:

  1. 一些請求間共享的狀態(tài)可能會(huì)被變成null,undefined或者其它無效值,結(jié)果就是下一個(gè)請求也失敗了。
  2. 數(shù)據(jù)庫(或其它)連接可能會(huì)被泄露,降低了能夠并行處理的請求數(shù)量。最后只剩下幾個(gè)可用連接會(huì)很壞,將導(dǎo)致請求由并行變成串行被處理。
  3. 更糟的是, postgres 連接會(huì)被留在打開的請求事務(wù)里。這會(huì)導(dǎo)致 postgres “持有”表中某一行的舊值,因?yàn)樗鼘@個(gè)事務(wù)可見。這個(gè)問題會(huì)存在好幾周,造成表無限制的增長,后續(xù)的請求全都被拖慢了,從幾毫秒到幾分鐘[腳注4]。雖然這個(gè)問題和 postgres 緊密相關(guān),但是它很好的說明了程序員一個(gè)簡單的失誤會(huì)讓應(yīng)用程序陷入一種非??膳碌臓顟B(tài)。
  4. 連接會(huì)停留在已認(rèn)證的狀態(tài),并且被后續(xù)的連接使用。結(jié)果就是在請求里搞錯(cuò)了用戶。
  5. 套接字會(huì)一直打開著。一般情況下 NodeJS 會(huì)在一個(gè)空閑的套接字上應(yīng)用兩分鐘的超時(shí),但這個(gè)值可以覆蓋,這將會(huì)泄露一個(gè)文件描述符。如果這種情況不斷發(fā)生,程序會(huì)因?yàn)橛霉饬怂械奈募枋龇鴱?qiáng)退。即使不覆蓋這個(gè)超時(shí)時(shí)間,客戶端會(huì)掛兩分鐘直到 “hang-up” 錯(cuò)誤的發(fā)生。這兩分鐘的延遲會(huì)讓問題難于處理和調(diào)試。
  6. 很多內(nèi)存引用會(huì)被遺留。這會(huì)導(dǎo)致泄露,進(jìn)而導(dǎo)致內(nèi)存耗盡,GC需要的時(shí)間增加,最后性能急劇下降。這點(diǎn)非常難調(diào)試,而且很需要技巧與導(dǎo)致造成泄露的失誤聯(lián)系起來。

最好的從失誤恢復(fù)的方法是立刻崩潰。你應(yīng)該用一個(gè)restarter 來啟動(dòng)你的程序,在奔潰的時(shí)候自動(dòng)重啟。如果restarter 準(zhǔn)備就緒,崩潰是失誤來臨時(shí)最快的恢復(fù)可靠服務(wù)的方法。

奔潰應(yīng)用程序唯一的負(fù)面影響是相連的客戶端臨時(shí)被擾亂,但是記住:

  • 從定義上看,這些錯(cuò)誤屬于Bug。我們并不是在討論正常的系統(tǒng)或是網(wǎng)絡(luò)錯(cuò)誤,而是程序里實(shí)際存在的Bug。它們應(yīng)該在線上很罕見,并且是調(diào)試和修復(fù)的最高優(yōu)先級。
  • 上面討論的種種情形里,請求沒有必要一定得成功完成。請求可能成功完成,可能讓服務(wù)器再次崩潰,可能以某種明顯的方式不正確的完成,或者以一種很難調(diào)試的方式錯(cuò)誤的結(jié)束了。
  • 在一個(gè)完備的分布式系統(tǒng)里,客戶端必須能夠通過重連和重試來處理服務(wù)端的錯(cuò)誤。不管 NodeJS 應(yīng)用程序是否被允許崩潰,網(wǎng)絡(luò)和系統(tǒng)的失敗已經(jīng)是一個(gè)事實(shí)了。
  • 如果你的線上代碼如此頻繁地崩潰讓連接斷開變成了問題,那么正真的問題是你的服務(wù)器Bug太多了,而不是因?yàn)槟氵x擇出錯(cuò)就崩潰。

如果出現(xiàn)服務(wù)器經(jīng)常崩潰導(dǎo)致客戶端頻繁掉線的問題,你應(yīng)該把經(jīng)歷集中在造成服務(wù)器崩潰的Bug上,把它們變成可捕獲的異常,而不是在代碼明顯有問題的情況下盡可能地避免崩潰。調(diào)試這類問題最好的方法是,把 NodeJS 配置成出現(xiàn)未捕獲異常時(shí)把內(nèi)核文件打印出來。在 GNU/Linux 或者 基于 illumos 的系統(tǒng)上使用這些內(nèi)核文件,你不僅查看應(yīng)用崩潰時(shí)的堆棧記錄,還可以看到傳遞給函數(shù)的參數(shù)和其它的 JavaScript 對象,甚至是那些在閉包里引用的變量。即使沒有配置 code dumps,你也可以用堆棧信息和日志來開始處理問題。

最后,記住程序員在服務(wù)器端的失誤會(huì)造成客戶端的操作失敗,還有客戶端必須處理好服務(wù)器端的奔潰和網(wǎng)絡(luò)中斷。這不只是理論,而是實(shí)際發(fā)生在線上環(huán)境里。

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號