事故復(fù)盤:從并發(fā)超賣到系統(tǒng)雪崩的避坑指南)
1. 從一次夜間緊急工單說起做Java開發(fā)這幾年最怕的不是需求復(fù)雜也不是工期緊張而是半夜手機(jī)震動(dòng)——線上報(bào)警了。有一次我印象特別深凌晨兩點(diǎn)多某商城系統(tǒng)的下單接口突然大面積超時(shí)緊接著庫存數(shù)據(jù)出現(xiàn)負(fù)數(shù)用戶瘋狂下單系統(tǒng)直接被打穿。事后復(fù)盤發(fā)現(xiàn)問題根源居然只是一個(gè)很不起眼的并發(fā)控制缺失庫存扣減用的是先查詢?cè)俑碌倪壿嫑]有加鎖也沒有做樂觀鎖控制多個(gè)請(qǐng)求同時(shí)讀到同一個(gè)庫存值各自扣減后寫回于是出現(xiàn)了嚴(yán)重超賣。那次事故直接導(dǎo)致故障時(shí)長近一小時(shí)損失難以估量。但也正是從那次開始我養(yǎng)成了一個(gè)習(xí)慣每遇到一起線上事故不管大小都會(huì)認(rèn)真做一次復(fù)盤把根因、修復(fù)過程、預(yù)防措施完整記錄下來。這篇文章就是基于我這些年積累的編碼生產(chǎn)事故總結(jié)復(fù)盤經(jīng)驗(yàn)整理成一份可以反復(fù)參考的資料。如果你也是一名Java開發(fā)尤其在做電商、交易、支付這類高并發(fā)場景的系統(tǒng)這篇文章應(yīng)該能幫你少踩不少坑。我不會(huì)去講那些教科書上早就寫過的理論而是把真實(shí)遇到過的場景、當(dāng)時(shí)怎么排查、問題出在哪里、代碼層面怎么改、流程層面怎么堵一五一十說清楚。很多內(nèi)容是我個(gè)人踩坑后的沉淀不一定適合所有團(tuán)隊(duì)但至少代表了一條被驗(yàn)證過的路。2. 編碼生產(chǎn)事故的主要形態(tài)與根因分析生產(chǎn)事故千奇百怪但把根子刨出來看Java后端領(lǐng)域的事故大多集中在幾個(gè)點(diǎn)上。我先把這幾年見過、經(jīng)歷過的典型類型梳理一遍再逐一展開講案例。事故類型典型現(xiàn)象根因走向并發(fā)控制缺失超賣、重復(fù)扣款、數(shù)據(jù)被覆蓋鎖粒度、事務(wù)隔離級(jí)別、冪等設(shè)計(jì)空指針與數(shù)據(jù)異常接口500、NullPointerException刷屏下游數(shù)據(jù)為空、參數(shù)校驗(yàn)缺失事務(wù)邊界錯(cuò)位部分成功部分失敗、數(shù)據(jù)不一致事務(wù)方法自調(diào)用、異常被吞異步與線程池問題消息積壓、線程池拒絕、狀態(tài)丟失異步鏈路未做補(bǔ)償、參數(shù)未傳全內(nèi)存與性能問題頻繁Full GC、OOM、接口RT飆升大對(duì)象、集合無界、慢SQL2.1 并發(fā)控制缺失從超賣事故講到鎖的選型先說我開頭提到的那個(gè)超賣案例。當(dāng)時(shí)的下單接口代碼大致長這樣public boolean createOrder(Long userId, Long skuId, Integer num) { // 1. 查庫存 Stock stock stockMapper.selectBySkuId(skuId); // 2. 判斷庫存是否足夠 if (stock.getAvailable() num) { return false; } // 3. 扣減庫存 stock.setAvailable(stock.getAvailable() - num); stockMapper.updateById(stock); // 4. 創(chuàng)建訂單 orderMapper.insert(Order.build(userId, skuId, num)); return true; }這段代碼在低并發(fā)下完全沒問題但一旦遇到秒殺或者大促問題立刻暴露。兩個(gè)線程同時(shí)執(zhí)行到第二步讀到的可用庫存都是10都認(rèn)為可以扣5分別更新成5寫回實(shí)際賣出了10件而庫存只剩5件系統(tǒng)還能繼續(xù)往下賣直到庫存變負(fù)數(shù)為止。這類問題的解法業(yè)內(nèi)已經(jīng)比較成熟了無非幾種樂觀鎖方式在更新SQL里加上庫存充足條件UPDATE stock SET available available - #{num} WHERE sku_id #{skuId} AND available #{num}悲觀鎖方式查詢時(shí)加上行鎖。分布式鎖方式適合跨服務(wù)的庫存扣減。我用得最多的是第一種因?yàn)閷?shí)現(xiàn)簡單、性能也好。實(shí)際工作中要注意很多人以為加上synchronized就萬事大吉了但如果是多實(shí)例部署synchronized只能鎖住本機(jī)JVM內(nèi)的線程跨節(jié)點(diǎn)依然會(huì)有問題。這點(diǎn)新手特別容易踩。后來我還總結(jié)出一條經(jīng)驗(yàn)**凡是涉及金額、庫存、狀態(tài)流轉(zhuǎn)的寫操作必須在設(shè)計(jì)階段就把并發(fā)模型畫出來而不是等上線出了事再去補(bǔ)鎖。**并發(fā)問題的修復(fù)成本永遠(yuǎn)比預(yù)防成本高一個(gè)數(shù)量級(jí)。2.2 空指針與數(shù)據(jù)異常那些被“看似不會(huì)為空”的數(shù)據(jù)坑慘的時(shí)刻如果說并發(fā)問題還分場景那空指針就是天天見的常客。有一次我們的用戶信息接口突然在某個(gè)渠道報(bào)錯(cuò)排查發(fā)現(xiàn)是上游服務(wù)返回的用戶擴(kuò)展字段里某個(gè)屬性為null而我們?cè)诖a里直接調(diào)用了該屬性的方法。這種問題在Java里實(shí)在太多了尤其是從接口拿數(shù)據(jù)時(shí)對(duì)方的文檔寫著“非空”但實(shí)際上某些歷史數(shù)據(jù)就是不滿足約束。我當(dāng)時(shí)給團(tuán)隊(duì)立了一條規(guī)矩**除了基礎(chǔ)類型和明確由自己寫入且必填的字段其他所有外部數(shù)據(jù)都要按可能為null來處理。**這不是代碼潔癖是對(duì)系統(tǒng)邊界保持敬畏。處理空指針的方法很多我用得最多的組合是方法入口用Objects.requireNonNull或者提前校驗(yàn)。傳輸對(duì)象用Optional做顯式處理不要無腦get()。集合操作前判斷空集合Collections.emptyList()兜底。參數(shù)校驗(yàn)落地到注解上比如NotNull、NotBlank在Controller層就把問題擋住。數(shù)據(jù)異常類的事故就更隱蔽了。比如前一陣我們做數(shù)據(jù)訂正腳本把一批訂單的金額字段從字符串轉(zhuǎn)成了整數(shù)結(jié)果有個(gè)別訂單因?yàn)榘诵?shù)點(diǎn)后兩位以外的特殊字符轉(zhuǎn)換直接拋異常腳本中斷。這種看起來不影響線上接口的事故往往會(huì)造成更大的數(shù)據(jù)污染。我現(xiàn)在的習(xí)慣是凡是批量更新、數(shù)據(jù)訂正、遷移腳本先加試跑模式只輸出結(jié)果不落庫核對(duì)無誤后再全量執(zhí)行而且在執(zhí)行前必須手動(dòng)備份相關(guān)表。2.3 事務(wù)邊界錯(cuò)位一個(gè)Transactional引發(fā)的連環(huán)事故事務(wù)是Java后端最常用的能力之一但也是最容易被誤用的。我遇到過一個(gè)特別典型的案例某個(gè)訂單回調(diào)服務(wù)里開發(fā)同學(xué)在方法上加了Transactional方法內(nèi)部先更新訂單狀態(tài)再調(diào)用遠(yuǎn)程接口通知倉庫發(fā)貨最后更新一張積分表。整個(gè)方法跑完大約需要3秒期間事務(wù)一直持有數(shù)據(jù)庫連接。問題出在遠(yuǎn)程調(diào)用如果超時(shí)事務(wù)會(huì)長時(shí)間掛起連接池很快被耗盡其他請(qǐng)求全部排隊(duì)等待最終導(dǎo)致服務(wù)雪崩。事務(wù)里做遠(yuǎn)程調(diào)用這件事我一直強(qiáng)調(diào)嚴(yán)格控制事務(wù)邊界絕不在事務(wù)內(nèi)做耗時(shí)操作。另一個(gè)坑是事務(wù)自調(diào)用。同一個(gè)類里A方法調(diào)用B方法B上有Transactional但Spring的AOP基于代理自調(diào)用不會(huì)觸發(fā)代理邏輯事務(wù)注解直接失效。很多團(tuán)隊(duì)因此專門用Autowired注入自身來繞開或者把被調(diào)方法拆到另一個(gè)Service里。后來我還在Code Review清單里加了一條自查項(xiàng)檢查所有事務(wù)注解是否真正生效。還有一類事務(wù)問題是異常被吞。方法上聲明了Transactional(rollbackFor Exception.class)但內(nèi)部catch了異常又沒往外拋事務(wù)照樣不回滾。數(shù)據(jù)已經(jīng)寫了一半接口返回成功下游卻看到的是半成品數(shù)據(jù)?,F(xiàn)在我對(duì)團(tuán)隊(duì)的規(guī)范是事務(wù)方法內(nèi)部盡量不要自己catch異常要catch就處理后重新拋出至少要區(qū)分哪些異常必須回滾、哪些可以忽略。2.4 異步處理與線程池場景下的隱患異步化是提升接口響應(yīng)速度的利器但異步鏈路里的坑一點(diǎn)也不少。第一個(gè)坑是線程池參數(shù)拍腦袋定。有一次我們把一個(gè)異步推送任務(wù)放到了一個(gè)無界的線程池里高峰時(shí)任務(wù)排隊(duì)上百萬個(gè)內(nèi)存直接飆升到接近堆上限頻繁Full GC。后來改成有界隊(duì)列、自定義拒絕策略才穩(wěn)定下來。線程池的核心參數(shù)并沒有萬能公式但至少要根據(jù)任務(wù)類型估算。比如任務(wù)平均執(zhí)行時(shí)間是50ms期望吞吐是每秒200個(gè)任務(wù)那核心線程數(shù)至少要讓處理能力大于流入速度否則隊(duì)列只會(huì)越積越長。我們?cè)谏a(chǎn)環(huán)境的做法是核心線程數(shù)和最大線程數(shù)壓到合理范圍內(nèi)隊(duì)列用有界隊(duì)列拒絕策略選擇CallerRunsPolicy讓提交任務(wù)的線程自己跑這樣不會(huì)丟任務(wù)也起到了天然限流的作用。還有一個(gè)經(jīng)典問題異步線程里的上下文丟失。比如在普通請(qǐng)求線程里設(shè)置了一個(gè)追蹤ID丟到異步線程池執(zhí)行時(shí)追蹤ID就拿不到了排查問題的時(shí)候日志全對(duì)不上。我們后期統(tǒng)一通過TransmittableThreadLocal解決沒有用原生的ThreadLocal。異步場景另一個(gè)高頻事故是漏傳參數(shù)或者丟失主鍵。有一次我們把某個(gè)通知任務(wù)異步化只傳了訂單號(hào)過去異步線程里根據(jù)訂單號(hào)查詢時(shí)訂單狀態(tài)已經(jīng)變了導(dǎo)致通知內(nèi)容發(fā)錯(cuò)。這個(gè)問題的本質(zhì)是異步執(zhí)行時(shí)的數(shù)據(jù)快照和提交時(shí)的數(shù)據(jù)快照可能完全不同依賴實(shí)時(shí)查詢的結(jié)果去做業(yè)務(wù)判斷要特別小心。2.5 內(nèi)存與性能方面的生產(chǎn)事故內(nèi)存和性能事故比較隱蔽往往不是一次性爆發(fā)的而是積少成多。我舉一個(gè)例子有個(gè)報(bào)表導(dǎo)出功能一次性從數(shù)據(jù)庫查了幾十萬條記錄放進(jìn)了List然后逐個(gè)處理每個(gè)對(duì)象還保留著一個(gè)很大的JSON字段。幾個(gè)用戶同時(shí)操作內(nèi)存立刻吃緊Full GC頻繁整個(gè)服務(wù)RT翻倍其他接口跟著一起遭殃。這個(gè)問題的本質(zhì)是大對(duì)象駐留老年代解決思路就兩個(gè)字分頁或者流式處理。數(shù)據(jù)庫端用游標(biāo)讀取應(yīng)用端處理一批就釋放一批千萬不能把全量數(shù)據(jù)一次性load到內(nèi)存里。性能類的生產(chǎn)事故里慢SQL也是大頭。最常見的情況是查詢條件里用了函數(shù)計(jì)算導(dǎo)致索引失效。比如WHERE DATE_FORMAT(create_time, %Y-%m-%d) 2024-01-01這種寫法一定全表掃描。正確的做法是算好時(shí)間范圍再傳參WHERE create_time ? AND create_time ?。我在復(fù)盤這些性能事故的時(shí)候越來越覺得**性能問題不是什么高深莫測的東西大多數(shù)時(shí)候就是數(shù)據(jù)的訪問模式錯(cuò)了。**只要對(duì)數(shù)據(jù)量有感知對(duì)索引的使用有常識(shí)就能避免絕大部分問題。3. 事故復(fù)盤方法論與排查實(shí)操遇到生產(chǎn)事故最怕的是慌亂。我自己的排查流程已經(jīng)固定下來了不管什么事故都按這套思路走效率會(huì)高很多。第一步看監(jiān)控、看日志、定性。先把影響范圍搞清楚是全部用戶掛還是部分接口掛是持續(xù)掛還是間歇性掛。這一步?jīng)Q定了接下來的排查方向。第二步看最近變更。很多事故不是突然出現(xiàn)的而是某個(gè)發(fā)布或者配置改動(dòng)觸發(fā)的。排查變更比從代碼海洋里撈針快得多。我們團(tuán)隊(duì)已經(jīng)形成習(xí)慣每次發(fā)布都必須有變更記錄否則出了事故連方向都找不到。第三步定位根因。這一步要用到日志、線程棧、堆轉(zhuǎn)儲(chǔ)、APM鏈路追蹤等手段。第四步驗(yàn)證并修復(fù)。修復(fù)不能用“試試看”的心態(tài)必須能解釋為什么這么改是對(duì)的最好有壓測或者回放作為佐證。第五步沉淀復(fù)盤報(bào)告。這一步很多人忽略但恰恰是最重要的。3.1 第一時(shí)間定位問題的思路我舉個(gè)例子。有一次線上接口突然超時(shí)我第一反應(yīng)不是打開IDE看代碼而是先看監(jiān)控面板的P95和P99。發(fā)現(xiàn)P99飆升但P50正常?;究梢詳喽ú皇钦w負(fù)載問題而是少數(shù)慢請(qǐng)求或者某條特殊路徑存在問題。再往下看發(fā)現(xiàn)超時(shí)請(qǐng)求集中在某幾個(gè)商品SKU而這些SKU有一個(gè)共同特點(diǎn)積壓的庫存記錄特別多。再結(jié)合數(shù)據(jù)庫慢查詢?nèi)罩景l(fā)現(xiàn)對(duì)庫存表的分頁查詢走了全表掃描因?yàn)椴樵儣l件里store_id沒有索引。整個(gè)過程大概花了二十分鐘如果從代碼開始看可能兩小時(shí)都未必找得到。這里我要強(qiáng)調(diào)一點(diǎn)**排查事故時(shí)要克制住“憑經(jīng)驗(yàn)猜”的沖動(dòng)。**我見過不少老開發(fā)一看超時(shí)就說“是不是GC了”上去先抓堆棧折騰半天發(fā)現(xiàn)根本不是。正確做法是用數(shù)據(jù)說話用監(jiān)控面板一步步縮小范圍。一線排查的常用命令我也會(huì)整理成一個(gè)速查表場景命令/操作作用查看JVM堆使用jstat -gcutil pid 1000觀察GC頻率、內(nèi)存余量查看線程狀態(tài)jstack pid thread.txt分析死鎖、線程阻塞抓取堆轉(zhuǎn)儲(chǔ)jmap -dump:formatb,fileheap.hprof pid分析OOM、大對(duì)象查詢慢SQL開啟慢日志并分析定位SQL性能問題鏈路追蹤根據(jù)traceId串聯(lián)日志定位跨服務(wù)耗時(shí)3.2 日志與監(jiān)控如何幫助我們復(fù)盤做事故復(fù)盤最怕的是沒有數(shù)據(jù)全靠記憶。所以日志記錄這件事真到了復(fù)盤時(shí)才會(huì)發(fā)現(xiàn)有多重要。我之前踩過一個(gè)坑生產(chǎn)環(huán)境某個(gè)對(duì)象的序列化日志被打得鋪天蓋地一天幾個(gè)GB真正需要追蹤的業(yè)務(wù)字段卻沒有一個(gè)。后來我主導(dǎo)了一次日志規(guī)范梳理才把日志從“能跑就行”提升到“可觀測”的水平。日志規(guī)范里核心就幾條關(guān)鍵業(yè)務(wù)操作必須記錄入?yún)ⅰ⒊鰠?、耗時(shí)、traceId。日志級(jí)別要有明確規(guī)范debug不進(jìn)生產(chǎn)info記錄業(yè)務(wù)關(guān)鍵節(jié)點(diǎn)error記錄異常和失敗路徑。禁止在循環(huán)里打日志尤其禁止在大集合的循環(huán)里打。敏感信息脫敏不能把手機(jī)號(hào)、身份證明文打進(jìn)日志。監(jiān)控方面我們現(xiàn)在做到了三層基礎(chǔ)層監(jiān)控CPU、內(nèi)存、磁盤、網(wǎng)絡(luò)應(yīng)用層監(jiān)控QPS、RT、錯(cuò)誤率、線程池活躍度業(yè)務(wù)層監(jiān)控訂單量、支付成功率、庫存扣減次數(shù)這種和用戶感受直接相關(guān)的指標(biāo)。有了這三層事故的定位速度會(huì)快非常多。3.3 從現(xiàn)象到根因的推理過程復(fù)盤文檔里我最看重的一個(gè)環(huán)節(jié)就是從現(xiàn)象到根因的推理過程而不是最終結(jié)論。因?yàn)橹挥邪淹评礞湆懗鰜聿拍茯?yàn)證當(dāng)時(shí)是不是有邏輯跳躍才能讓后人學(xué)習(xí)到完整的思路。舉一個(gè)之前遇到的事故?,F(xiàn)象是某天晚上整體訂單量無明顯增長但支付回調(diào)接口的錯(cuò)誤率突然上升持續(xù)了大概十分鐘后自行恢復(fù)。如果只看結(jié)論可能就寫一句“上游服務(wù)抖動(dòng)導(dǎo)致的偶發(fā)失敗”。但我們深入推理后發(fā)現(xiàn)錯(cuò)誤率上升的時(shí)間點(diǎn)正好和某臺(tái)Tomcat節(jié)點(diǎn)被重啟的時(shí)間吻合——那臺(tái)節(jié)點(diǎn)重啟是因?yàn)榻】禉z查失敗被自動(dòng)摘除了。健康檢查失敗的原因是JVM發(fā)生了一次長達(dá)20秒的Full GC而Full GC的誘因是前一天的灰度發(fā)布增加了對(duì)象緩存的大小。所以整條鏈路是灰度發(fā)布加緩存 → 內(nèi)存占用升高 → 觸發(fā)Full GC → 節(jié)點(diǎn)對(duì)健康檢查無響應(yīng) → 被摘除 → 流量重新分配到其他節(jié)點(diǎn) → 其他節(jié)點(diǎn)壓力短暫升高 → 支付回調(diào)偶發(fā)超時(shí) → 客戶端重試反饋到錯(cuò)誤率上。如果只停留在“上游抖動(dòng)”這件事永遠(yuǎn)不可能真正解決。也正因?yàn)槲覀儓?jiān)持把推理鏈挖到最底層才發(fā)現(xiàn)真正需要優(yōu)化的是對(duì)象緩存的過期策略和堆內(nèi)存參數(shù)。這種復(fù)盤方式讓我對(duì)系統(tǒng)運(yùn)行的理解上升了不止一個(gè)層次。4. 如何從制度與習(xí)慣上減少編碼生產(chǎn)事故事故復(fù)盤再多如果只是寫文檔那也只是一堆文字。真正有效的復(fù)盤一定要落在制度建設(shè)和習(xí)慣改變上。4.1 代碼評(píng)審與靜態(tài)檢查的落地很多人覺得代碼評(píng)審走形式但我認(rèn)為**評(píng)審是編碼生產(chǎn)事故的第一道防線。**問題在于很多團(tuán)隊(duì)的評(píng)審只關(guān)注“代碼能不能跑”“功能對(duì)不對(duì)”很少關(guān)注“會(huì)不會(huì)掛”。我建議評(píng)審時(shí)至少增加這幾個(gè)視角并發(fā)安全視角這段代碼有沒有可能被多個(gè)線程同時(shí)執(zhí)行覆蓋的共享資源是什么邊界視角傳入?yún)?shù)有沒有可能為null集合有沒有可能為空數(shù)據(jù)量有沒有可能超出預(yù)期依賴視角這個(gè)方法依賴的外部服務(wù)不可用時(shí)系統(tǒng)會(huì)怎樣生命周期視角臨時(shí)文件、線程、連接這些資源是否都會(huì)釋放靜態(tài)檢查工具也是必須的比如SonarQube、SpotBugs、Alibaba Java Coding Guidelines插件。但要記住工具只能抓明顯的壞味道抓不了設(shè)計(jì)層面的問題。真正有價(jià)值的還是人。后來我們團(tuán)隊(duì)要求所有代碼合并前必須有至少一個(gè)評(píng)審人認(rèn)真過一遍不是點(diǎn)個(gè)“通過”按鈕那種而是要真的理解改動(dòng)邏輯。4.2 測試策略與灰度發(fā)布編碼事故里很多都是可以通過測試提前發(fā)現(xiàn)的。我們現(xiàn)在的分支覆蓋率要求可能不算高但核心鏈路的場景測試覆蓋得很全。什么叫核心鏈路就是用戶下單、支付回調(diào)、退款、庫存扣減、對(duì)賬這些每一個(gè)都要有正反向用例。測試環(huán)境的問題是數(shù)據(jù)量小、不會(huì)觸發(fā)并發(fā)和性能隱患所以單測過了不代表生產(chǎn)安全。我比較推崇的是把壓測常態(tài)化。團(tuán)隊(duì)每隔一個(gè)迭代做一次核心接口的壓測不需要極其精確但至少能知道當(dāng)前接口的瓶頸在哪里。很多線上超時(shí)事故其實(shí)在壓測階段就該暴露了。灰度發(fā)布更是救命稻草。我們現(xiàn)在的發(fā)布方式是先金絲雀節(jié)點(diǎn)觀察幾分鐘沒有異常再全量。而且灰度期間要重點(diǎn)盯業(yè)務(wù)指標(biāo)不只是CPU、內(nèi)存這些技術(shù)指標(biāo)。有一次新版本發(fā)布后接口技術(shù)上一切正常但訂單轉(zhuǎn)化率掉了1.5%灰度監(jiān)控立刻把它抓了出來一查發(fā)現(xiàn)是前端頁面的某個(gè)控件被錯(cuò)誤地挪了位置。技術(shù)指標(biāo)正常不代表業(yè)務(wù)正常這條經(jīng)驗(yàn)很重要。4.3 團(tuán)隊(duì)層面的復(fù)盤與知識(shí)沉淀團(tuán)隊(duì)例行的事故復(fù)盤會(huì)一定要開成“學(xué)習(xí)會(huì)”而不是“追責(zé)會(huì)”。一旦追責(zé)所有人都會(huì)選擇隱瞞和掩藏事故信息就會(huì)丟失。我們的原則很簡單**只談技術(shù)根因不談責(zé)任人。**所有代碼都是團(tuán)隊(duì)資產(chǎn)別人寫的爛代碼如果你沒有在評(píng)審時(shí)攔住那這個(gè)責(zé)任是共同的。復(fù)盤會(huì)的流程通常是這樣的事故當(dāng)事人講時(shí)間線和現(xiàn)象。大家一起剖析根因允許反駁和補(bǔ)充。得出行動(dòng)計(jì)劃責(zé)任到人給截止時(shí)間。把復(fù)盤內(nèi)容歸檔到團(tuán)隊(duì)知識(shí)庫并且同步到新人學(xué)習(xí)資料里。我還特別建議做“事故案例庫”。每一起事故不論大小都記錄在一個(gè)共享文檔里標(biāo)注清楚標(biāo)題、現(xiàn)象、根因、影響、修復(fù)方案、預(yù)防措施。新人入職的時(shí)候不要先看規(guī)范文檔先看事故案例庫這比任何培訓(xùn)都有效。因?yàn)橐?guī)范講的是“應(yīng)該怎么做”事故案例講的是“不這么做會(huì)有什么后果”后者才能真正讓人記住。5. 我個(gè)人的幾條心得與建議講完這么多最后再說幾條比較零散、但確實(shí)是我工作里反復(fù)驗(yàn)證過的經(jīng)驗(yàn)吧。第一**寫代碼前先想一想失敗的情況。**很多事故的根源是開發(fā)默認(rèn)“所有事都會(huì)成功”。但真實(shí)生產(chǎn)環(huán)境里下游服務(wù)會(huì)超時(shí)數(shù)據(jù)庫連接會(huì)斷緩存會(huì)失效消息會(huì)丟失。寫代碼的時(shí)候把異常路徑和正常路徑同等對(duì)待很多事故從一開始就不會(huì)存在。第二**變更比故障更值得關(guān)注。**生產(chǎn)事故絕大多數(shù)是由某個(gè)變更觸發(fā)的。所以做任何變更前都要想一想影響范圍列一個(gè)回滾方案。發(fā)布后盯著看一會(huì)兒再離開工位。不用多十五分鐘就好往往能攔住大部分問題。第三**不要迷信“測試環(huán)境能測出來”。**生產(chǎn)環(huán)境的數(shù)據(jù)量、并發(fā)量、網(wǎng)絡(luò)狀況測試環(huán)境永遠(yuǎn)模擬不出來。所以線上監(jiān)控和日志必須從一開始就做好那是你唯一能在生產(chǎn)環(huán)境看清系統(tǒng)的眼睛。第四**復(fù)盤報(bào)告不是寫給領(lǐng)導(dǎo)看的是寫給自己和下一個(gè)人看的。**哪怕沒有人要求你寫出了事故我也會(huì)自己寫一份哪怕只是幾百字的備忘錄下次遇到類似問題翻出來看一眼能省幾個(gè)小時(shí)。做Java開發(fā)越久我越覺得真正拉開差距的往往不是框架用得有多熟新特性背得有多牢而是面對(duì)復(fù)雜系統(tǒng)時(shí)那種“下意識(shí)地敬畏”敬畏數(shù)據(jù)敬畏并發(fā)敬畏每一個(gè)可能為空的外部依賴。一次生產(chǎn)事故是一次很好的學(xué)習(xí)機(jī)會(huì)關(guān)鍵看你能不能把機(jī)會(huì)變成經(jīng)驗(yàn)把經(jīng)驗(yàn)變成制度把制度變成習(xí)慣。希望這篇文章里記錄的那些坑和思路能幫你在碼代碼的路上少翻幾次車。