把錯(cuò)誤分成兩大類很有用[腳注3]:
系統(tǒng)內(nèi)存不足
程序員失誤?是程序里的Bug。這些錯(cuò)誤往往可以通過修改代碼避免。它們永遠(yuǎn)都沒法被有效的處理。
讀取 undefined 的一個(gè)屬性
調(diào)用異步函數(shù)沒有指定回調(diào)
該傳對象的時(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ò)),你不能用更多的代碼再去修復(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)。例如:
null
,undefined
或者其它無效值,結(jié)果就是下一個(gè)請求也失敗了。最好的從失誤恢復(fù)的方法是立刻崩潰。你應(yīng)該用一個(gè)restarter 來啟動(dòng)你的程序,在奔潰的時(shí)候自動(dòng)重啟。如果restarter 準(zhǔn)備就緒,崩潰是失誤來臨時(shí)最快的恢復(fù)可靠服務(wù)的方法。
奔潰應(yīng)用程序唯一的負(fù)面影響是相連的客戶端臨時(shí)被擾亂,但是記住:
如果出現(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)境里。
更多建議: