DevOps·2026年6月12日·12 分鐘閱讀

部署不等於發布:我用 Feature Flag 把支付路由從 1% 灰度到 100% 的那一週

L
Leo Wu

後端工程師,用 Feature Flag 在金流系統做漸進式發布。

那天凌晨兩點,我沒有 rollback,只改了一個布林值

先講一個畫面。

去年我們要換掉一個用了三年的支付路由邏輯。舊的邏輯是:所有刷卡交易固定打第一家收單行,失敗才轉第二家。問題是第一家在每天晚上 11 點到凌晨 1 點的對帳批次期間,授權成功率會從平常的 96% 掉到 88% 左右。每天就那兩個小時,我們白白損失大概八個百分點的成交。以我們當時一天約四萬筆刷卡、平均客單一千二來算,那兩小時內的失敗交易,一個月加起來是七位數的 GMV 蒸發。

新的邏輯複雜很多:會根據卡組織、發卡行 BIN、當下各收單行的即時成功率,動態決定打哪一家。寫起來不難,難的是我完全不敢一次全量上。支付這種東西,你寫錯一行 BIN 比對,可能就是把某一整個發卡行的客戶全部擋在門外,而且你還不會馬上發現——因為那家銀行的卡可能只佔你交易量的 3%,監控圖上看不太出來,但客服信箱會在隔天爆掉。

所以那次上線,我做的不是「部署完就開」。我是把整套新路由邏輯包在一個叫 payment_routing_v2 的 flag 後面,部署上去,但 flag 預設是關的。也就是說,新程式碼進了 production,但對使用者來說什麼都沒變。這是我想先講清楚的第一件事:部署(deploy)和發布(release)是兩件事。程式碼進到伺服器,跟這段程式碼開始服務真實使用者,完全可以、也應該分開。

那一週的節奏是這樣的:

  • 週一晚上,我把 flag 對「內部員工帳號」打開。只有我們公司自己人刷卡會走 v2,大概兩百多筆,我盯著看了一晚。
  • 週二,開到 1%。隨機抽樣 1% 的使用者走新路由。
  • 週三,5%。我特別挑了晚上 11 點到 1 點那個對帳時段看,新路由果然把流量導去第二家,那段時間 v2 的成功率是 95.8%,對照組(走舊邏輯的)只有 88.4%。
  • 週四,25%。
  • 週五,50%。
  • 下週一,100%。

整個過程沒有任何一次 rollback,沒有任何一次半夜 hotfix 部署。中間在 5% 的時候確實出過一個小狀況:某張美國運通的卡因為 BIN 區間我寫的範圍少了一段,被錯誤地導去一家不支援 AMEX 的收單行,直接授權失敗。客服轉了兩封信給我。我做的事情是——把 flag 從 5% 轉回 0%,大概三十秒。然後不慌不忙地修 BIN 表,重新部署,再從 1% 開始。

如果沒有 flag,這個情境會是:我得緊急發一個 revert 的 commit,跑完整套 CI/CD,等映像檔 build、等部署 rolling update,順利的話十五到二十分鐘,期間所有走錯路由的 AMEX 交易繼續失敗。有 flag,是三十秒,而且只影響到那 5% 的人。

這就是 feature flag 對我來說最核心的價值:它把「關掉一個壞功能」的成本,從「一次完整的部署流程」降到「改一個值」。

為什麼 deploy != release 這件事這麼重要

很多剛接觸的人會覺得這是在玩文字遊戲。我自己也是踩過坑才真的懂。

傳統的模式裡,這兩件事是綁死的:你 merge、你部署,功能就上了。這帶來一個很討厭的後果——你的「上線」這個動作本身就是高風險的,而且是不可逆的(除非再來一次部署)。所以大家會傾向把上線排在離峰、排在週末、排在大家都很緊張地圍在螢幕前的時候。上線變成一個儀式,一個事件。

把兩者拆開之後,世界觀整個變了:

部署變成一件無聊的、低風險的、隨時可以做的事。因為部署的程式碼後面跟著的 flag 是關的,部署本身不改變任何使用者體驗。我可以一天部署十次,在大白天,沒人會緊張。

發布變成一個「營運決策」,而不是「工程動作」。要不要開 v2、開給誰、開多少,這件事甚至不一定要工程師來按。在我們後來的系統裡,PM 自己有一個後台可以調百分比。發布跟那條程式碼進不進伺服器,徹底解耦了。

我後來常跟團隊講一句話:如果你的上線需要挑時間、需要全員待命、需要事先寫好 rollback 計畫,那代表你的上線本身就是設計失敗的。健康的狀態應該是:部署隨時做,發布慢慢轉,出事就秒關。

Flag 不是只有一種,混在一起用會出事

我早期犯過一個錯,就是把所有的 flag 都當成同一種東西管。後來吃了苦頭才學會分類。不同類型的 flag 生命週期、擁有者、清理策略完全不一樣。

Release flag(發布旗標)。這就是上面講的那種:包住一個還沒完全 ready 的新功能,讓你部署跟發布分離。它的特性是「短命」——一旦功能 100% 開出去、穩定了,這個 flag 就該死掉。payment_routing_v2 就是這種。它活了大概三週就被我刪了。

Ops flag(營運旗標)。這是 kill switch、降級開關這一類。比如「關掉推薦系統」「把首頁的個人化區塊換成靜態快取」「暫停所有非必要的背景 job」。這種 flag 的特性是「長命甚至永久」,因為它存在的意義就是在系統壓力大或某個下游掛掉時,讓你能手動降級。我們有一個叫 disable_realtime_pnl 的 ops flag,在交易所行情劇烈波動、即時損益計算把 DB 壓垮的時候,值班的人可以一鍵把即時損益關掉、改成五秒刷新一次。這個 flag 活了好幾年,而且應該要永遠活著。

Experiment flag(實驗旗標)。A/B test 用的。把使用者分桶,A 組看舊版、B 組看新版,收集轉換率數據。它的擁有者通常是產品或數據團隊,生命週期跟著實驗走,實驗結束就該收掉。這種 flag 最危險的地方是,實驗做完了大家就忘了它,程式碼裡留著一個永遠走某一邊的死分支。

Permission flag(權限旗標)。這其實算是一種長期的功能授權,比如「VIP 客戶才能用的進階下單類型」「企業版才有的 API」。它跟使用者的方案、身份綁定,本質上是業務邏輯的一部分,會長期存在。

我現在的習慣是,每個 flag 在建立的時候就要標清楚它是哪一種,還有「預計什麼時候清掉」。release flag 沒有清理日期是不准建的。

灰度的真正麻煩:百分比之外的事

把流量從 1% 開到 100% 聽起來很簡單,實作上有幾個我踩過的雷。

第一個是分桶的一致性。你不能每一個 request 都重新擲骰子決定他走不走 v2。同一個使用者這次刷卡走 v2、下次刷卡走 v1,如果這兩條路徑的行為有差異,使用者會看到詭異的不一致,你 debug 的時候也會瘋掉。正確做法是用使用者 ID 做 hash,讓同一個人永遠落在同一個桶。我們用的是把 user_id 拿去算一個 hash,對 100 取模,如果結果小於當前的百分比門檻就走新路。這樣「1% 變 5%」是純擴張的——本來在 1% 桶裡的人,在 5% 的時候一定還在裡面,不會有人從新版被踢回舊版。

第二個是 cohort canary,按族群而不是純隨機。純隨機的 1% 在某些場景不夠好。支付這個案例,我其實更在意的是「不同發卡行」的覆蓋,而不是單純抓 1% 的人。因為風險是按 BIN 分布的。所以我中間有一段是手動指定:先只開給某幾家大型發卡行的卡,確認沒問題,再擴大。canary 的維度可以是地區、可以是裝置、可以是使用者等級、可以是某個特定的大客戶。「百分比」只是 canary 的其中一種切法。

第三個,也是最容易被忽略的:你必須能分組看監控。如果你開了 5% 的 v2,但你的 dashboard 只有一條「整體成功率」的線,那這 5% 出問題的時候會被 95% 的正常流量稀釋掉,你根本看不見。我們在每筆交易的 metrics 上都打了一個 routing_version 的 tag,Grafana 上可以把 v1 跟 v2 的成功率、延遲分開畫。沒有這個,灰度等於蒙著眼睛開車。

Flag 的黑暗面:我見過活了四年的殭屍 flag

講了這麼多好處,該講代價了。Feature flag 用久了會變成一種債,而且是會利滾利的那種。

最直接的是程式碼裡的 if 越來越多。每一個 flag 都是一個分支,兩個 flag 就是四種組合,十個 flag 理論上就是一千零二十四種狀態。你不可能把每種組合都測過。我看過最誇張的一段程式碼,巢狀了三層 flag 判斷,沒有人敢動,因為沒人知道把某個 flag 拿掉之後,跟另外兩個 flag 的交互會發生什麼事。這就是 combinatorial explosion,組合爆炸。

再來是殭屍 flag。我前公司有一個 flag,叫 use_new_settlement_calc,是某個前同事在我進公司之前建的。我接手的時候它的值是 true,而且全環境都是 true 已經好幾年了。沒有人記得它原本的「舊版」是什麼,也沒有人敢把那段 false 分支的死程式碼刪掉,因為註解寫著「2020 對帳問題暫時保留」。它就這樣活了四年,佔著程式碼、佔著 config、每次新人 onboarding 都要花時間問「這個是幹嘛的」,然後得到「不知道,別動」的答案。

還有一個比較隱蔽的坑:flag 的判斷本身會變成效能熱點。如果你的 flag 系統是每次判斷都去打一次遠端的 config service,那在高併發路徑上這就是一個額外的網路往返。我們撮合引擎的熱路徑上絕對不放需要遠端查詢的 flag,所有 flag 的值都是啟動時或定期拉下來快取在記憶體裡的,判斷就是讀一個 local 的 map。這個取捨要在一開始就想清楚:flag 的「即時性」和「效能」是有張力的。

我現在怎麼管理 flag 的生命週期

吃過上面那些虧之後,我們團隊定了幾條規矩,不是什麼業界最佳實踐,就是被現實磨出來的。

每個 release flag 建立時就要有死亡日期。我們用 ticket 系統,建 flag 的同時開一張「清理這個 flag」的票,排到大概兩個 sprint 後。功能穩定了就去把那張票做掉:刪 flag、刪 config、刪掉沒在走的那條分支。如果到期了還沒清,要嘛功能其實沒穩,要嘛它根本不是 release flag,該重新歸類。

定期掃一次殭屍。我寫了一個簡單的 script,定期掃 codebase 裡所有 flag 的 key,跟 config service 裡實際存在的 flag 對照。在程式碼裡找不到、但 config 裡還在的,是孤兒;config 裡早就 100% 或 0% 很久、值再也沒變過的,是清理候選。每個 quarter 拉出來檢視一次。

flag 要有擁有者。每個 flag 在系統裡都掛一個負責人。沒有 owner 的 flag,就是將來的殭屍。

永久型的 ops flag 要跟 release flag 在系統裡分開放、分開標。不然你 quarterly 清理的時候會誤刪掉 kill switch,那就尷尬了。

說到底,feature flag 是一把很利的刀。它讓我可以在凌晨兩點面對一個壞掉的支付路由時,不用緊張地寫 revert、不用叫醒整個 team,只是冷靜地把一個值轉回 0,然後去倒杯水,回來慢慢修。那種「主導權在我手上」的感覺,是這個工具給我最大的東西。但它也是一把會反過來割到自己的刀——每一個你圖方便加上去、卻忘了拿掉的 flag,都是埋給未來的你的一顆地雷。我這幾年學到最重要的一課,不是怎麼加 flag,加 flag 太簡單了;是怎麼有紀律地把它們殺掉。能讓一個東西活下來不難,難的是知道什麼時候、以及有沒有勇氣讓它好好地死掉。

#Feature Flag#漸進式發布#灰度#部署#DevOps

相關文章