我怎麼把一個要半夜全員待命的部署,變成按一個鍵就好的日常
先講一個我到現在想起來還會手心冒汗的晚上。
那時我在一間交易所,負責撮合跟金流附近的服務。我們的部署排在每週四凌晨兩點,因為那是交易量最低的時段。為什麼挑半夜?因為部署這件事在當時根本是一場手術,而且是沒有麻醉、開到一半才發現器械沒備齊的那種。
這篇不是 DevOps 入門,也不是工具比較。是我自己怎麼從那種晚上,走到現在可以白天按一個按鈕就上線、出事自己會回滾、半夜手機安安靜靜的日子。中間踩過的坑、做過的取捨,我盡量講具體。
那個我不想再經歷第二次的凌晨
把場景描清楚你才知道有多荒謬。
週四凌晨一點半,五個人擠在 Slack 一個臨時開的語音頻道。有後端、有 DBA、有一個前端、一個主管在旁邊「待命」(其實就是看著)。流程是一份 Google 文件,二十幾個步驟,從「把流量從這台機器移走」「手動跑這支 migration」「scp 上去」「重啟 service」「看 log 確認有起來」「再把流量切回來」,一步一步來。每一步做完要在頻道裡回報一聲「OK 下一步」。
那天我們上的是一個結算相關的改動。跑到第十四步,要對一張很大的表加欄位、補預設值。我在測試環境跑過,三秒。正式環境的那張表大了兩個數量級,那支 ALTER 直接鎖表,鎖了十一分鐘。
十一分鐘對一個正在凌晨低峰、但還是有人在掛單的交易所是什麼概念?掛單進不來、撤單卡住、API 開始噴 timeout。監控的圖整片紅。主管原本在旁邊滑手機,整個人坐直起來問「現在是什麼情況」。
更糟的是回滾。那份文件裡的「回滾步驟」是另外二十步,而且其中有一步是「如果已經跑了 migration,需要手動把欄位 drop 掉、再把這段資料從備份撈回來」——換句話說,回滾本身比部署還危險。那一刻我們五個人對著螢幕,誰都不敢按下一步,因為前進跟後退都可能把事情搞得更大。
最後是硬等。等那支 ALTER 自己跑完,等流量自己恢復,凌晨三點四十我們才敢說「應該沒事了」。我回到家四點,躺在床上眼睛還是閉不起來,因為我知道下週四同樣的事還要再來一次。
那種痛不是技術上的,是一種對上線本身的恐懼。每次部署都像在拆彈,而且拆的人不是專家,是輪到的倒楣鬼。
我先做的不是上工具,是把恐懼拆開來看
被那晚嚇到之後,我沒有馬上去裝什麼 CI/CD 工具。我先做一件很笨的事:把那份二十幾步的文件攤開,一步一步問自己「這一步為什麼要人來做?換成機器做會怎樣?」
問完一輪我發現,二十幾步裡面,真正需要人判斷的只有兩步:一個是「決定要不要上」,一個是「上完之後確認業務指標正常」。其他全部都是機械性的動作——而機械性的動作交給人做,就是在等人出錯。凌晨兩點的人特別會出錯。
這個盤點對我很關鍵,因為它把「自動化」從一個口號,變成一張很具體的清單:哪幾步可以直接寫成腳本、哪幾步要設計成可以安全重跑、哪一步是真正需要人點頭的閘門。我不是要一步登天做出完美 pipeline,我是要把那二十步裡的人為失誤點一個一個拆掉。
第一階段:先讓「同一份東西」到處都跑得起來
那次事故的真正導火線,其實是測試環境跟正式環境差太多——測試的表很小,所以沒人發現那支 ALTER 會鎖表。所以我動的第一刀是環境一致性。
我們把服務容器化。Go 在這點上幫了大忙,編譯出來就是一個單一執行檔,丟進一個很小的基底映像就能跑,沒有一堆 runtime 依賴要喬。光是這一步,就消滅了一整類「我這邊明明可以」的鬼故事——因為大家跑的就是同一個映像,連作業系統的函式庫版本都一樣。
更重要的是資料庫。我們開始要求所有 schema 變更都走 migration 工具、進版控,而且每一支 migration 上正式之前,要先在一份從正式環境定期還原出來的「接近正式」的資料量上跑過。就是這條規矩,讓「鎖表十一分鐘」這種事在進正式之前就會被抓到——因為你會在預演的時候就看到它跑了十一分鐘,然後你會去把它改成不鎖表的做法(線上加欄位、分批回填)。
這裡有個取捨值得講:維護一份接近正式的資料其實有成本,要定期還原、要處理敏感資料的遮罩。但跟一次凌晨事故比起來,這個成本便宜到不值得猶豫。
第二階段:把那二十步寫成一條流水線
環境一致之後,我才開始接 CI/CD。重點不是用哪個工具,是把那份文件裡的機械步驟一條一條搬進去:
- 程式碼推上去,自動跑測試(含那些我們特別在意的金流邊界案例)
- 測試過了自動建置映像、打上版本標籤
- 部署改成滾動式:一次換一台,新的那台健康檢查通過了,才換下一台
- migration 變成 pipeline 的一個明確步驟,而且設計成可以重複執行不會壞
做到這裡,「上線」這個動作從二十步變成兩件事:合併一個 PR、在白天按一個確認鍵。挑半夜這個習慣,我們大概兩個月後就徹底丟掉了,因為已經沒有理由挑半夜——滾動部署讓單一一台壞掉不會影響整體,而且我們有了下面這個東西。
我最在意、也最晚才做對的一件事:自動回滾
老實說,前面那些都還算順,真正讓我晚上睡得著的是回滾。
我給自己定一條規矩:如果一個部署不能在一分鐘內、不需要人去查文件就回到上一個好的版本,那它就不算做完。
具體上我們做了兩件事。第一,部署永遠保留上一個版本的映像跟設定,回滾就是把流量切回去,一個指令的事,不重建、不撈備份。第二,部署完成後,pipeline 會自己盯著幾個關鍵指標看一段時間——錯誤率、關鍵 API 的延遲、撮合的成交筆數有沒有掉。如果在這個觀察窗內指標破線,pipeline 自己就把流量切回舊版,然後才告訴我「我回滾了,你來看一下為什麼」。
這裡最大的設計取捨是:資料庫的變更要做到「可回滾」,跟程式碼不一樣。所以我們養成一個習慣——schema 變更一律往「相容舊版」的方向做。先加欄位、不要馬上刪舊欄位;先讓新舊程式碼都能跑,等新版穩定幾天了,再用另一個部署去清理。這讓「回滾程式碼」永遠不需要「回滾資料庫」,那個凌晨「要不要 drop 欄位」的兩難,從此不存在。
那個把我吵醒、但根本不該吵醒我的告警
監控跟告警我想單獨講一個很具體的例子,因為這是我體會最深的。
剛把監控建起來那陣子,我犯了一個新手都會犯的錯:什麼都報。其中有一個告警是「某個下游 API 的 p99 延遲超過 800 毫秒」。聽起來很合理對吧?
結果這個告警在凌晨三點把我吵醒過三次。我爬起來、開電腦、連 VPN、開儀表板,看了老半天——每一次都是因為那個下游在凌晨跑它自己的批次作業,p99 短暫飆高個幾分鐘,然後就自己恢復了。對我們的服務完全沒影響,使用者根本沒感覺,因為我們對那個下游本來就有 timeout 跟重試,它慢一點我們扛得住。
第三次被吵醒的那個早上,我做了兩件事。第一,我把那個告警從「立刻呼叫人」降級成「記錄下來、白天看報表」。第二,更重要的,我重新想了一遍「什麼樣的事才值得在半夜叫醒一個人」。
我給自己的答案是:只有當使用者真的受影響、而且這件事不會自己好、需要人立刻介入時,才該呼叫人。 p99 高一點但使用者無感,不符合;磁碟還有三天才會滿,不符合(那是白天該處理的事)。但「下單成功率掉到九成以下」「結算服務完全沒回應」——這種才值得把人從床上挖起來。
調完之後,我們的待命告警數量掉了大概七成。剩下的那三成,每一個響起來都是真的有事。這件事的副作用很妙:當告警變少、而且每個都當真,大家對告警的敏感度反而回來了。以前那種「又響了,先無視,應該又是誤報」的麻木感不見了。一個會在半夜叫醒你的告警,必須要值得你爬起來,不然它就是在訓練整個團隊忽略它。
改完之後,生活真的不一樣了
我不想把這寫成一個太漂亮的結局,但差別是真實的。
以前部署是一件要在行事曆上標紅、要協調五個人時間、要心理建設的大事。現在它是我下午改完一個 bug、跑過測試、合併 PR、按一個鍵,然後去倒杯咖啡的事。回來瞄一眼 pipeline 綠了就繼續做下一件事。如果它紅了,它通常已經自己回滾好了,我只要心平氣和地去看是哪裡出問題。
待命也從一件令人焦慮的事,變成一件大部分時候很無聊的事——而維運上「無聊」是最高的讚美。手機半夜不響,響的時候我知道是真的,這種信任感很值錢。
回頭看,我覺得這整件事的核心其實不是工具,是把人從機械性、容易出錯的環節裡拿出來,只留下真正需要判斷的地方給人。自動化不是為了炫技,是為了讓凌晨兩點不再需要五個人盯著一份文件發抖。能寫出功能、能讓它穩穩地跑、出事能快速且安全地復原——這三件事我以前覺得是三種能力,現在覺得是同一件事的三個面向。少了任何一面,你交付的就不是一個完整的系統,只是一段碰運氣的程式碼。
留言討論
有想法、有不同經驗、或想糾正我?歡迎在下面留言,免註冊,填個暱稱就能留。