DevOps – 從漸進式交付說起(含實踐 Demo)

作者:CODING – 王煒

1. 開篇

如果讓你主導一款千萬、甚至億級用戶產品的功能迭代,你會怎麼做?你需要面對的挑戰可能來自於:

商業戰略的變化帶來新的產品訴求,而產品的任何改動哪怕僅是介面調整,都將面臨無數存量用戶的挑戰

這時候,作為產品負責人,你會選擇穩定壓倒一切?還是自我革新,繼續追求用戶和市場的價值呢?筆者通過對 Facebook、Twitter 等互聯網巨頭的調研,試圖窺探他們在瞬息萬變的市場中仍然保持「穩定」迭代的秘密 – 漸進式交付

通過本篇文章,你將收穫:

  1. 什麼是漸進式交付,為什麼 DevOps 能夠天然與其結合
  2. 為什麼漸進式交付能賦予大規模組織下的產品持續交付及穩定迭代的能力
  3. 小項目,大項目同樣適用的實踐經驗

2. 什麼是漸進式交付

移動互聯網時代的爆發,誕生了一大批巨型互聯網企業和項目,部分大型項目的技術複雜程度和組織複雜程度甚至不亞於傳統的工業項目,為了實現對這些項目的管理和迭代,我們試圖將目光投向已經完成工業革命的傳統工業尋找答案。

而「漸進式交付」一詞最早起源於大型、複雜的工業化項目,比如:鐵路、汽車和軍事產業、新基建的 5G 網路產業等等。

它和 DevOps 的目標一致,試圖將複雜的工程化項目進行分階段拆解,通過持續進行小型迭代閉環,降低交付成本和節約交付時間

可查詢的資料顯示,「漸進式交付」流行於互聯網產品是在近兩年 Kubernetes 以及雲原生大規模使用之後。這兩項技術的出現,為「漸進式交付」在互聯網的應用提供了基礎設施和實現方法。

DevOps 是「漸進式交付」的實現手段,而其中的「流水線」為「漸進式交付」提供了實現途徑

在產品的迭代過程,可以將「漸進式交付」的具體行為附著在「流水線」中,將整條交付「流水線」看做是產品迭代的一個過程和一次「漸進式交付」周期。

說了這麼多「漸進式交付」的理論基礎,在實踐中又是以哪些技術方法落地呢?

  1. A/B 測試
  2. 金絲雀 / 灰度發布

以 Facebook 為例,每次發布重大功能,都會經歷一次典型的「漸進式交付」過程:

  1. 迭代發布
  2. 公司全員 A/B 測試
  3. 特定用戶 A/B 測試
  4. 灰度發布
  5. 全量發布

這種漸進式交付的好處是,對於新迭代正式推向市場前提供了灰度用戶的數據支撐,幫助決策者充分了解用戶傾向和市場訴求。

在「漸進式交付」的過程中,A/B 測試環節以及灰度發布環節都可以根據用戶數據和市場回饋決定是否進入全量發布,這種方式既能夠保證迭代敏捷進行,又能夠保證迭代的用戶和市場安全性。

2.1 A/B 測試

例如通過對用戶畫像中地理位置和性別組合條件進行 A/B 測試,使其訪問新版本,而其他的用戶則繼續訪問舊版。一段時間後,研究用戶行為數據和用戶體驗報告,決定功能是否繼續進入下一個發布環節。

2.2 金絲雀 / 灰度發布

使用特定分流技術使流量由新老版本共同承擔,如典型的「MurmurHash」演算法

3. 技術價值和商業價值

從原理上來看,這些技術並不是多麼新的技術,比如 A/B 測試,我們用最原始的方式:業務程式碼增加邏輯判斷條件也可實現,但為什麼沒有大規模運用呢?

原因很簡單:純業務程式碼的實現依賴於技術開發,需求方無法自主控制 A/B 測試的環境和條件,這種過度依賴於技術開發並不能帶來規模化的運用。

我們需要的是一種完全脫離業務程式碼的實現方式,最好能以自動化/半自動化實現,並且盡量能把這個動作加入到已有的內部流程內

現在,有了雲原生和 Kubernetes 的支援,結合 DevOps 的流水線,自動化的漸進式交付成為了可能。

我們參考 Facebook 的發布方式,設計了這個 Pipeline Demo

它主要實現了:

  1. 提交程式碼後自動執行單元測試,並構建 Docker 鏡像
  2. 將 Docker 鏡像推送到私有製品庫,自動觸發流水線
  3. 執行 K8S Job Migrate 資料庫(如果有改動),並部署新版到預發布環境
  4. 人工確認:發布生產環境前是否進行 A/B 測試
  5. A/B 測試通過後,設置灰度發布的比例,自動灰度發布
  6. 人工確認:是否全量發布生產環境
  7. 生產環境自動配置限流和熔斷策略,保證生產穩定

最終實現的效果圖:

1. 提交程式碼後自動構建鏡像、單元測試、推送到鏡像倉庫並觸發 CD 流水線。


2. 執行 K8S Job 對預發布環境資料庫自動 Migrate,並發布到預發布環境。

3. 此時通過訪問 //dev.coding 可以訪問到新發布的服務。

4. 人工確認:發布生產環境前是否進行 A/B 測試。

5. 在本例,發布後以 Header 包含 location=shenzhen 作為區分 A/B 測試流量。

在瀏覽器內直接請求 //pro.coding,流量仍然分流到生產環境。在 Postman 攜帶 location=shenzhen 的 Header 請求,可以發現流量被分流到「A/B 測試環境」。

6. 人工確認:設置期望的灰度發布比例,自動灰度發布,如選擇灰度發布比例為 50%。

7. 請求 //pro.coding ,灰度發布環境和生產環境將以 1:1 的流量比例對外提供服務。

8. 人工確認:全量發布生產環境。

9. 此時請求 //pro.coding 訪問的是生產環境新版本。

10. 自動配置限流和熔斷策略,保證生產穩定性。通過壓力測試可看到並發請求有一部分被限流。

HttpCode=429,代表 Too Many Requests ,拒絕服務。

對於開發人員,這種漸進式交付經過多輪的灰度、A/B 測試,最大程度減少程式碼 BUG 發布到生產環境

對於運維人員,這種幾乎全自動的交付方式改變了手動修改 yaml 文件,手動 apply 的麻煩,最大程度減少發布產生的人為錯誤。通過自動觸發的方式,減小了與開發的溝通成本

對於產品經理和運營人員,產品迭代不再是靠內部團隊「YY」,而是基於實際用戶體驗數據,了解新功能對於用戶和市場的回饋,最大程度減小新功能的用戶和市場風險

4. 動手實踐

4.1 概覽

  1. 準備一個 K8S 集群,推薦使用騰訊雲容器服務
  2. K8S 集群部署 Traefik 替換 nginx-Ingress 作為 Ingress Gateway,提供更好的流量治理能力;
  3. 開通 CODING DevOps,提供鏡像構建和流水線的部署能力;
  4. 克隆示例程式碼並推送到自己的 CODING 程式碼倉庫;
  5. 複製模板創建部署流水線;
  6. 盡情測試。

4.2 實踐步驟

4.2.1 克隆源程式碼並創建構建計劃

  1. 克隆源程式碼並推送到自己的 CODING 倉庫

    git clone //e.coding.net/wangweicoding/cd-production.git
    git remote remove origin
    git remote add origin 你的 CODING 倉庫地址
    git push origin master
    
  2. 創建構建計劃

  • 使用 cd-production 程式碼倉庫內的 Jenkinsfile 創建構建計劃

4.2.2 開通騰訊雲容器服務

  • 開通集群,並在 CODING DevOps 新建 K8S 集群憑證(如有必要,請允許集群外網訪問)

4.2.3 通過 CODING DevOps 初始化 Traefik

  • 複製程式碼倉庫 cd-productioncoding-templates/traefik.json 內容,並在 部署控制台 創建 pipeline,點擊「編輯 JSON 配置」,將內容複製到輸入框。


點擊「Update Pipeline」後,自動創建了對應的 Pipeline。

注意請將每一個階段的雲帳號修改為自己的雲帳號。
再點擊「保存」即完成 Traefik 初始化的 Pipeline 創建,返回後,點擊「立即啟動」完成集群 Traefik 的初始化。

進入「騰訊雲」容器服務,打開集群Service,點擊命名空間 traefik-system ,找到名為 traefik-ingress 的 IP 地址,並在本機新建兩個 Host 規則:

IP地址 dev.coding
IP地址 pro.coding

這樣在本地通過訪問 dev.coding 就可以訪問發布的服務了。

4.2.4 通過 CODING DevOps 初始化 Mysql

  • 以上述同樣的操作複製程式碼倉庫 cd-productioncoding-templates/mysql.json 內容,並在每一個階段修改為自己的「雲帳號」,創建 Pipeline 並啟動完成集群 Mysql 的初始化。

4.2.5 創建漸進式交付流水線

在創建漸進式交付流水線之前,請先開通 CODING 製品庫,開通完成後,請按照指引在本地使用 cd-productionDockerfile 構建鏡像並推送至「製品庫」

隨後以上述同樣的操作複製程式碼倉庫 cd-productioncoding-templates/devops.json 內容,創建漸進式交付的 Pipeline。

請將「配置」階段的「啟動所需製品」修改為自己的 CODING 項目Git 倉庫、鏡像倉庫、鏡像。
請將「配置」階段的「自動觸發器」修改為自己的 CODING 項目、鏡像倉庫、鏡像。
請將每一個階段的雲帳號修改為自己的雲帳號。

嘗試修改項目 index.html 並推送,觸發流水線。

5. 項目說明與核心原理

5.1 項目說明

├── Dockerfile
├── Jenkinsfile  # CODING CI 構建腳本
├── Pipfile
├── Pipfile.lock
├── README.md
├── app.py
├── coding-templates
│   ├── devops.json  # CODING CD 漸進式交付模板
│   ├── mysql.json    # CODING CD Mysql 初始化模板
│   └── traefik.json   # CODING CD Traefik 初始化模板
├── config.py
├── database_version.py
├── devops
│   ├── README.md
│   ├── mysql
│   │   ├── dev
│   │   │   ├── mysql-deployment.yaml
│   │   │   └── mysql-pv.yaml
│   │   └── pro
│   │       ├── mysql-deployment.yaml
│   │       └── mysql-pv.yaml
│   └── traefik
│       ├── deployment
│       │   ├── configmap.yaml
│       │   └── deployment.yaml
│       ├── deployment.yaml
│       ├── open-treafik.yaml
│       └── router
│           ├── dev
│           │   └── flask-dev.yaml      # Dev 環境的 Traefik IngressRoute 規則
│           └── pro
│               ├── circuitbreaker.yaml  # Pro 環境的 Traefik 熔斷規則
│               ├── flask-abtest.yaml    # Pro 環境的 A/B Testcase
│               ├── flask-pro-all.yaml   # Pro 環境的 Traefik IngressRoute 規則
│               ├── flask-pro.yaml
│               ├── mysql-ratelimit.yaml
│               ├── mysql-tcp-router.yaml
│               └── ratelimit.yaml         # Pro 環境的 Traefik 限流規則
├── flask_test.py
├── k8s-canary                             # 灰度發布環境的 K8S Manifest
│   ├── deployment.yaml                
│   └── nodeport-canary.yaml
├── k8s-dev                                 # Dev 環境的 K8S Manifest
│   ├── deployment.yaml                
│   ├── migrate-mysql-job.yaml     # Dev 環境的 Migrate Database K8S Job
│   ├── nodeport-service.yaml
│   └── service.yaml
├── k8s-pro                                 # Pro 環境的 K8S Manifest
│   ├── deployment.yaml
│   ├── migrate-mysql-job.yaml
│   └── nodeport-service.yaml
├── manage.py
├── migrations
│   ├── README
│   ├── alembic.ini
│   ├── env.py
│   ├── script.py.mako
│   └── versions
│       ├── 95585fe4b611_initial_migration.py
│       └── fece98dad497_second_migrate.py
├── requirements.txt
└── templates
  └── index.html                           # 項目發布首頁

5.2 核心原理

在這個例子中,我們使用了 Traefik 作為集群網關,使用 Router 對 Host dev.codingpro.coding 進行匹配,使流量按照不同發布階段進行不同的分配。

5.2.1 Dev 環境架構圖

訪問 dev.coding 時,Router 匹配到此 Host 規則,將流量轉發到名為k8s-flask-nodeportService(即 Dev 環境的 Service)。

Traefik Router 核心配置程式碼為:

apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
  name: k8s-flask-dev
  namespace: dev
spec:
  entryPoints:
  - web
    routes:
  - kind: Rule
    match: Host(`dev.coding`)
    services:
    - name: k8s-flask-nodeport
      port: 8080
      namespace: dev

5.2.2 A/B 測試環境架構圖

訪問 pro.coding 時,Router 匹配到此 Host 規則,並檢查 Header 是否匹配,並將根據匹配結果決定將流量轉發到 k8s-flask-canary 或者 k8s-flask 兩個不同環境的 Services。

A/B 測試 Traefik Router 核心配置程式碼為:

apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
  name: k8s-flask-abtest
  namespace: pro
spec:
  entryPoints:
  - web
    routes:
  #A/B test
  - kind: Rule
    match: Host(`pro.coding`) && Headers(`location`, `shenzhen`)
    services:
    - name: k8s-flask-canary
      port: 8080
      namespace: pro

5.2.3 灰度發布架構圖

訪問 pro.coding 時,Router 匹配到此 Host 規則,並根據配置的 Weight 權重,將流量按比例轉發到 k8s-flask-canary 或者 k8s-flask Service

例如以 1:1 的比例分配灰度比例,Traefik Router 核心配置程式碼為:

apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
  name: k8s-flask-pro
  namespace: pro
spec:
  entryPoints:
  - web
    routes:
  #canary deploy
  - kind: Rule
    match: Host(`pro.coding`)
    services:
    - name: k8s-flask
      port: 8080
      namespace: pro
      weight: 50    # 權重比例
    - name: k8s-flask-canary
      port: 8080
      namespace: pro
      weight: 50    # 權重比例

當全量發布生產環境的時候,只需要將 Canary 環境的 Weight 權重設置為 0,即所有流量都轉發到生產環境。

5.2.4 熔斷和限流架構圖

在生產環境,我們一般使用限流和熔斷技術來應對流量激增,犧牲部分用戶的體驗來保證生產環境的穩定。

Traefik 內熔斷和限流是通過配置 middlewares 來實現,對流量進行匹配後,再進行中間件二次流量確認。

Traefik Middlewares 限流核心配置:

apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
  name: flask-k8s-traffic
  namespace: pro
spec:
  rateLimit:
    # 1s 內接收的請求數的平均值不大於500個,高峰最大1000個請求
    burst: 1000
    average: 500

Traefik Middlewares 熔斷核心配置:

# Latency Check
apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
  name: k8s-flask-breaker
  namespace: pro
spec:
  circuitBreaker:
    expression: LatencyAtQuantileMS(50.0) > 100
    # 50% 的請求比例響應時間大於 100MS 時熔斷

6. 小結

Kubernetes 和 Service Mesh 的出現,給 DevOps 帶來了更多可能,漸進式交付只是一種藉助其便利性的比較典型的發布方式。

我們藉助了 Traefik 作為集群網關,通過分流技術實現了 A/B 測試和灰度發布,當然,你也可以引入 Service Mesh 使用 Istio 管理集群流量,藉助 Virtual ServiceDestination Rule 實現同樣效果。

藉助 CODING DevOps 的能力,我們將「推送程式碼」、「構建鏡像」、「觸發部署流程」進行打通,實現了自動化的 DevOps 能力。

當然,還可以做到更多有價值的發布流程,比如:

  • 運營、產品人員可以實現很方便地隨時修改 A/B Testcase 進行分流測試,只需要配置一個修改 A/B Testcase 的 Pipeline,輸入相關的分流指標,並運行即可生效;
  • 除了 A/B Testcase,灰度發布也變成了實時可控的數值,甚至可以實現一個「漸進式灰度發布」的 Pipeline,增加 Wait 階段實現灰度比例隨著時間推移自動增加;
  • 可以很方便地實現一個自動回滾的 Pipeline,通過輸入版本號就可以實現自動回滾到對應的版本,如使用資料庫 ORM 產品,甚至可以實現資料庫的自動回滾;
  • Traefik 提供的熔斷和限流能力,結合 Pipeline 的 Webhook 觸發以及監控系統如 Prometheus 聯動,可以實現業務系統壓力較大的時候自動觸發熔斷和限流 Pipeline 改變限流策略,保證生產環境的穩定性。

7. 資源鏈接和參考資料