譯者 | 劉汪洋
審校 | 重樓
很多年前,我在維護(hù)一個(gè)數(shù)據(jù)庫(kù)驅(qū)動(dòng)的系統(tǒng)時(shí)遇到了一個(gè)奇怪的生產(chǎn)環(huán)境的 bug。我讀取的列有一個(gè)空值,但是代碼中不允許這樣,而且也沒(méi)有地方可以讓這個(gè)值為空。數(shù)據(jù)庫(kù)嚴(yán)重?fù)p壞,我們沒(méi)有任何線索。雖然有日志,但是由于隱私問(wèn)題,關(guān)鍵信息并未被打印出來(lái)。即使我們能打印,我們?cè)趺粗涝撜沂裁茨兀?/p>
應(yīng)用程序出錯(cuò)不可避免。我們努力減少出錯(cuò),但總是還會(huì)出錯(cuò)。我們還有另一項(xiàng)工作,它并未得到足夠的關(guān)注:故障分析。有一些最佳實(shí)踐和常見方法,最著名的就是日志記錄。我曾多次說(shuō)過(guò),日志其實(shí)是預(yù)知性的調(diào)試,但是我們?cè)撊绾蝿?chuàng)建一個(gè)更容易調(diào)試的應(yīng)用程序呢?
我們應(yīng)如何構(gòu)建系統(tǒng),以便當(dāng)它出現(xiàn)類似的錯(cuò)誤時(shí),我們能知道出了什么問(wèn)題?
一個(gè)常見的理念是:“艱苦的準(zhǔn)備讓工作更輕松。” 在開發(fā)階段,我們面臨的挑戰(zhàn)可能更大,因?yàn)槲覀儫o(wú)法預(yù)見在生產(chǎn)過(guò)程中會(huì)遇到哪些問(wèn)題。但是,這個(gè)階段的準(zhǔn)備工作也很有價(jià)值,因?yàn)槲覀冋跒樯a(chǎn)階段做準(zhǔn)備。
這種準(zhǔn)備超出了測(cè)試和質(zhì)量保證的范圍。它意味著我們需要為代碼和基礎(chǔ)設(shè)施在未來(lái)可能出現(xiàn)的問(wèn)題做好準(zhǔn)備。到了出現(xiàn)問(wèn)題的那一刻,測(cè)試和質(zhì)量保證就失效了。簡(jiǎn)單來(lái)說(shuō),這是一種對(duì)未知問(wèn)題的預(yù)防措施。
失敗的定義
首先,我們需要明確什么是失敗。當(dāng)我提到生產(chǎn)環(huán)境中的失敗,人們往往會(huì)自動(dòng)聯(lián)想到系統(tǒng)崩潰、網(wǎng)站宕機(jī)或?yàn)?zāi)難級(jí)別的事件。然而,實(shí)際上,這些情況相對(duì)較少,大部分都由運(yùn)維人員和系統(tǒng)工程師處理。
當(dāng)我向開發(fā)人員詢問(wèn)他們最近遇到的生產(chǎn)問(wèn)題時(shí),他們通常會(huì)猶豫不決,甚至無(wú)法回憶起具體情況。然而在進(jìn)行更深入的交談和詢問(wèn)后,開發(fā)者們回憶起了一個(gè)他們最近處理過(guò)的錯(cuò)誤。這個(gè)錯(cuò)誤的確是在生產(chǎn)環(huán)境中出現(xiàn)的,并且是由客戶報(bào)告的。他們必須在本地以某種方式復(fù)現(xiàn)它,或者審查信息來(lái)修復(fù)它。我們并不把這種錯(cuò)誤看作是生產(chǎn)錯(cuò)誤,但它們確實(shí)是。需要復(fù)現(xiàn)已經(jīng)在現(xiàn)實(shí)世界中發(fā)生的故障,這使我們的工作變得更困難。
如果我們能夠僅通過(guò)查看問(wèn)題在生產(chǎn)環(huán)境中出現(xiàn)的方式就理解問(wèn)題,那就太好了。
簡(jiǎn)潔性
簡(jiǎn)潔性非常顯而易見,但是在實(shí)際應(yīng)用中,人們對(duì)于簡(jiǎn)潔性的理解卻各不相同。簡(jiǎn)潔性是主觀的。以下哪段代碼更簡(jiǎn)單?
return obj.method(val).compare(otherObj.method(otherVal));
還是這段代碼更簡(jiǎn)單?
var resultA = obj.method(val);
var resultB = otherObj.method(otherVal);
return resultA.compare(resultB);
從代碼行數(shù)來(lái)看,第一個(gè)示例似乎更簡(jiǎn)單,而且許多開發(fā)人員確實(shí)會(huì)更喜歡那樣。但這可能是一個(gè)錯(cuò)誤。注意,第一個(gè)示例在一行中包含了多個(gè)可能出現(xiàn)故障的地方,對(duì)象可能無(wú)效,或者三個(gè)方法中的任何一個(gè)都可能會(huì)失敗。如果真的出現(xiàn)了故障,我們可能無(wú)法清晰地判斷具體是哪一部分出了問(wèn)題。
此外,我們無(wú)法適當(dāng)?shù)赜涗浗Y(jié)果。我們無(wú)法輕易地對(duì)代碼進(jìn)行調(diào)試,因?yàn)檫@需要我們逐個(gè)進(jìn)入每一個(gè)方法進(jìn)行檢查。如果故障發(fā)生在方法內(nèi),堆棧跟蹤應(yīng)該會(huì)引導(dǎo)我們到正確的位置,甚至在第一個(gè)示例中也是如此。
試想一下,如果我們調(diào)用的方法改變了某個(gè)狀態(tài),那么obj.method(val)是否在otherObj.method(otherVal)之前被調(diào)用呢?
第二種方式寫代碼,可以很容易地看出方法的調(diào)用順序和結(jié)果。此外,我們還可以審查和記錄中間狀態(tài),即 resultA 和 resultB 的值。
我們來(lái)看一個(gè)常見的例子:
var result = list.stream()
.map(MyClass::convert)
.collect(Collectors.toList());
這是一段非常常見的代碼,與下面的代碼有相似之處:
var result = new ArrayList<OtherType>();
for(MyClass c: list) {
result.add(c.convert());
}
從可調(diào)試性的角度看,這兩種方法都有其優(yōu)點(diǎn),我們的選擇可能會(huì)對(duì)代碼的長(zhǎng)期質(zhì)量產(chǎn)生重大影響。第一個(gè)示例中的一個(gè)微妙之處在于,返回的列表是不可修改的。這既是優(yōu)點(diǎn)也是問(wèn)題。
當(dāng)我們?cè)噲D更改不可修改的列表時(shí),會(huì)在運(yùn)行時(shí)出現(xiàn)故障,這是一種潛在的風(fēng)險(xiǎn)。然而,故障的原因通常是明確的,我們可以清楚地知道什么導(dǎo)致了這個(gè)問(wèn)題。
如果我們對(duì)第二個(gè)示例中的列表結(jié)果進(jìn)行更改,可能會(huì)引發(fā)一系列問(wèn)題。然而,只要這些問(wèn)題在生產(chǎn)環(huán)境中沒(méi)有導(dǎo)致故障,我們就可以視為問(wèn)題已經(jīng)被解決。
那么,我們應(yīng)該選擇哪個(gè)呢?
通過(guò)只讀列表可以實(shí)現(xiàn)快速失敗的原則,這對(duì)我們調(diào)試生成問(wèn)題有幫助。當(dāng)快速失敗時(shí),我們降低了連鎖故障的可能性。這些可能是我們?cè)谏a(chǎn)環(huán)境中遇到的最糟糕的故障,因?yàn)閷?duì)應(yīng)用程序狀態(tài)的深入理解在生產(chǎn)環(huán)境中具有很高的復(fù)雜度。
在構(gòu)建大型應(yīng)用程序時(shí),我們經(jīng)常提到 "robust"(魯棒性)這個(gè)詞。系統(tǒng)應(yīng)該具有魯棒性,這種魯棒性應(yīng)該由代碼以外的部分提供。同時(shí),你的代碼本身應(yīng)該遵循快速失敗的原則。
一致性原則
在我之前關(guān)于日志記錄最佳實(shí)踐的分享中,我提到過(guò),無(wú)論在哪家曾服務(wù)過(guò)的公司,都有一套自己的代碼風(fēng)格指南,或者至少會(huì)遵循某種公認(rèn)的風(fēng)格。然而,很少有公司會(huì)有一份關(guān)于日志記錄的指南,告訴我們應(yīng)該在何處記錄日志,以及應(yīng)該記錄哪些內(nèi)容。
我們追求的一致性,實(shí)際上比代碼格式化更為重要。 在進(jìn)行調(diào)試的時(shí)候,我們需要知道應(yīng)該尋找什么。如果禁止使用某些特定的包,我期望這個(gè)規(guī)則能適用于整個(gè)代碼庫(kù)。 同樣,如果大家普遍不推薦某種編碼實(shí)踐,我期望這是一個(gè)共識(shí)。
幸運(yùn)的是,有了持續(xù)集成(CI),這些一致性規(guī)則可以輕易地得到執(zhí)行,而不會(huì)增加我們的代碼審查負(fù)擔(dān)。像 SonarQube 這樣的自動(dòng)化工具是可插拔的,并且可以通過(guò)自定義的檢測(cè)代碼進(jìn)行擴(kuò)展。我們可以調(diào)整這些工具,以強(qiáng)制執(zhí)行我們的一致性規(guī)則集,限制代碼中某個(gè)特定子集的使用,或者要求適當(dāng)?shù)娜罩居涗洝?/p>
當(dāng)然,每條規(guī)則都可能有例外。我們不應(yīng)受過(guò)于嚴(yán)格的規(guī)則束縛。這就是我們需要能夠調(diào)整這些工具,通過(guò)開發(fā)者審查來(lái)合并代碼變更的原因,這非常重要。
雙重驗(yàn)證
調(diào)試,其本質(zhì)是在我們發(fā)現(xiàn)并修復(fù) bug 的過(guò)程中對(duì)假設(shè)進(jìn)行驗(yàn)證。通常,這一過(guò)程進(jìn)行得相當(dāng)迅速:我們找到問(wèn)題所在,進(jìn)行驗(yàn)證,然后修復(fù)。但偶爾,我們可能會(huì)在追蹤某個(gè) bug 上花費(fèi)過(guò)多的時(shí)間,尤其是那些難以復(fù)現(xiàn)的 bug 或只在生產(chǎn)環(huán)境出現(xiàn)的 bug。
當(dāng)我們遇到難以解決的 bug,重要的是能夠后退一步反思,這通常表示我們可能需要重新審視我們的假設(shè)和驗(yàn)證方法。雙重驗(yàn)證的關(guān)鍵在于利用不同的方法來(lái)確認(rèn)假設(shè)的有效性,以保證我們的結(jié)論準(zhǔn)確無(wú)誤。
一般來(lái)說(shuō),我們需要驗(yàn)證 bug 存在的兩個(gè)方面。例如,假設(shè)后端存在一個(gè)問(wèn)題,而這個(gè)問(wèn)題會(huì)在前端呈現(xiàn)出錯(cuò)誤的數(shù)據(jù)。為了定位這個(gè) bug,我最初做出了兩個(gè)假設(shè):
- 前端準(zhǔn)確地顯示了后端的數(shù)據(jù)
- 數(shù)據(jù)庫(kù)查詢返回了正確的數(shù)據(jù)
為了驗(yàn)證這兩個(gè)假設(shè),我可能會(huì)打開瀏覽器查看數(shù)據(jù),使用網(wǎng)絡(luò)開發(fā)者工具檢查響應(yīng)來(lái)確保顯示的數(shù)據(jù)確實(shí)來(lái)自服務(wù)器查詢。對(duì)于后端,我可以直接發(fā)出數(shù)據(jù)庫(kù)查詢,確認(rèn)返回的數(shù)據(jù)是否正確。
但這僅僅是驗(yàn)證這些數(shù)據(jù)的一種方法。我們希望在理想情況下,有第二種驗(yàn)證方法。例如,如果緩存返回了錯(cuò)誤的數(shù)據(jù),又或者 SQL 查詢的前提假設(shè)錯(cuò)誤呢?
第二種驗(yàn)證方法應(yīng)盡可能與第一種方法不同,以防止犯下與第一種方法相同的錯(cuò)誤。對(duì)于前端代碼,我們可能會(huì)嘗試使用像 cURL 這樣的工具進(jìn)行驗(yàn)證。 使用像 cURL 這樣的工具進(jìn)行驗(yàn)證是一個(gè)好方法,我們應(yīng)該嘗試。但更好的方式可能是在服務(wù)器上查看記錄的數(shù)據(jù),或者調(diào)用支持前端的 WebService。
同樣,對(duì)于后端,我們希望能夠看到從應(yīng)用程序內(nèi)部返回的數(shù)據(jù)。這是可觀察性的一個(gè)核心概念。一個(gè)具有可觀察性的系統(tǒng),就是我們可以對(duì)其提出問(wèn)題并得到答案的系統(tǒng)。在開發(fā)過(guò)程中,我們應(yīng)通過(guò)兩種不同的方式來(lái)提高我們的系統(tǒng)可觀察性,以便更好地回答問(wèn)題。
為何避免使用三種以上驗(yàn)證方式?
我們通常不采用超過(guò)兩種驗(yàn)證方式,這是因?yàn)檫^(guò)度驗(yàn)證會(huì)導(dǎo)致我們的成本增加,性能降低。我們需要將收集的信息量限制在合理的范圍內(nèi),尤其在收集信息的過(guò)程中,必須重視個(gè)人信息保護(hù)的風(fēng)險(xiǎn),這是我們必須重視的關(guān)鍵因素。
可觀察性往往根據(jù)其使用的工具,柱狀指標(biāo)或者某些明顯的特征來(lái)定義,但這其實(shí)是錯(cuò)誤的。可觀察性應(yīng)當(dāng)根據(jù)它提供的訪問(wèn)權(quán)限來(lái)定義。我們決定記錄什么,監(jiān)控什么,決定追蹤的范圍,決定信息的粒度,以及決定是否希望部署開發(fā)者的可觀察性工具。
我們需要保證我們的生產(chǎn)系統(tǒng)能夠得到適當(dāng)?shù)谋O(jiān)控。為此,我們需要進(jìn)行故障注入,可能還需要安排混沌工程的執(zhí)行。在運(yùn)行這樣的場(chǎng)景時(shí),我們需要考慮如何解決出現(xiàn)的問(wèn)題。我們能對(duì)系統(tǒng)提出哪些問(wèn)題?我們?nèi)绾位卮疬@些問(wèn)題?
例如,當(dāng)特定問(wèn)題出現(xiàn)時(shí),我們通常關(guān)心有多少用戶的操作正在實(shí)時(shí)影響系統(tǒng)數(shù)據(jù)。因此,我們可以為這個(gè)信息添加一個(gè)度量。
通過(guò)特性標(biāo)志進(jìn)行驗(yàn)證
雖然我們可以使用可觀察性工具來(lái)驗(yàn)證假設(shè),但我們也可以使用一些更具創(chuàng)新性的驗(yàn)證工具。其中,一個(gè)意想不到的工具就是特性標(biāo)志系統(tǒng)。特性標(biāo)志解決方案通??梢苑浅<?xì)粒度地操作,例如我們可以只為特定用戶關(guān)閉或改變一個(gè)特性的設(shè)置等。
這種功能非常強(qiáng)大。如果特定的代碼被封裝在一個(gè)標(biāo)志中,我們就可以通過(guò)切換一個(gè)特性來(lái)為我們提供對(duì)特定行為的驗(yàn)證。我并不建議在所有代碼中都使用特性標(biāo)志,但是能夠拉動(dòng)開關(guān)并在生產(chǎn)環(huán)境中改變系統(tǒng)是一種強(qiáng)大的調(diào)試工具,這種工具的威力常常被低估。
bug 分析會(huì)議
我在 90 年代開發(fā)過(guò)飛行模擬器,期間有幸與許多戰(zhàn)斗機(jī)飛行員合作。我從他們那里了解到了"分析會(huì)議"這個(gè)概念。我之前只把這樣的會(huì)議當(dāng)作任務(wù)失敗后的討論,然而戰(zhàn)斗機(jī)飛行員無(wú)論任務(wù)成功還是失敗,都會(huì)在任務(wù)結(jié)束后立刻進(jìn)行這樣的會(huì)議。
我們可以從中學(xué)習(xí)以下重要的要點(diǎn):
- 即時(shí)性 - 我們需要在事件剛發(fā)生不久的時(shí)候討論這些信息。如果等待過(guò)久,部分信息可能會(huì)丟失,而我們的記憶也會(huì)發(fā)生明顯的改變。
- 成功與失敗 - 每次任務(wù)都包含成功與失敗的元素。我們需要理解在哪些方面做對(duì)了,哪些方面做錯(cuò)了,尤其是在任務(wù)成功的情況下。
解決了一個(gè) bug 之后,我們通常只想把這件事了結(jié),不再討論它。即使我們想要"炫耀",也通常只是對(duì)追蹤過(guò)程的模糊記憶。通過(guò)開放的討論,我們對(duì)做對(duì)的事情和做錯(cuò)的事情沒(méi)有任何評(píng)判,我們可以了解到我們的現(xiàn)狀。這些信息可以幫助我們?cè)谧粉檰?wèn)題時(shí)提升我們的效果。
這樣的分析會(huì)議能幫我們發(fā)現(xiàn)在可觀測(cè)數(shù)據(jù)中的缺失、不一致性以及問(wèn)題流程。在許多團(tuán)隊(duì)中,流程是一個(gè)常見的問(wèn)題。通常,當(dāng)出現(xiàn)問(wèn)題時(shí),它的解決過(guò)程是這樣的:
- 客戶發(fā)現(xiàn)問(wèn)題
- 向支持部門報(bào)告問(wèn)題
- 運(yùn)維團(tuán)隊(duì)進(jìn)行檢查
- 問(wèn)題轉(zhuǎn)交給研發(fā)部門
如果你在研發(fā)部門,你離客戶隔了 4 層,而你接收到的問(wèn)題可能不包含你需要的所有信息。盡管優(yōu)化這些流程并不涉及到代碼修改,但我們可以在代碼中添加工具,以便更輕松地定位問(wèn)題。一種常見的做法是為每個(gè)異常對(duì)象添加一個(gè)唯一的鍵。在出現(xiàn)故障的情況下,這將呈現(xiàn)到用戶界面。
當(dāng)客戶報(bào)告問(wèn)題時(shí),他們可能會(huì)包含這個(gè)錯(cuò)誤碼,方便研發(fā)部門通過(guò)日志快速定位錯(cuò)誤。這些都是通過(guò)這樣的分析會(huì)議,我們可以發(fā)現(xiàn)并優(yōu)化流程的方法。
審閱有效的日志與儀表板
僅僅等待失敗的發(fā)生并非是解決問(wèn)題的正確方式。我們需要定期查閱日志和儀表板,以便于追蹤可能已經(jīng)存在但未曾顯露出來(lái)的 bug,并能建立正常運(yùn)行狀態(tài)的基準(zhǔn)。健康的儀表板或者日志應(yīng)該呈現(xiàn)出怎樣的狀態(tài)。
在日志中,我們可能會(huì)遇到一些錯(cuò)誤信息。如果我們?cè)谧粉?bug 的過(guò)程中,花費(fèi)時(shí)間去查看那些并無(wú)實(shí)質(zhì)性影響的錯(cuò)誤,那我們就是在浪費(fèi)時(shí)間。理想情況下,我們需要盡可能地減少這些錯(cuò)誤,因?yàn)樗鼈儠?huì)使得日志的閱讀變得困難。但是在服務(wù)器開發(fā)中的現(xiàn)實(shí)狀況是,我們不能總是做到這一點(diǎn)。不過(guò),我們可以通過(guò)對(duì)源代碼的深入理解和適當(dāng)?shù)淖⑨專瑏?lái)降低這方面的時(shí)間開銷。
結(jié)語(yǔ)
創(chuàng)建 Codename One 數(shù)年后,我們的 Google App Engine 賬單突然飆升,甚至在幾天內(nèi)就會(huì)導(dǎo)致公司的破產(chǎn)。這是由于他們后端系統(tǒng)的一次更新,引發(fā)了一個(gè)突發(fā)性的回歸問(wèn)題。
這個(gè)問(wèn)題的源頭在于未被緩存的數(shù)據(jù),但是由于當(dāng)時(shí) App Engine 的運(yùn)行方式,我們無(wú)法定位到代碼中哪一個(gè)具體的區(qū)域觸發(fā)了這個(gè)問(wèn)題。我們無(wú)法直接調(diào)試這個(gè)問(wèn)題,只能嘗試通過(guò)更新服務(wù)器并觀察其運(yùn)行情況來(lái)判斷問(wèn)題是否得到了解決。
幸運(yùn)的是,我們當(dāng)時(shí)解決了這個(gè)問(wèn)題。我們對(duì)所有可能的部分盡可能進(jìn)行了緩存處理。直到今天,我仍然不清楚是什么觸發(fā)了這個(gè)問(wèn)題,也不知道我們做了什么修復(fù)了問(wèn)題。
但是我知道的是:
選擇使用"App Engine"是我當(dāng)時(shí)的一個(gè)決策失誤。它未能提供足夠的可觀察性,并留下了關(guān)鍵的盲點(diǎn)。如果我在部署前花時(shí)間對(duì)可觀察性能力進(jìn)行審查,我就能夠察覺這點(diǎn)。選擇使用"App Engine"是我當(dāng)時(shí)的一個(gè)決策失誤。
譯者介紹
劉汪洋,51CTO社區(qū)編輯,昵稱:明明如月,一個(gè)擁有 5 年開發(fā)經(jīng)驗(yàn)的某大廠高級(jí) Java 工程師,擁有多個(gè)主流技術(shù)博客平臺(tái)博客專家稱號(hào)。
原文標(biāo)題:Building for Failure: Best Practices for Easy Production Debugging,作者:Shai Almog