量保障不靠運(yùn)氣:Impeccable項目構(gòu)建可控交付鏈路)
Impeccable 是我在團(tuán)隊內(nèi)部發(fā)起的一個質(zhì)量改進(jìn)項目的代號中文直譯過來就是“無可挑剔”。當(dāng)初起這個名字的時候我就知道它早晚會被現(xiàn)實(shí)打臉——因?yàn)闆]有任何系統(tǒng)可以做到真正意義上的零缺陷但這并不妨礙我們把它當(dāng)成一個方向去逼近。這個項目要解決的問題也很樸素怎么讓每一次代碼提交、每一次發(fā)布都不再靠個別同事的責(zé)任心和運(yùn)氣而是靠一套機(jī)制穩(wěn)穩(wěn)托住。如果你正被線上事故反復(fù)折騰或者被“測試環(huán)境好好的一上生產(chǎn)就翻車”的魔咒困擾又或者只是想讓自己的交付結(jié)果更靠譜一點(diǎn)那這篇文章應(yīng)該能給你一些參考。我會把 Impeccable 從立項到落地、從原則到細(xì)節(jié)的過程都拆開講包括我們踩過的坑、改過的配置、以及那些看起來很小但實(shí)際很致命的檢查項。1. 為什么要把“無可挑剔”當(dāng)成一個項目來做1.1 質(zhì)量問題不該靠某幾個人的責(zé)任心撐住我見過太多團(tuán)隊把質(zhì)量掛在嘴上但實(shí)際行動全靠“這次發(fā)布大家小心一點(diǎn)”。小心是什么是沒有任何標(biāo)準(zhǔn)的口頭約定。代碼寫得快的人會少做測試資歷深的工程師會偷偷繞過規(guī)范新來的同學(xué)根本不知道哪些環(huán)節(jié)是必須的——最后的結(jié)果就是線上三天兩頭出問題運(yùn)維和測試背鍋研發(fā)覺得自己很冤。Impeccable 最初的導(dǎo)火索是一次很小的數(shù)據(jù)修復(fù)腳本。腳本語法沒問題邏輯也正確但因?yàn)檠h(huán)里少了一個限速參數(shù)上線后直接把數(shù)據(jù)庫連接池打滿了?;貪L花了十五分鐘但數(shù)據(jù)庫恢復(fù)用了快兩個小時。復(fù)盤的時候大家發(fā)現(xiàn)我們的發(fā)布檢查單里根本沒有“性能影響”這一項也沒有任何機(jī)制阻止這種腳本直接部署到生產(chǎn)環(huán)境。那次之后我就想明白一件事質(zhì)量問題從來不是測試一個環(huán)節(jié)的問題而是從單人提交到最終上線這一整條鏈路的問題。如果不把這條鏈路里的關(guān)鍵節(jié)點(diǎn)都變成可檢查、可執(zhí)行、可追責(zé)的規(guī)則那質(zhì)量就只是一個漂亮的形容詞不是工程結(jié)果。1.2 項目整體拆成四條主線Impeccable 的計劃沒有上來就買工具、上平臺而是先把問題分層。我們把自己能控制的環(huán)節(jié)拆成四條主線規(guī)范線、工具線、流程線、文化線。規(guī)范線負(fù)責(zé)統(tǒng)一標(biāo)準(zhǔn)比如編碼風(fēng)格、提交信息格式、接口設(shè)計約定、數(shù)據(jù)庫變更規(guī)范。工具線負(fù)責(zé)用程序去強(qiáng)制這些標(biāo)準(zhǔn)比如 lint、單元測試、依賴掃描、CI 門禁。流程線負(fù)責(zé)人在協(xié)作過程中的動作比如合并請求模板、發(fā)布檢查單、灰度發(fā)布規(guī)則。文化線則負(fù)責(zé)讓這些規(guī)則被真正接受而不是被當(dāng)成形式主義。很多團(tuán)隊一上來就搞平臺搞流水線但標(biāo)準(zhǔn)都沒定清楚工具只是把一個混亂的過程自動化了結(jié)果只是更快地制造混亂。Impeccable 的先后順序是先定標(biāo)準(zhǔn)再做自動化再設(shè)流程最后用文化去消化大家的情緒。這個順序很重要如果倒過來團(tuán)隊只會感到被工具和流程綁架。2. 核心細(xì)節(jié)解析與實(shí)操要點(diǎn)2.1 把“開發(fā)完成”重新定義一遍Impeccable 做的第一件具體事情是重新定義什么叫“完成”。以前團(tuán)隊里說“這個功能寫完了”意思往往是“代碼已經(jīng)提交了”但它在本地能不能跑、有沒有測試、有沒有文檔、會不會拖垮性能一概沒人回答。于是我們落地了一份 Definition of Done也就是完成標(biāo)準(zhǔn)清單每個人必須逐項確認(rèn)后才能把合并請求標(biāo)記為可評審。清單不算復(fù)雜核心項包括功能實(shí)現(xiàn)且自測通過、編寫或更新單元測試、更新接口文檔或使用說明、評估性能與安全性、確認(rèn)回滾方案、核對數(shù)據(jù)庫變更兼容舊數(shù)據(jù)。對于緊急修復(fù)可以走簡化版但至少也要包含自測、回滾和監(jiān)控告警這三項。落地的時候我們給每條標(biāo)準(zhǔn)都配上例子避免出現(xiàn)“我說的自測和你理解的自測不是一回事”的扯皮情況。在執(zhí)行方式上我們沒有采用不勾完就鎖 merge 的硬方式因?yàn)槟菚寛F(tuán)隊為了通過而隨便勾選。我們選擇的是讓評審人承擔(dān)責(zé)任如果代碼合并后出問題評審人要和作者一起復(fù)盤。這個設(shè)計是想告訴大家完成標(biāo)準(zhǔn)不是給系統(tǒng)看的是給團(tuán)隊里的真人看的。實(shí)際跑了兩周之后明顯感覺到大家提交代碼前會多想一步因?yàn)檎l都不想在被問到“你在自測時到底測了什么”的時候無話可說。2.2 自動化檢查怎么設(shè)置才不惹人煩完成標(biāo)準(zhǔn)定完之后第二步是把它落到自動化檢查里。這部分的坑非常多。一開始我們天真地想把所有規(guī)范都塞進(jìn) pre-commit結(jié)果開發(fā)者每次提交都要等三分鐘各種風(fēng)格檢查還經(jīng)常報一些“對齊空格”之類的低價值錯誤大家的逆反情緒一下就上來了。后來我們調(diào)整了策略本地只做最輕量、最相關(guān)的檢查比如格式化和基礎(chǔ)的靜態(tài)檢查把單元測試、構(gòu)建、依賴掃描、甚至性能回歸放到 CI 里執(zhí)行。本地檢查追求的是快不能超過十秒CI 檢查追求的是全面可以多跑一點(diǎn)。這樣才能讓開發(fā)者在寫代碼階段就得到反饋又不會被無關(guān)緊要的問題打斷心流。一個很關(guān)鍵的小技巧是自動化規(guī)則要按“信號強(qiáng)度”分級。信號最強(qiáng)的檢查是“能不能編譯、能不能跑、測試通不通”這些必須阻塞合并。中等信號的是“代碼風(fēng)格、命名規(guī)范、重復(fù)代碼”可以預(yù)警但不強(qiáng)制。弱信號的是“代碼覆蓋率低于閾值、某個文件行數(shù)太長”這些只做統(tǒng)計和趨勢跟蹤不阻塞任何流程。不要把強(qiáng)弱信號混在同一個門禁里否則團(tuán)隊會被弱信號煩死強(qiáng)信號反而被忽略。2.3 發(fā)布檢查單與灰度發(fā)布怎么設(shè)計才不擺設(shè)代碼合并只是中點(diǎn)上線才是真正的終點(diǎn)。Impeccable 的第二大塊工作是把發(fā)布過程變成可勾選、可驗(yàn)證的清單式操作。我們的發(fā)布檢查單不再是一張沒人看的 Excel而是根據(jù)變更類型自動生成的一組問題提交發(fā)布單的人必須逐一確認(rèn)。普通功能變更的檢查單里包含代碼是否已合并到主分支、測試是否通過、是否需要執(zhí)行數(shù)據(jù)遷移、配置項是否經(jīng)過評審、發(fā)布后需要觀察哪些監(jiān)控指標(biāo)、回滾步驟是什么。涉及數(shù)據(jù)變更的發(fā)布還會額外要求提供導(dǎo)入導(dǎo)出驗(yàn)證腳本和備份確認(rèn)。每一條都要求填寫真實(shí)信息不允許只寫“是”或“確認(rèn)”必須寫清楚“在哪個環(huán)境驗(yàn)證過”“影響范圍是什么”?;叶劝l(fā)布方面我們制定的規(guī)則是分批放量不能用天數(shù)的線性時間去替代業(yè)務(wù)指標(biāo)的驗(yàn)證。我們通常的配置是第一批放 1 個實(shí)例或 5% 流量觀察十五分鐘錯誤率第二批放 30%觀察十到二十分鐘第三批放 100%。每一批放量前都要求錯誤率低于網(wǎng)關(guān)基準(zhǔn)值的 1.5 倍并且業(yè)務(wù)核心路徑響應(yīng)時間沒有明顯上升。如果超過閾值系統(tǒng)會自動觸發(fā)回滾不需要值班同學(xué)糾結(jié)“再等一下會不會好”。3. 實(shí)操過程與核心環(huán)節(jié)實(shí)現(xiàn)3.1 從一次線上故障到項目立項的全過程還是繼續(xù)說那次數(shù)據(jù)修復(fù)腳本的事故。當(dāng)時的值班記錄現(xiàn)在回頭來看特別有意思問題一共分四層。第一層是業(yè)務(wù)異常用戶看到下單失敗第二層是系統(tǒng)告警數(shù)據(jù)庫連接池滿第三層是根因定位發(fā)現(xiàn)是腳本循環(huán)執(zhí)行了全量更新第四層是流程缺失發(fā)布平臺沒有任何提示說“這個操作會大規(guī)模更新線上數(shù)據(jù)”。復(fù)盤會開到一半就有人在問為什么這種代碼能上線因?yàn)闆]有性能評估環(huán)節(jié)。為什么沒有性能評估環(huán)節(jié)因?yàn)闆]有標(biāo)準(zhǔn)要求。為什么沒有標(biāo)準(zhǔn)因?yàn)榇蠹叶寄J(rèn)“腳本很小不會出事”。聽完這個因果鏈我覺得問題并不在于某個人而在于團(tuán)隊沒有一個項目級的兜底機(jī)制。于是當(dāng)周就在內(nèi)部立項代號 Impeccable目標(biāo)從“降事故”擴(kuò)成了“讓交付鏈條的每個環(huán)節(jié)都有審查節(jié)點(diǎn)”。團(tuán)隊一開始只有三個人在做這件事一個后端、一個測試、一個 DevOps我作為負(fù)責(zé)人。我們給自己定了兩條原則第一不搞重流程能自動化堅決不手填第二不追求一步到位先覆蓋最痛的發(fā)布事故場景再逐步往代碼提交階段延伸。3.2 落地第一套 Definition of Done 模板第一周的產(chǎn)出是一份名為 definition-of-done.md 的文件放在每個代碼倉庫的根目錄下。你可能覺得這就是個文檔沒什么技術(shù)含量但實(shí)際操作的時候細(xì)節(jié)點(diǎn)比想象中多。比如我們規(guī)定“編寫或更新單元測試”這條不只要求有測試文件還要求在 CI 上能看到新增測試用例的執(zhí)行記錄再比如“確認(rèn)回滾方案”這條要求寫出具體是回滾代碼、回滾數(shù)據(jù)還是回滾配置不能只寫“按標(biāo)準(zhǔn)流程”。這份模板后來還按變更類型做了分區(qū)。普通功能有基礎(chǔ)區(qū)涉及數(shù)據(jù)遷移或第三方接口的變更會增加對應(yīng)區(qū)塊。為了不讓模板變得冗長我們規(guī)定每條標(biāo)準(zhǔn)最多十個字到二十個字詳細(xì)說明統(tǒng)一放在 wiki 里。合并請求的描述模板也會自動載入這些區(qū)塊開發(fā)者直接在描述里勾選即可不需要額外打開文檔。我們給合并請求模板加了一行注釋“如果無法勾選某一條不要硬勾把原因?qū)懙皆擁椇竺?。”這個設(shè)計很重要因?yàn)橛行└唢L(fēng)險變更確實(shí)做不到完整自測比如無法在本地模擬真實(shí)第三方回調(diào)。允許寫例外反而讓真正卡住的阻塞項浮出水面比一團(tuán)和氣地全勾上更有價值。3.3 接入第一層自動化門禁的完整配置第二周我們把完成標(biāo)準(zhǔn)中能自動化的部分接進(jìn)了 CI。這里用 GitLab CI 舉例GitHub Actions 也類似。我們的核心思路是把檢查拆成多個并行任務(wù)并且讓部署任務(wù)只依賴必須通過的任務(wù)避免某一項檢查掛掉之后整條流水線被連帶卡住。stages: - verify - build - deploy verify:lint: stage: verify image: registry.example.com/lint-image:latest script: - npm run lint rules: - if: $CI_PIPELINE_SOURCE ! merge_request_event when: never - when: always verify:unit-test: stage: verify image: registry.example.com/node:20 script: - npm ci - npm run test:unit coverage: /All files[^|]*\|[^|]*\s([\d.])%/ artifacts: when: always reports: junit: junit.xml verify:security: stage: verify image: registry.example.com/security-scanner:latest script: - npm audit --audit-levelhigh allow_failure: false build:image: stage: build script: - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA . only: - main needs: - verify:lint - verify:unit-test - verify:security deploy:production: stage: deploy script: - ./deploy.sh production $CI_COMMIT_SHA environment: production only: - main when: manual needs: - build:image這套配置里有一個容易被忽略的地方單元測試的coverage正則捕獲覆蓋率數(shù)值是為了讓 MR 頁面可以直觀看到增量覆蓋率和總覆蓋率趨勢安全掃描audit --audit-levelhigh是阻塞級別可以直接攔截高風(fēng)險依賴依賴部署任務(wù)是手動觸發(fā)不給自動流水線直接上生產(chǎn)的權(quán)限。這樣既做到了自動化又把最需要人判斷的一步留給操作者。接入后第一個月的效果是MR 被阻塞的比率大約在 18%其中 70% 是單元測試掛掉20% 是安全掃描發(fā)現(xiàn)問題10% 是 lint。而我們最擔(dān)心的“developers 什么都不看就點(diǎn)合并”并沒有出現(xiàn)因?yàn)椴噬艿涝?MR 頁面上太顯眼了誰都不想和紅叉作伴。3.4 灰度發(fā)布規(guī)則的真實(shí)配置與參數(shù)計算第三周我們把灰度發(fā)布規(guī)則從口頭約定變成了平臺配置。因?yàn)橛行﹫F(tuán)隊沒有現(xiàn)成的灰度發(fā)布系統(tǒng)這里給出一個可以在部署腳本里實(shí)現(xiàn)的判斷邏輯示意用來解釋我們當(dāng)時參數(shù)是怎么算出來的。#!/usr/bin/env bash # 灰度發(fā)布判斷腳本根據(jù)當(dāng)前實(shí)例數(shù)和錯誤率自動決定放量比例 TOTAL_INSTANCES20 BATCH_11 # 第一批 1 個實(shí)例約 5% 流量 BATCH_26 # 第二批再加 5 個實(shí)例累計 30% 流量 BATCH_320 # 第三批全量 if [ $CURRENT_INSTANCES -lt $BATCH_1 ]; then echo 啟動第一批灰度1 個實(shí)例 elif [ $CURRENT_INSTANCES -lt $BATCH_2 ]; then echo 待錯誤率低于閾值后放量到 6 個實(shí)例 elif [ $CURRENT_INSTANCES -lt $BATCH_3 ]; then echo 待錯誤率低于閾值后放量到 20 個實(shí)例 fi參數(shù)計算的核心不是“拍腦袋定百分比”而是先定義可回滾范圍。比如服務(wù)總共 20 個實(shí)例第一批只放 1 個即使這批全掛也只影響 5% 用戶第二批放到 6 個累計影響 30%第三批全量。放量間隔不是固定十五分鐘而是必須滿足“連續(xù)觀察窗口內(nèi)錯誤率低于基準(zhǔn)的 1.5 倍且 P95 延遲低于基準(zhǔn)的 1.2 倍”。如果系統(tǒng)自動檢測到錯誤率飆高腳本會執(zhí)行回滾把實(shí)例數(shù)恢復(fù)到上一批并向值班群發(fā)送告警。這套手動腳本看起來簡單實(shí)際執(zhí)行時最大的價值是把決策規(guī)則寫在代碼里。以前灰度放量完全靠發(fā)版同學(xué)的感覺感覺快就快點(diǎn)感覺不對就停?,F(xiàn)在腳本會明確告訴你當(dāng)前應(yīng)該處于哪個批次閾值是多少回滾動作是什么人和機(jī)器都清楚。4. 常見問題與排查技巧實(shí)錄4.1 自動檢查被繞過怎么辦我們剛接入自動門禁的時候遇到一個非常真實(shí)的場景開發(fā)者在本地把 pre-commit 跳過或者 CI 里故意把a(bǔ)llow_failure: true。原因無外乎覺得自己改動很小沒必要跑完整檢查。第一次發(fā)現(xiàn)這種事我們沒有立刻處罰人而是先看是不是流程設(shè)計有問題。排查后發(fā)現(xiàn)很多人并不是想偷懶而是本地檢查確實(shí)太慢了而且報錯信息不友好。于是我們把本地檢查精簡到只剩格式化、類型檢查和最基礎(chǔ)的 smoke test同時把 CI 門禁設(shè)置為服務(wù)端強(qiáng)制。關(guān)鍵點(diǎn)是任何自動化檢查都不能只依賴開發(fā)者本地執(zhí)行本地是可選的、快速的服務(wù)端才是必須的。只要在 MR 目標(biāo)分支上強(qiáng)制設(shè)置管道必須通過再有權(quán)限規(guī)則禁止跳過就能從機(jī)制上堵住主動繞過的口子。如果發(fā)現(xiàn)有人通過修改 pipeline 配置來繞過那就不是流程問題而是文化問題了。我們的處理原則是公開復(fù)盤代碼不點(diǎn)名批評但要把繞過的動作當(dāng)成事故對待。做過一兩次之后大家就明白“不要和機(jī)制作對”這個道理了。4.2 完成標(biāo)準(zhǔn)被當(dāng)成形式主義怎么辦這是最隱蔽的問題。上線兩周后我開始在 MR 里看到“全部完成標(biāo)準(zhǔn)已確認(rèn)”的勾選但代碼評審時明顯感覺作者根本沒有理解需求。為什么因?yàn)橥瓿蓸?biāo)準(zhǔn)一旦變成“只有勾選才能合并”它就會變成一種門檻而門檻會激發(fā)人的簡化心理——只要不被系統(tǒng)攔下來就好。我們的調(diào)整是讓評審人不再只看勾選狀態(tài)而要追問證據(jù)。比如“已自測通過”后面必須附上自測結(jié)果截圖或日志片段“已評估性能影響”后面必須寫清楚用的是 QPS 還是響應(yīng)時間指標(biāo)。我以身作則在評審高風(fēng)險的 MR 時會先要求作者在評論里回答三個問題這次變更最可能導(dǎo)致什么異常如果這個異常發(fā)生了你會怎么發(fā)現(xiàn)如果發(fā)現(xiàn)了你會怎么回滾這三個問題比任何完成標(biāo)準(zhǔn)都能逼出真實(shí)思考。團(tuán)隊也一度有人覺得這樣評審太慢。但我的經(jīng)驗(yàn)是風(fēng)險高的代碼花二十分鐘問清楚比上線后整個團(tuán)隊花兩個多小時排查要劃算得多。而且這種追問幾次之后作者自己就會提前準(zhǔn)備評審速度反而會快起來。4.3 發(fā)布檢查單漏項和誤報的排查發(fā)布檢查單一開始很容易變成兩極端要么太長沒人仔細(xì)看要么太簡單形同虛設(shè)。我們第一次跑正式發(fā)布生產(chǎn)前檢查單有 27 項很多人填到后面已經(jīng)不看題目了直接一路打勾結(jié)果漏掉了最關(guān)鍵的“數(shù)據(jù)庫備份恢復(fù)驗(yàn)證”。后來我們把檢查單改成智能路由按變更類型展示對應(yīng)條目。代碼發(fā)布只顯示 8 項常規(guī)檢查涉及數(shù)據(jù)庫變更再多顯示 5 項涉及第三方接口調(diào)用再顯示 3 項。每項還加了一個“自動預(yù)填充”入口比如合并請求中已有的測試狀態(tài)和 lint 結(jié)果可以直接帶出來不需要人工手填。真正需要人判斷的只剩三五項漏項就少很多。還有一個經(jīng)驗(yàn)誤報比漏報的破壞力更大。如果檢查單里經(jīng)常出現(xiàn)“配置項 A 未變更已確認(rèn)”這種永遠(yuǎn)都是默認(rèn)值的項它會拉低整個清單的可信度。我們養(yǎng)成了每個季度做一次檢查單瘦身的習(xí)慣把連續(xù)三個月命中率低于 10% 的項刪掉或者用監(jiān)控替代人工確認(rèn)。4.4 灰度環(huán)境看不出問題的現(xiàn)象與對策灰度發(fā)布最容易踩的坑是“灰度環(huán)境什么問題都測不出來”。我們的第一版灰度是基于測試環(huán)境集群做的但測試環(huán)境和生產(chǎn)環(huán)境的流量模型、數(shù)據(jù)量、緩存命中率都不一樣就算灰度集群掛掉也模擬不出真實(shí)用戶的組合場景。第一次灰度新版本時測試環(huán)境一切正常生產(chǎn)環(huán)境第一批流量進(jìn)去 P99 就直接漲了三倍。排查下來主要是兩個原因一是灰度流量沒有做染色無法精確識別請求來自灰度集群二是監(jiān)控重點(diǎn)放在了 CPU 和內(nèi)存上沒有盯事務(wù)響應(yīng)時間和錯誤率。后來的對策是把監(jiān)控升級為“技術(shù)指標(biāo) 業(yè)務(wù)指標(biāo)”雙層結(jié)構(gòu)。技術(shù)指標(biāo)看錯誤率、P95 延遲、線程池占用業(yè)務(wù)指標(biāo)看訂單成功率、支付回調(diào)延遲、核心接口的調(diào)用量。只有兩類指標(biāo)都正常才能繼續(xù)放量。我們還在灰度網(wǎng)關(guān)里加了流量染色標(biāo)記把灰度請求的 header 透傳到日志中心。這樣出現(xiàn)問題的時候能快速判斷是灰度版本的責(zé)任還是外部依賴抖動造成的。這個改動帶來的收益非常直接以前灰度失敗要爭論半天現(xiàn)在已經(jīng)形成標(biāo)準(zhǔn)結(jié)論“看染色日志先比指標(biāo)再定責(zé)”。5. 如果現(xiàn)在重新開始我會更早做這幾件事Impeccable 走到今天已經(jīng)不再只是一個代號它變成了一套團(tuán)隊內(nèi)默認(rèn)的質(zhì)量語言。如果現(xiàn)在重新開始我會把三件事排得再靠前一點(diǎn)。第一件事先挑一個“最痛的線上事故”做全套復(fù)盤然后把對應(yīng)的檢查項做成門禁。不要試圖一開始覆蓋所有場景找到一個最近真實(shí)傷害過團(tuán)隊的坑把它堵上團(tuán)隊才會真的信這套機(jī)制。第二件事把完成標(biāo)準(zhǔn)里最容易被模糊化的部分寫得更具體。不要寫“代碼質(zhì)量好”要寫“核心路徑有測試覆蓋且評審人可以運(yùn)行測試看到覆蓋結(jié)果”。第三件事把自動化檢查的反饋速度做快寧可少一個檢查項也要保證合并請求看到結(jié)果的時間不超過五分鐘。檢查節(jié)奏太慢人會等得不耐煩然后開始想辦法繞過。最后再分享一個我個人的體會項目叫 Impeccable但你要接受它永遠(yuǎn)不完美也不需要完美。這本身就是一個動態(tài)的校準(zhǔn)過程每一次事故、每一次復(fù)盤、每一次規(guī)則優(yōu)化都是讓系統(tǒng)離理想狀態(tài)更近一步的嘗試。質(zhì)量和其他工程問題一樣不是一個終點(diǎn)而是一套持續(xù)運(yùn)轉(zhuǎn)的機(jī)制。