[持續交付實踐] 最後一公里,你需要一套具備質量思維的釋出平臺!

蒋刚毅發表於2019-09-12

前言

釋出是持續交付的最後一公里。
傳統上,軟體的最終釋出是個充滿壓力的過程,需要大量的手工配置、操作和團隊配合。為了釋出的可靠性,開發人員需要準備詳盡的部署文件,然後再把相關資訊同步給運維人員執行部署,由運維人員執行一系列個性化的釋出指令碼,部署完後還需要測試人員做詳盡的手工驗證。
每個步驟裡都有很多需要人為判斷和資訊溝通的事情,稍有不慎就很會產生人為錯誤造成系統故障,釋出時間和結果都不可預測,釋出之後忙活到凌晨,絞盡腦汁想著怎麼讓剛剛部署的應用程式能夠正常工作,最後常常不得不回滾,類似這樣的場景都很常見。
要解決這樣的痛點,除了在軟體研發時採用小步迭代的方式,降低交付複雜度外,一套易用、快速、穩定、容錯力強,必要時有能力快速回滾的釋出系統必不可少,本文將重點建設微醫在釋出平臺建設過程中的一些優秀實踐。

核心特性

  • 覆蓋主流應用: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://testerhome.com/topics/9977

相關文章