灰度發布 (Progressive Delivery / Gradual Rollout) 是什麼?從 Canary、Blue-Green Deployment 到 Feature Flag

當新的版本服務要上 production 生產環境時,最直覺的做法就是把所有流量一次全部切過去上線,這是時常看到的,這種做法如果順利的話那就皆大歡喜(實際上不太可能),但只要新版本藏了一個沒測到的 critical bug,等於讓 100% 的使用者同時出問題。這種「一次全量發布」在生產環境裡最省事、卻也最危險,在短期快速迭代開發下很難有 100% 沒有任何缺陷的軟體應用服務,但現在還是時常看到不少企業在做這樣的事情。實際上為了控制這個風險,software engineering 圈子早已發展出「灰度發布」這個概念,底下可細分出 Canary Rollout、藍綠部署(Blue-Green Deployment)、A/B Testing、Feature Flag 幾種做法,但這幾個詞常常被混著用、甚至誤用。這系列文章會先在第一篇把名詞和框架釐清,後面幾篇再針對 AWS EKS + Kubernetes 的具體實作、以及 Web 通訊 App(含 WebSocket 長連線、Android/iOS 行動端)的灰度發布細節展開。在往下讀名詞定義之前,先介紹這系列文章會用的範例情境,方便對照後面出現的程式碼與設定。

本系列的範例情境

這系列文章會用一套虛構的雲端 Web 通訊 App (姑且稱它 ChatApp) 當作貫穿全系列的範例,情境設定盡量貼近實際專案常見的架構,方便對照後面出現的程式碼與設定檔。

ChatApp 提供三種客戶端,使用者可以自由選擇任一種登入使用:

  • iOS App(原生 Swift)
  • Android App(原生 Kotlin)
  • 網頁版(瀏覽器直接使用的 Web App)

三種客戶端底層都呼叫同一組後端 API,後端部署在 AWS EKS(Kubernetes)上,由幾個主要模組組成:

  • Gateway / Ingress 層:對外的統一入口,也是本文提到的 Canary 流量分流實際作用的地方
  • 訊息服務(Message Service):處理聊天訊息的收發,跟客戶端之間走 WebSocket 長連線
  • 認證服務(Auth Service):處理登入、身分驗證
  • 資料庫(Database):儲存使用者資料與歷史訊息記錄

後面所有的範例(包含 chatapp-v1/chatapp-v2chatapp-blue/chatapp-green 這些命名),指的都是這個情境裡的服務。

(一)什麼是灰度發布(Gradual Rollout / Progressive Delivery)

灰度發布是中國大陸技術圈常常聽到也常用的講法,台灣鮮少聽到有人直接講「灰度」發佈。如果跟海外各國團隊溝通時,建議直接辨識其英文全名。以下名詞都要認識,因為它們指的是同一件事的不同抽象層次。

先講清楚層次關係:灰度發布是廣義的策略概念,泛指「任何不是一次把新版本全量發給所有使用者」的發布方式,本身不是一種特定技術,而是一個傘狀概念,底下可以用不同具體手段實現。以下是常見名詞的中英對照,後續文章都會照這個對照表使用名詞:

中文名稱英文原文定義簡述所屬層次
灰度發布Gradual Rollout / Progressive Delivery廣義策略,任何非一次全量的漸進式發布方式策略概念層
金絲雀發布Canary Release / Canary Deployment用少量流量或少量副本驗證新版本穩定性具體實現—流量層
藍綠部署Blue-Green Deployment新舊版本並存,一次性整批切換流量具體實現—部署層
A/B 測試A/B Testing依使用者分群比較不同版本的業務效果具體實現—實驗層
功能旗標Feature Flag / Feature Toggle程式碼層面用開關控制功能顯示,不涉及重新部署具體實現—應用層
常見誤解:有些文章把「灰度發布」跟「A/B Testing」畫等號,但兩者目的不同:一個是為了降風險,一個是為了比效果。混用這兩個詞,容易在團隊溝通時產生誤解,例如工程師以為要做風險控管,PM 卻預期看到轉換率報告。

(二)四種具體做法差在哪:Canary、藍綠部署、A/B Testing、Feature Flag

接下來的範例都以「本系列的範例情境」介紹過的 ChatApp 為背景。

Canary(金絲雀發布)
目的:用少量流量驗證新版本穩定性,出事可以秒級回滾。這是「流量層級」的漸進切換,重點在風險驗證,不是實驗業務指標。

metadata:
  annotations:
    nginx.ingress.kubernetes.io/canary: "true"
    nginx.ingress.kubernetes.io/canary-weight: "5"

這段設定會讓 ChatApp Gateway 層 5% 的流量進到新版本 Pod,其餘 95% 還是打舊版本。權重可以隨監控結果逐步調高,出事的話直接把 canary-weight 調回 0,流量立刻全部回到舊版本。

藍綠部署(Blue-Green Deployment)
目的:保留完整的舊環境(Blue),新版本在另一套環境(Green)跑起來確認沒問題後,一次性整批把流量切過去,不是像 Canary 那樣逐步遞增。

aws elbv2 modify-listener \
  --listener-arn arn:aws:elasticloadbalancing:ap-northeast-1:123456789012:listener/app/chatapp-alb/abc123/def456 \
  --default-actions Type=forward,TargetGroupArn=arn:aws:elasticloadbalancing:ap-northeast-1:123456789012:targetgroup/chatapp-green/xyz789

這個指令把 ChatApp 的 ALB 預設轉發目標從 chatapp-blue 換成 chatapp-green,是一次性整批切換,不像 Canary 是漸進調權重。優點是回滾也是整批切回,行為單純;缺點是無法用小流量驗證,風險比 Canary 集中。

A/B Testing
目的:比較不同版本的業務指標(轉換率、留存率),通常兩個版本都是「已知穩定」的版本,重點不是驗證穩定性,而是看哪個效果更好。

def get_variant(user_id: str) -> str:
    bucket = hash(user_id) % 100
    return "B" if bucket < 50 else "A"

user_id 做穩定的 hash 分桶,確保同一個使用者每次都拿到同一個版本,這樣業務數據才不會因為版本跳來跳去而失真。

Feature Flag(功能旗標)
目的:在應用程式碼層面用開關控制功能是否顯示,不需要重新部署或動流量層設定。

if feature_flags.is_enabled("ai_chatbot", user_id=current_user.id):
    render_ai_chatbot_widget()

Feature Flag 常常被拿來跟灰度發布搭配用, 先用 Canary 把新版本 Pod 部署上去,Pod 裡的程式碼再用 Feature Flag 決定要不要開放給這批使用者。兩者是互補關係,不是互斥的。

(三)灰度發布的三種常見分流維度

流量比例(Traffic Percentage):最常見,對應 Canary weight、ALB weighted target group。
使用者屬性(User Attribute):白名單、VIP、內部員工優先看到新版本,通常透過 Request Header 或 Cookie 判斷。
客戶端/裝置版本(Client Version):特別是 Android/iOS App,使用者不會同時升級,Server 端要能同時服務多個 App 版本共存。這是純 Web 服務不太會遇到的維度,後續文章會針對這塊展開。

行動 App 的提醒:Server 端做了 Canary,不代表 Android/iOS 使用者馬上看得到差異。 App Store 的分階段發布(Staged Rollout / Phased Release)是另一條獨立的控制軸,兩者要分開規劃,不能只靠 server 端流量分流就以為做完了灰度發布。

(四)灰度發布不只是前端:後端服務模組同樣適用

灰度發布常常被誤以為只是前端在做畫面切版,但它實際作用的對象是「服務」,不分前端後端。以 ChatApp 為例,後端的訊息服務、認證服務,一樣可以各自獨立做 Canary rollout:例如訊息服務要導入一個新的推播機制,可以先讓 5% 的訊息流量走新版本的訊息服務 Pod,前端(iOS/Android/Web)完全感知不到後端在做灰度發布,因為前端呼叫的還是同一個 API endpoint,差別只在背後有一部分 request 被導到新版本的 Pod。

換句話說,一個完整的產品線裡,前端、後端、甚至資料庫 schema migration,都可以各自套用灰度發布的概念,只是「切換的對象」跟「要盯的監控指標」不一樣:

  • 前端灰度:使用者直接看得到的 UI/UX 變化,例如新版聊天介面
  • 後端服務灰度:API 行為或效能的變化,使用者通常感知不到,但穩定性風險一樣存在,甚至因為看不到,更容易被忽略監控
  • 資料庫 schema 灰度:新舊 schema 並存期間的相容性議題,下一節細說

實務上,前端跟後端建議不要同時做灰度發佈,比較常見的做法是先讓後端 API 用向下相容的方式擴充完並穩定運行到 100%(作為對照基準),再開始讓前端逐步灰度切換過去使用新版本 API;反過來,如果是純前端的 UI 改動、API 完全沒變,就只灰度發佈前端,後端維持不動。這樣做的原因很單純:如果前端跟後端同時各自灰度發佈,出現異常時很難判斷問題是出在前端新版本、後端新版本,還是兩者搭配才會發生的組合問題... canary rollout 分析的前提是「一次只驗證一個變因」,兩邊同時變動會讓監控指標失去歸因能力。

如果這次改動確實前後端密不可分、非得一起上線不可,比較穩妥的做法是用 Feature Flag 把「部署(Deploy)」跟「發布(Release)」拆開:先把前後端的新版本都完整部署到 100%,但功能保持關閉狀態,確認兩邊各自運行穩定後,再用同一個 Feature Flag 一次性開啟功能。這樣實際上仍然只有一個變因在切換(Flag 開或關),而不是兩套獨立的流量分流機制同時運作。

(五)灰度發布跟資料庫:新舊版本並存時,資料怎麼辦

上一節提到,前端跟後端建議不要同時做灰度、一次只讓一邊在動。這也代表「資料庫層會不會受影響」要分兩種情境來看,因為兩種情境下,實際承受相容性壓力的角色不一樣。

情境一:後端在灰度中(未達 100%),前端版本固定不動

這是資料庫相容性風險真正發生的地方。此時同一個資料庫,同時有跑舊程式碼的 V1 backend Pod、跟跑新程式碼的 V2 backend Pod 在讀寫,前端呼叫的還是同一個 API endpoint,只是背後有一部分 request 被導到 V2 處理。以 ChatApp 的訊息記錄為例:如果 V2 版本在寫入訊息時多寫了一個欄位(例如已讀狀態),V1 backend Pod 完全不知道這個欄位存在。如果沒處理好向後相容性,可能出現:

  • V1 backend Pod 讀到 V2 寫入的資料,因為預期欄位對不上而處理失敗
  • V2 backend Pod 讀取到 V1 寫入、根本沒有新欄位的舊資料,因為預期欄位不存在而丟出錯誤

這裡的解法是資料庫 schema 變更要遵守 Expand-Contract Pattern(擴張-收縮模式)

  1. Expand 階段:只新增欄位,不刪除、不改變既有欄位的型別或語意。V2 開始寫入新欄位,V1 忽略新欄位,兩邊都能正常運作。
  2. 過渡階段:V1、V2 並存,新欄位設計成 nullable 或給預設值,確保 V1 寫入時不會因為缺少新欄位而失敗。
  3. Contract 階段:等確認 V1 流量已經降到 0%(也就是後端灰度完成、全部切到 V2),才能安全地移除舊欄位或做破壞性變更。

Expand-Contract 是新增或淘汰欄位這類小範圍變更最常用、也最基礎的模式,但不是資料庫相容性問題的唯一解法。變更幅度更大的時候,通常會搭配其他技巧一起用:

  • 訊息格式的向後/向前相容規則:如果 WebSocket 傳輸或跨服務溝通用的是 Protobuf、Avro 這類結構化格式,本身就有一套 schema evolution 規則(只能新增 optional 欄位、不能重複使用或修改既有欄位編號),比在資料庫層單獨處理更早一層把相容性鎖住。
  • 雙寫(Dual Write):如果是搬資料表、換資料庫,甚至是微服務拆分導致資料要搬到新的 owner 服務,通常會讓新舊兩套資料同時寫入一段時間,搭配 CDC(Change Data Capture,例如 Debezium)做資料同步,確認新舊資料一致後再切斷舊寫入路徑。這種變更幅度比單純加欄位大得多,Expand-Contract 三階段不夠用。
  • Online Schema Change 工具:Expand-Contract 講的是「什麼時候、用什麼順序」改 schema,但實際執行 DDL 本身如果資料表很大,直接下 ALTER TABLE 可能鎖表造成服務中斷,這時候會搭配 gh-ostpt-online-schema-change(MySQL)之類的線上改表工具,或雲端資料庫原生的 Blue/Green Deployments 功能(例如 AWS RDS Blue/Green Deployments),來避免鎖表。
  • 應用層版本轉接(Repository/DAO 抽象層):在服務內加一層 Repository/DAO,由這層負責把底層 schema 轉換成統一的介面格式,V1、V2 的程式碼都透過這層存取資料,不用各自知道底層 schema 現在是新是舊。

哪一種變更該搭配哪些技巧,取決於變更幅度:單純加欄位,Expand-Contract 就夠;搬資料表或換資料庫,才需要上雙寫跟 CDC;資料表太大鎖表有疑慮,才需要上線上改表工具。

重要提醒:資料庫 schema 的破壞性變更(例如砍欄位、改型別)絕對不要在後端灰度期間直接做,一定要先進入 Contract 階段、確認 V1 已經沒有流量,才能安全執行。

情境二:前端在灰度中(未達 100%),後端 API 版本固定不動

這種情境下,因為所有前端版本(不管新舊)打的都是同一套、行為固定的後端 API,資料庫幾乎不會因為前端灰度進度而受影響, 寫入資料庫的程式碼路徑從頭到尾只有一種,不存在「兩套程式碼同時讀寫」的問題。

但這裡有一個常見的錯誤要提醒:如果這次前端改版需要後端提供新欄位或新行為才能運作,就代表它其實已經不是「純前端灰度」,而是前後端有相依關係。正確的順序應該是延續上一節的做法——先讓後端把新能力擴充完(也就是 Expand-Contract 的 Expand 階段)並穩定運行到 100%,確認資料庫跟 API 都準備好之後,才輪到前端開始灰度切換去使用這個新能力,而不是讓前端搶先上線一個後端還沒支援的功能。

補充:Android/iOS App 的長期多版本並存,跟「灰度發布中」是兩回事

即使團隊沒有在主動做灰度發布,Android/iOS App 因為使用者不會同時升級,本來就長期存在「多個 App 版本同時在線上」的現實,這個現象可能持續數週甚至數月,直到大多數使用者升級完成才會消失——時間跨度通常比一次灰度發布本身還要長。這代表即使使用新版本的使用者 A 跟使用舊版本的使用者 B 互相聊天,WebSocket 傳輸的 payload 格式跟資料庫儲存的 schema,都必須讓兩個版本的 client 長期並存也能正確解析。這跟情境一討論的「團隊主動控制的後端灰度」是兩件事,但需要的向後相容(backward compatible)與向前相容(forward compatible)設計原則是共通的。這塊細節,會在後面談 WebSocket 長連線的那篇文章裡展開。

(六)灰度發布的整體框架

不管底層用哪種具體做法,一個完整的灰度發布流程都會落在這三層裡:

灰度發布三層框架示意圖:流量控制層、監控回饋層、決策層
灰度發布的三層框架:流量控制層、監控回饋層、決策層

流量控制層(Traffic Control Layer):Ingress annotation、Service Mesh(Istio、AWS App Mesh)、ALB weighted target group、App Store 分階段發布。
監控回饋層(Observability Layer):Prometheus + Grafana、CloudWatch,關注錯誤率、延遲,以及跟業務相關的指標(例如通訊類 App 的訊息送達率)。
決策層(Decision Layer):人工判斷是否擴大流量比例,或用 Flagger 這類工具做自動化 canary analysis,指標不過門檻就自動回滾。

(七)決策表

情境建議策略
想壓低新版本風險,且要能秒級回滾Canary
版本差異大,需要保留完整舊環境隨時整批切回藍綠部署
想比較兩個版本的業務指標,而不是單純驗證穩定性A/B Testing
想在不重新部署的情況下,對特定用戶群體開關功能Feature Flag
產品有 Android/iOS App,存量版本無法強制升級Client-side 分階段發布 + API 向下相容,不能只做 server 端 Canary
資料庫 schema 有不可逆的破壞性變更先做 Expand 階段 + 雙寫,確認舊版本流量清空後才進入 Contract 階段,不要一次到位遷移

這三個問題作爲自我驗證:

  1. 你在意的是「這個版本會不會down」還是「這個版本效果好不好」?前者用 Canary/藍綠部署,後者用 A/B Testing。
  2. 你的使用者端(瀏覽器 vs App)能不能被強制重新整理拿到新版本?不能的話(Android/iOS),server 端 Canary 要另外搭配 client-side 分階段發布才夠。
  3. 出事的時候,你要的是「秒級調回 0% 流量」還是「整批切回舊環境」?前者是 Canary 的強項,後者是藍綠部署的強項。

結論

名詞釐清完之後,之後會繼續在 AWS EKS 上,用 Nginx Ingress Controller 設定一個真正的 Canary 流量分流,並帶進 Prometheus 監控指標;另外, Web 通訊 WebSocket 長連線在流量切換時的斷線問題、Android/iOS App 版本碎片化下該怎麼做灰度發布,以及新舊版本並存時的訊息協定相容性細節在後續也會進一步討論。

常見問題 FAQ

灰度發布和 A/B Testing 差在哪?我看很多文章把兩者混著用

灰度發布(含 Canary、藍綠部署)的目的是降低新版本上線的風險,關注的是「這個版本穩不穩」;A/B Testing 的目的是比較兩個已知穩定版本的業務效果,關注的是「哪個版本效果比較好」。兩者可以先後串接使用:先用 Canary 確認新版本穩定,再對穩定版本做 A/B Testing。

Canary 發布和藍綠部署可以一起用嗎?

可以。常見做法是新版本先用藍綠部署整套環境獨立跑起來,再用 Canary 的流量權重機制,把 Green 環境的流量從 0% 慢慢調高,兼顧「環境隔離」跟「漸進驗證」兩種優點。

Feature Flag 算不算灰度發布的一種?

算是灰度發布傘狀概念底下的一種手段,但作用層次不同:Canary/藍綠部署是在流量或部署層做切換,Feature Flag 是在應用程式碼層做開關,不需要重新部署或動流量設定。兩者常搭配使用,不是互斥選項。

灰度發布一定要導入 Service Mesh(Istio)才能做嗎?我們只有簡單的 Nginx Ingress

不一定。Nginx Ingress Controller 的 canary annotation 就能做基本的流量權重分流,適合單一維度(例如流量比例)的簡單場景。如果需要同時依多個條件(Header、Cookie、地區)分流,或需要自動化的 canary analysis,才會需要 Istio、AWS App Mesh 或 Flagger 這類更進階的工具。

灰度發布時,新舊版本的使用者互相聊天,訊息在資料庫會不會出問題?

如果資料庫 schema 變更遵守 Expand-Contract Pattern(只新增欄位、不做破壞性變更,等舊版本流量清空才收斂),並且 WebSocket 傳輸協定有做好向後相容與向前相容,新舊版本互聊是不會出問題的。風險通常出在跳過過渡階段、直接對 schema 做破壞性變更,導致舊版本讀不到或解析不了新格式的訊息。

留言

熱門文章

Docker 環境下的 Proxy 配置