最後一公里,你需要一套具備品質思維的發布平台!
- 2019 年 10 月 7 日
- 筆記
前言
發布是持續交付的最後一公里。
傳統上,軟體的最終發布是個充滿壓力的過程,需要大量的手工配置、操作和團隊配合。為了發布的可靠性,開發人員需要準備詳盡的部署文檔,然後再把相關資訊同步給運維人員執行部署,由運維人員執行一系列個性化的發布腳本,部署完後還需要測試人員做詳盡的手工驗證。
每個步驟里都有很多需要人為判斷和資訊溝通的事情,稍有不慎就很會產生人為錯誤造成系統故障,發布時間和結果都不可預測,發布之後忙活到凌晨,絞盡腦汁想著怎麼讓剛剛部署的應用程式能夠正常工作,最後常常不得不回滾,類似這樣的場景都很常見。
要解決這樣的痛點,除了在軟體研發時採用小步迭代的方式,降低交付複雜度外,一套易用、快速、穩定、容錯力強,必要時有能力快速回滾的發布系統必不可少,本文將重點建設微醫在發布平台建設過程中的一些優秀實踐。
核心特性
- 覆蓋主流應用: Java、NodeJS、Python、PHP、Lua、Android、IOS等
- 支援分批發布和灰度發布
- 支援發布暫停和恢復
- 支援歷史版本的快速回滾
- 支援應用重啟和停止等操作
- 支援多實例應用日誌聚合查看
- 支援分散式的發布節點
- 發布前品質紅線卡點
- 發布中實時監控分析
- 發布後質效度量
整體架構

- 典型應用發布流程:
- 系統立項後,在WCP-應用管理中申請創建應用,輸入包括開發語言、jdk版本、構建工具、部署方式、產物路徑、程式碼地址、應用負責人等應用基礎資訊(應用資訊是所有研發協作的基礎)。
- 應用創建後,在WCP-資源管理中申請申請資源,在CMDB中自動建立應用和IP/域名之間的關係(包括測試環境、預發環境、生產環境等)。
- 應用建立後,發布平台自動生成發布job,發布時獲取應用和資源等基礎資訊,可觸發灰度發布或分批發布的執行。
- 發布操作前,自動執行品質紅線檢查(主要包括pipeline執行結果以及發布檢查表確認等),品質紅線未達標拒絕發布。
- 發布操作中,自動暫停監控,灰度發布或首批發布後,自動觸發監控。 若監控失敗,停止發布; 若監控通過,可繼續發布。
- 發布操作後,採集存儲發布數據,輸出給質效看板做發布數據度量(發布成功率,發布頻率,發布時長等)。
- 發布出問題,可執行快速回滾等功能,並提供發布日誌以及應用聚合日誌的查詢,以快速定位問題。
- 發布平台介面:

發布前品質卡點
品質是持續交付的內置特性。在發布這最後一公里,如何通過發布平台自動化做好品質紅線的卡點,是發布平台的一個重要特性。
在Pipeline流水線中,開發程式碼提交後自動觸發單元測試,、靜態程式碼掃描、安全測試、集成測試, 構成了開發和測試共同參與的一套流水線。
發布卡點是用於保障交互品質的重要手段,為了達到持續交付的目標,我們把研發pipeline執行結果作為品質紅線(也可以增加人工的發布檢查表結果),以此方式來保障整個持續交付的順利進行。

常用的自動化品質卡點策略:
- 研發流水線狀態
- 單元測試結果
- 單元測試覆蓋率
- 程式碼靜態檢查
- 集成測試結果
- 安全掃描結果
- 發布計劃狀態(發布計劃管理系統)
- 發布時間窗口
- 發布評審結果等
發布中品質監控
為保障系統的穩定性,我們每個應用在監控平台都配置有對應的撥測監控點。
如何減少發布時的告警誤報,或者避免發布過程中出現重大故障,這裡需要發布平台和監控平台配合做一系列精密的控制策略。
- 發布場景1:
- 描述: 發布新版本,在重啟某個實例服務時,有一定的概率會觸發該實例的應用報警,對應用開發人員會造成一些不必要的干擾。
- 策略: 發布平台在將應用編譯打包好,執行正式發布之前,調用監控平台API停止該應用下的監控任務,在發布完後同樣調用監控平台API啟用該應用下的監控任務。
- 發布場景2:
- 描述: 發布時應用實例因為各種原因(如程式碼部署出錯,新版本存在明顯BUG等),出現了系統故障。
- 策略: 採用分批發布策略,各個實例發布完後立即觸發該實例的監控,如果監控發現異常,標識該批次發布操作失敗,並強制中止後續批次的發布操作,以避免更多的實例出現問題。
- 邏輯流程

這裡需要強調的是,撥測監控覆蓋率在微醫會作為團隊的重要指標。因為應用只有在配置監控點後,發布平台才能在發布過程中進行有效的監測和干預。
發布後質效度量
質效度量是研發協作平台的一個重要組成部分,主要質效指標將按照研發品質、研發效率、研發成本三方面進行細分。

其中在發布過程中產生的數據,將會輸送給質效度量系統進行質效分析,重點包括:
- 發布頻率
- 發布時長
- 發布成功率等
所有團隊和研發成員可以結合這些發布數據指標,發現自己存在的問題和短板,並進行有效改進。
分批發布
分批發布是批次進行應用部署,每次僅對應用的一部分實例進行升級。分批發布過程中如果出現故障,則終止回退,待問題修復後重新發布。
這裡我們採用了比較簡潔的批次分配演算法,因為公司目前使用的是雙機房IDC,當應用進行分批發布時,首批發布會在兩個機房中隨機各選擇一個實例執行,其他的實例則放到了第二批發布。

選擇發布暫停,則可在首批發布完後暫停發布,等人工確認首批發布的實例沒有問題後,再執行後續其他實例的發布,如此可有效保障發布的穩定性。
發布過程中,可在發布平台中實時查看運行日誌,若發現問題,可隨時執行暫停、取消或者回滾等操作。

- 最佳實踐
每個實例進行部署時,需要保證沒有請求會派發到該實例,否則用戶就會看到502的錯誤。所以需要有一個「下線」的操作,把當前機器從負載均衡中摘除,然後在部署完成之後,再把自己掛回到負載均衡中,這個過程稱為「上線」。
為了實現該目的,可基於OpenResty自研Nginx網關對實例上下線進行實時調度,基於 OpenResty 的 Nginx 網關的實現過程比較複雜,這裡不再詳盡展開。
問題響應
在發布過程中,如果出現了一些意料之外的情況,發布平台也提供了一些常用的功能,滿足開發人員定位和處理問題的需要,同時也盡量避免開發人員直接登錄伺服器操作。
- 查看日誌
主要對接了公司統一的日誌平台系統,可實時查看應用日誌,並且聚合了多實例的日誌資訊,減少幾個實例不停切換尋找問題的痛苦。

- 重啟或停止實例
某個實例故障時,可快速重啟或停用實例。

- 快速回滾
每個發布的版本發布平台都會有備份,當發布新版本發現問題時,可快速回滾到歷史版本

Jenkins Pipeline
在整套發布平台中,Jenkins Pipeline提供了核心的構建、打包、部署以及分散式調度的底層基礎能力,只不過為了更靈活的調度發布操作、管理應用與發布任務之間關係等,我們摒棄了Jenkins自身的UI介面,而通過發布平台調用Jenkins API的方式將其定位為基礎引擎。
其中Jenkins Pipeline的共享庫特性,讓我們通過groovy編程的方式,很好的實現了發布腳本的版本管理,再也不用發愁怎麼管理那堆凌亂的shell腳本了。
這裡只截取一部分結構程式碼,Jenkins共享庫的具體使用可參見之前的系列文章。
import groovy.json.JsonSlurper def call(Map map) { pipeline { agent any parameters { //java應用參數 string(name: 'BUILD_TOOL', defaultValue: 'maven', description: '構建工具') string(name: 'MAVEN_VERSION', defaultValue: 'maven3', description: 'maven構建工具版本') string(name: 'GRADLE_VERSION', defaultValue: 'Gradle3.3', description: 'gradle構建工具版本') string(name: 'WAR_RELATIVE_PATH', defaultValue: '', description: 'war包地址') string(name: 'WAR_STD_NAME', defaultValue: '', description: 'war包地址') string(name: 'POM_RELATIVE_PATH', defaultValue: '/pom.xml', description: 'pom文件地址') string(name: 'HAS_TEMPLATES', defaultValue: 'false', description: '是否有模板文件') string(name: 'TEMPLATES_RELATIVE_PATH', defaultValue: '', description: '模板文件路徑') string(name: 'JETTY_VERSION', defaultValue: '', description: 'jetty版本') string(name: 'GRADLE_TASK', defaultValue: 'war', description: 'gradleTask打包方式') ...... } tools { gradle "${params.GRADLE_VERSION}" jdk "${params.LANGUAGE_VERSION}" maven "${params.MAVEN_VERSION}" } stages { stage('部署正式環境') { steps { script { def pmap = [:] try { //應用參數傳遞 pmap.put('BUILD_TOOL', BUILD_TOOL.trim()) pmap.put('WAR_RELATIVE_PATH', WAR_RELATIVE_PATH.trim()) pmap.put('WAR_STD_NAME', WAR_STD_NAME.trim()) pmap.put('POM_RELATIVE_PATH', POM_RELATIVE_PATH.trim()) pmap.put('HAS_TEMPLATES', HAS_TEMPLATES.trim()) pmap.put("TEMPLATES_RELATIVE_PATH", TEMPLATES_RELATIVE_PATH.trim()) pmap.put('JETTY_VERSION', JETTY_VERSION.trim()) pmap.put('GRADLE_TASK', GRADLE_TASK.trim()) ...... } catch (MissingPropertyException ex) { println("Catching the MissingPropertyException " + ex.messageWithoutLocationText) ...... } pmap = Utils_EnvConfig(pmap) //發布前監控調度 Utils_Monitor(pmap.isRestartMonitor,monitorApiDomain,appIpName,monitorTimeOut,true) switch (ACTION) { case "package": java_package(pmap) break case "copy": java_copy(pmap) break case "start": java_start(pmap) break case "rollback": java_rollback(pmap) break case "restart": java_start(pmap) break case "stop": java_start(pmap) break case "kill": java_start(pmap) break case "backup": java_backup(pmap) break default: echo "pipeline do nothing please choose one step (package,copy,start,rollback,restart,stop,kill)" } //發布後監控調度 Utils_Monitor(pmap.isRestartMonitor, monitorApiDomain, appIpName, monitorTimeOut, false) } } } } } }
結語
一套好的發布平台可以充當最後的守衛角色,對交付給線上用戶的產品進行最後的檢查,將未達到要求的軟體版本擋於門外,一套完善的自動化發布平台也往往會比制訂各類書面上的發布制度更為有效。
這套發布平台從19年年初開始重構,從原有一個單純驅動shell腳本操作的發布工具,逐漸進化成內嵌大量品質和效率特性的發布平台,過程收穫良多。這裡一方面得益於持續交付的先進工程理念,另一方面也是站在了Jenkins Pipeline以及內部積累的大量成熟基礎設施之上,讓我們在開發時事半功倍。
當然這套平台也還有不少可以繼續加強的功能,比如灰度發布的能力、基於更多品質指標對發布前後智慧分析的能力等等,這些都在規劃和進行之中。
來源:https://www.cnblogs.com/cay83/p/11512792.html