Spark 灰度釋出在十萬級節點上的成功實踐 CI CD
本文所述內容基於某頂級網際網路公司數萬節點下 Spark 的 CI 與 CD & CD 實踐。為了提高本文內容的可借鑑性,隱去了公司特有內容,只保留通用部分
Spark CI 持續整合實踐
CI 介紹
持續整合是指,及時地將最新開發的且經過測試的程式碼整合到主幹分支中。
持續整合的優點
- 快速發現錯誤 每次更新都及時整合到主幹分支中,並進行測試,可以快速發現錯誤,方便定位錯誤
- 避免子分支大幅偏離主幹分支 主幹在不斷更新,如果不經常整合,會產生後期整合難度變大,甚至難以整合,並造成不同開發人員間不必要的重複開發
- 為快速迭代提供保障 持續整合為後文介紹的持續釋出與持續部署提供了保證
Spark CI 實踐
目前主流的程式碼管理工具有,Github、Gitlab等。本文所介紹的內容中,所有程式碼均託管於私有的 Gitlab 中。
鑑於 Jenkins 幾乎是 CI 事實上的標準,本文介紹的 Spark CI CD & CD 實踐均基於 Jenkins 與 Gitlab。
Spark 原始碼儲存在 spark-src.git 庫中。
由於已有部署系統支援 Git,因此可將整合後的 distribution 儲存到 Gitlab 的釋出庫(spark-bin.git)中。
每次開發人員提交程式碼後,均通過 Gitlab 發起一個 Merge Requet (相當於 Gitlab 的 Pull Request)
每當有 MR 被建立,或者被更新,Gitlab 通過 Webhook 通知 Jenkins 基於該 MR 最新程式碼進行 build。該 build 過程包含了
- 編譯 Spark 所有 module
- 執行 Spark 所有單元測試
- 執行效能測試
- 檢查測試結果。如果有任意測試用例失敗,或者效能測試結果明顯差於上一次測試,則 Jenkins 構建失敗
Jenkins 將 build 結果通知 Gitlab,只有 Jenkins 構建成功,Gitlab 的 MR 頁面才允許 Merge。否則 Gitlab 不允許 Merge
另外,還需人工進行 Code Review。只有兩個以上的 Reviewer 通過,才能進行最終 Merge
所有測試與 Reivew 通過後,通過 Gitlab Merge 功能自動將程式碼 Fast forward Merge 到目標分支中
該流程保證了
- 所有合併進目標分支中的程式碼都經過了單元測試(白盒測試)與效能測試(黑盒測試)
- 每次發起 MR 後都會及時自動發起測試,方便及時發現問題
- 所有程式碼更新都能及時合併進目標分支
Spark CD 持續交付
CD 持續交付介紹
持續交付是指,及時地將軟體的新版本,交付給質量保障團隊或者使用者,以供評審。持續交付可看作是持續整合的下一步。它強調的是,不管怎麼更新,軟體都是可隨時交付的。
這一階段的評審,一般是將上文整合後的軟體部署到儘可能貼近生產環境的 Staging 環境中,並使用貼近真實場景的用法(或者流量)進行測試。
持續釋出的優點
- 快速釋出 有了持續整合與持續釋出,可快速將最新功能釋出出來,也可快速修復已知 bug
- 快速迭代 由於釋出及時,可以快速判斷產品是否符合產品經理的預期或者是否能滿足使用者的需求
Spark CD 持續釋出實踐
這裡有提供三種方案,供讀者參考。推薦方案三
方案一:單分支
正常流程
如下圖所示,基於單分支的 Spark 持續交付方案如下
- 所有開發都在
spark-src.git/dev
(即 spark-src.git 的 dev branch) 上進行 - 每週一將當前最新程式碼打包,放進
spark-bin.git/dev
的spark-${ build # }
(如圖中第 2 周的 spark-72)資料夾內 - spark-prod 指向當前 spark-dev 指向的資料夾(如圖中的 spark-71 )
- spark-dev 指向
spark-${ build # }
(如圖中的 spark-72) - 自動將 spark-bin.git 最新內容上線到 Staging 環境,並使用 spark-dev 進行測試
- spark-prod 比 spark-dev 晚一週(一個 release 週期),這一週用於 Staging 環境中測試
注:
- 藍色圓形是正常 commit
- 垂直虛線是釋出時間點,week 1、week 2、week 3、week 4
- 最上方黑色粗橫線是原始碼時間線
- 下方黃色粗橫線是 release 時間線
- 綠色方框是每週生成的 release,帶 build #
- 藍色方框是開發版本的 symbolic
- 橘色方框是線上版本的 symbolic
bug fix
在 Staging 環境中發現 spark-dev 的 bug 時,修復及整合和交付方案如下
- 如果在 Staging 環境中發現了 spark-dev 的 bug,且必須要修復(如不修復,會帶到下次的 spark-prod 的 release 中),則提交一個 commit,並且 commit message 包含 bugfix 字樣(如圖中黑色圓形 commit 9 所示)
- 該 bugfix 被 Merge 後,Jenkins 得到通知
- Jenkins 發現該 commit 是 bugfix,立即啟動構建,生成
spark-${ build \# }
(如圖中的 spark-73) - spark-dev 指向
spark-${ build \# }
(如圖中的 spark-73 )
hot fix
生產環境中發現 bug 時修復及交付方案如下
- 如果發現線上版本(即 spark-prod)有問題,須及時修復,則提交一個 commit,並且 commit message 包含 hotfix 字樣 (如圖中紅色圓形 commit 9 所示)
- 該 hotfix 被 Merge 後,Jenkins 得到通知
- Jenkins 發現該 commit 是 hotfix,立即啟動構建,生成
spark-${ build \# }
(如圖中的 spark-73) - spark-dev 與 spark-prod 均指向
spark-${ build \# }
(如圖中的 spark-73 )
Pros.
- spark-src.git 與 spark-bin.git 都只有一個分支,維護方便
- spark-prod 落後於 spark-dev 一週(一個 release),意味著 spark-prod 都成功通過了一週的 Staging 環境測試
Cons.
- 使用 spark-prod 與 spark-dev 兩個 symbolic,如果要做灰度釋出,需要使用者修改相應路徑,成本較高
- hotfix 時,引入了過去一週多(最多兩週)未經 Staging 環境中通過 spark-dev 測試的 commit,增加了不確定性,也違背了所有非 hotfix commit 都經過了一個釋出週期測試的原則
方案二:兩分支
正常流程
如下圖所示,基於兩分支的 Spark 持續交付方案如下
spark-src.git
與spark-bin.git
均包含兩個分支,即 dev branch 與 prod branch- 所有正常開發都在
spark-src.git/dev
上進行 - 每週一(如果是 weekly release)從
spark-src.git/dev
打包出一個 release 放進spark-bin.git/dev
的spark-${ build \# }
資料夾內(如圖中第 2 週上方的的 spark-2 )。它包含了之前所有的提交(commit 1、2、3、4) spark-bin.git/dev
的 spark 作為 symbolic 指向spark-${ build \# }
資料夾內(如圖中第 2 週上方的的 spark-2)spark-src.git/prod
通過 fast-forward merge 將spark-src.git/dev
一週前最後一個 commit 及之前的所有 commit 都 merge 過來(如圖中第 2 周需將 commit 1 merge 過來)- 將
spark-src.git/prod
打包出一個 release 放進spark-bin.git/prod
的spark-${ build \# }
資料夾內(如圖中第 2 周下方的的 spark-1 ) spark-bin.git/prod
的 spark 作為 symbolic 指向spark-${ build \# }
bug fix
在 Staging 環境中發現了 dev 版本的 bug 時,修復及整合和交付方案如下
- 在
spark-src.git/dev
上提交一個 commit (如圖中黑色的 commit 9),且 commit message 包含 bugfix 字樣 - Jenkins 發現該 commit 為 bugfix 後,立即構建,從
spark-src.git/dev
打包生成一個 release 並放進spark-bin.git/dev
的spark-${ build \# }
資料夾內(如圖中第二週與第三週之間上方的的 spark-3 ) spark-bin.git/dev
中的 spark 作為 symbolic 指向spark-${ build \# }
hot fix
在生產環境中發現了 prod 版本的 bug 時,修復及整合和交付方案如下
- 在
spark-src.git/dev
上提交一個 commit(如圖中紅色的 commit 9),且 commit message 包含 hotfix 字樣 - Jenkins 發現該 commit 為 hotfix 後,立即將
spark-src.git/dev
打包生成 release 並 commit 到spark-bin.git/dev
的spark-${ build \# }
(如圖中上方的 spark-3 )資料夾內。 spark 作為 symbolic 指向該spark-${ build \# }
- 通過 cherry-pick 將 commit 9 double commit 到
spark-src.git/prod
(如無衝突,則該流程全自動完成,無需人工參與。如發生衝突,通過告警系統通知開發人員手工解決衝突後提交) - 將
spark-src.git/prod
打包生成 release 並 commit 到spark-bin.git/prod
的spark-${ build \# }
(如圖中下方的 spark-3 )資料夾內。spark作為 symbolic 指向該spark-${ build \# }
Pros.
- 無論是 dev 版還是 prod 版,路徑都是 spark。切換版對使用者透明,無遷移成本
- 方便灰度釋出
- hotfix 不會引入未經測試的 commit,穩定性更有保障
- prod 版落後於 dev 版一週(一個 release 週期),即 prod 經過了一個 release 週期的測試,穩定性強
Cons.
- hot fix 時,使用 cherry-pick,但
spark-src.git/dev
(包含 commit 1、2、3、4、5) 與spark-src.git/prod
(包含 commit 1) 的 base 不一樣,有發生衝突的風險。一旦發生衝突,便需人工介入 - hot fix 後再從
spark-src.git/dev
合併 commit 到spark-src.git/prod
時需要使用 rebase 而不能直接 fast-forward merge。而該 rebase 可能再次發生衝突 - bug fix 修復的是當前
spark-bin.git/dev
的 bug,即圖中的 commit 1、2、3、4 後的 bug,而 bug fix commit 即 commit 9 的 base 是 commit 5,存在一定程度的不一致 - bug fix 後,第 3 周時,最新的
spark-bin.git/dev
包含了 bug fix,而最新的spark-bin.git/prod
未包含該 bugfix (它只包含了 commit 2、3、4 而不包含 commit 5、9)。只有到第 4 周,spark-bin.git/prod
才包含該 bugfix。也即 Staging 環境中發現的 bug,需要在一週多(最多兩週)才能在 prod 環境中被修復。換言之,Staging 環境中檢測出的 bug,仍然會繼續出現在下一個生產環境的 release 中 spark-src.git/dev
與spark-src.git/prod
中包含的 commit 數一致(因為只允許 fast-forward merge),內容也最終一致。但是 commit 順序不一致,且各 commit 內容也可能不一致。如果維護不當,容易造成兩個分支差別越來越大,不易合併
方案三:多分支
正常流程
如下圖所示,基於多分支的 Spark 持續交付方案如下
- 正常開發在
spark-src.git/master
上進行 - 每週一通過 fast-forward merge 將
spark-src.git/master
最新程式碼合併到spark-src.git/dev
。如下圖中,第 2 周將 commit 4 及之前所有 commit 合併到spark-src.git/dev
- 將
spark-src.git/dev
打包生成 release 並提交到spark-bin.git/dev
的spark-${ build \# }
(如下圖中第 2 周的 spark-2) 資料夾內。spark 作為 symbolic,指向該spark-${ build \# }
- 每週一通過 fast-forward merge 將
spark-src.git/master
一週前最後一個 commit 合併到spark-src.git/prod
。如第 3 周合併 commit 4 及之前的 commit - 上一步中,如果 commit 4 後緊臨有一個或多個 bugfix commit,均需合併到
spark-src.git/prod
中,因為它們是對 commit 4 進行的 bug fix。後文介紹的 bug fix 流程保證,如果對 commit 4 後釋出版本有多個 bug fix,那這多個 bug fix commit 緊密相連,中間不會被正常 commit 分開 - 將
spark-src.git/prod
打包生成 release 並提交到spark-bin.git/prod
的spark-${ build \# }
(如下圖中第 2 周的 spark-2) 資料夾內。spark 作為 symbolic,指向該spark-${ build \# }
bug fix
在 Staging 環境中發現了 dev 版本的 bug 時,修復及整合和交付方案如下
- 如下圖中,第 2 周與第 3 周之間在 Staging 環境中發現 dev 版本的 bug,在
spark-src.git/dev
(包含 commit 1、2、3、4) 上提交一個 commit(如圖中黑色的 commit 9),且 commit message 中包含 bugfix 字樣 - Jenkins 發現該 bugfix 的 commit 後立即執行構建,將
spark-src.git/dev
打包生成 release 並提交到spark-bin.git/dev
的spark-${ build \# }
(如圖中的 spark-3) 資料夾內,spark 作為 symbolic,指向該spark-${ build \# }
- 通過
git checkout master
切換到spark-src.git/master
,再通過git rebase dev
將 bugfix 的 commit rebase 到spark-src.git/master
,如果 rebase 發生衝突,通過告警通知開發人員人工介入處理衝突 - 在一個 release 週期內,如發現多個 dev 版本的 bug,都可按上述方式進行 bug fix,且這幾個 bug fix 的 commit 在
spark-src.git/dev
上順序相連。因此它們被 rebase 到spark-src.git/master
後仍然順序相連
hot fix
在生產環境中發現了 prod 版本的 bug 時,修復及整合和交付方案如下
- 在
spark-src.git/prod
中提交一個 commit,且其 commit message 中包含 hotfix 字樣 - Jenkins 發現該 commit 為 hotfix,立即執行構建,將
spark-src.git/prod
打包生成 release 並提交到spark-bin.git/prod
的spark-${ build \# }
(如圖中的 spark-3) 資料夾內,spark 作為 symbolic,指向該spark-${ build \# }
- 通過
git checkout master
切換到spark-src.git/master
,再通過git rebase prod
將 hotfix rebase 到spark-src.git/master
- 在一個 release 週期內,如發現多個 prod 版本的 bug,都可按上述方式進行 hot fix
灰度釋出
本文介紹的實踐中,不考慮多個版本(經實踐檢驗,多個版本維護成本太高,且一般無必要),只考慮一個 prod 版本,一個 dev 版本
上文介紹的持續釋出中,可將 spark-bin.git/dev
部署至需要使用最新版的環境中(不一定是 Staging 環境,可以是部分生產環境)從而實現 dev 版的部署。將 spark-bin.git/prod
部署至需要使用穩定版的 prod 環境中
回滾機制
本文介紹的方法中,所有 release 都放到 spark-${ build \# }
中,由 spark 這一 symbolic 選擇指向具體哪個 release。因此回滾方式比較直觀
- 對於同一個大版本(dev 或者 prod)的回滾,只需將 spark 指向 build # 較小的 release 即可
- 如果是將部分環境中的 prod 版遷至 dev 版(或者 dev 版改為 prod 版)後,需要回滾,只需將 dev 改回 prod 版(或者將 prod 版改回 dev 版)即可
Pros.
- 正常開發在
spark-src.git/master
上進行,Staging 環境的 bug fix 在spark-src.git/dev
上進行,生產環境的 hot fix 在spark-src.git/prod
上進行,清晰明瞭 - bug fix 提交時的 code base 與 Staging 環境使用版本的 code 完全一致,從而可保證 bug fix 的正確性
- bug fix 合併回
spark-src.git/master
時使用 rebase,從而保證了spark-src.git/dev
與spark-src.git/master
所有 commit 的順序與內容的一致性,進而保證了這兩個 branch 的一致性 - hot fix 提交時的 code base 與 生產環境使用版本的 code 完全一致,從而可保證 hot fix 的正確性
- hot fix 合併回
spark-src.git/master
時使用 rebase,從而保證了spark-src.git/dev
與spark-src.git/master
所有 commit 的順序性及內容的一致性,進而保證了這兩個 branch 的一致性 - 開發人員只需要專注於新 feature 的開發,bug fix 的提交,與 hot fix 的提交。所有的版本維護工作全部自動完成。只有當 bug fix 或 hot fix rebase 回
spark-src.git/master
發生衝突時才需人工介入 spark-bin.git/dev
與spark-bin.git/prod
將開發版本與生產版本分開,方便獨立部署。而其路徑統一,方便版本切換與灰度釋出
Cons.
- 在本地
spark-src.git/master
提交時,須先 rebase 遠端分支,而不應直接使用 merge。在本方案中,這不僅是最佳實踐,還是硬性要求 - 雖然 bug fix 與 hot fix commit 都通過 rebase 進入
spark-src.git/master
。但發生衝突時,需要相應修改spark-src.git/master
上後續 commit。如上圖中,提交紅色 commit 9 這一 hot fix 後,在 rebase 回spark-src.git/master
時,如有衝突,可能需要修改 commit 2 或者 commit 3、4、5。該修改會造成本地解決完衝突後的版本與遠端版本衝突,需要強制 push 回遠端分支。該操作存在一定風險
Spark CD 持續部署
持續部署是指,軟體通過評審後,自動部署到生產環境中
上述 Spark 持續釋出實踐的介紹都只到 "將 *** 提交到 spark-bin.git
" 結束。可使用基於 git 的部署(為了效能和擴充套件性,一般不直接在待部署機器上使用 git pull --rebase,而是使用自研的上線方案,此處不展開)將該 release 上線到 Staging 環境或生產環境
該自動上線過程即是 Spark 持續部署的最後一環