基于CI的服務端自動化設計與實踐
一、寫在前面
1、開發模式的演進
(1) 傳統的開發模式
在傳統的開發模式下,開發、運維、物理機三者之間的關系是非常緊密的。當開發完成項目后,運維會負責把項目部署到一臺物理機上,由這臺物理機向外提供服務。
由于服務和物理機關系緊密,導致服務非常依賴于物理機的環境。一旦需要調換物理機器,運維同事又需要在另一臺物理機上安裝服務依賴的環境,經過一頓折騰后,才能完成服務的部署。
(2) 容器的顛覆革命
為了解決這個問題,出現了一種名為虛擬機的操作系統虛擬化產品。不過還發展沒有太久,就已經被一種更輕量級的操作系統容器化產品替代了,它就是Docker。
運用Docker容器技術,運維可以把服務依賴的環境資源都編入 Image 中,然后把服務運行在 Container 上,實現服務與物理機的解耦。開發人員也可以將運維的工作以可編程的方式編入 Dockerfile 文件中,從此打破了開發和運維之間的壁壘,大大降低了部署成本。
(3) 敏捷開發的創新
敏捷開發把一個產品交付過程拆分為了多個小周期,創新性的通過不斷重復迭代的方式,實現產品的逐步改進和提前交付。從此改變了以往瀑布流模式下的大周期開發問題,可以提前了解市場需求,規避風險。
(4) Devops文化運動
正是在基于容器、敏捷開發、CI/CD 等各種前瞻技術思想的發展下,催生了一場名為 Devops 的文化運動。在這場文化運動中,更加強調加強開發、測試、運維之間的溝通。現在已經越來越多的公司或團隊開始重視和投入到 Devops 的建設中,基于 CI/CD 構建服務端的自動化,讓整個開發模式高效高質量的同時,也更加流程化、規范化。
2、演進過程中的特點
從上可以發現,隨著技術發展,RD 與測試、運維之間的不再是涇渭分明的工作線,反而他們之間的關系愈加緊密,RD 的整個開發模式也開始向自動化方向演進。
3、為什么要寫這篇文章
在目前的實際工作中,遇到了一系列影響效率和質量的問題。雖然目前有現成的 CI/CD 機制可以使用,但它的接入成本較高且不易擴展。
所以開始嘗試運用 Devops 思想并結合 CI/CD 技術,創建一個基于CI的服務端自動化,把項目的開發、單測、集測、部署、檢測、監控等整個完整的項目流程,都閉環集成至這個自動化中,實現更加快速高效高質量地交付產品。
經過一段時間的實踐,效果很好,驗證了想法的正確性,故總結和分享此文章,為大家提供思路與經驗。
4、這篇文章主要寫了什么
本文會以 CI/CD 技術為核心,重點介紹它能為我們帶來什么,我們為什么要設計自己的CI服務端自動化,以及如何設計。
5、閱讀文章前的注意內容
- CI/CD 這項技術,下文中會簡稱為 CI
- 文章內容如果出現錯誤理解、病句、錯別字等,請見諒并歡迎大家的指出
二、問題現狀
1、項目迭代速度快
在瀑布流開發模式下,一個項目的周期是足夠長的,一般有充足的時間來完成整個項目流程。但在敏捷開發模式下,開發內容的縮減,伴隨而來的也是需求、測試等環節時間的縮減,導致迭代速度也越來越快。
舉例 :今天需要開發一個功能,但可能不經過測試,產品驗收后就可以上線。
2、第三方工具太重
市面上流行的各種第三方工具大都非常重,功能繁多,依賴復雜,甚至還都會附帶一個后臺管理系統。導致很多時候我們只需要某個工具10%的功能,但不得不接受它90%附加的功能,這類重型工具只適合穩定發展的項目。
對于快速迭代的項目,需要的是輕量、輕量、還是輕量!
舉例 :接口文檔自動生成工具中,大部分都需要私有服務器部署而且會附有各種管理系統。
3、測試的時間緊張
在傳統項目流程中,測試一直是時間分配最少的環節,再加上多端測試、Bug驗證以及整體回歸,導致測試時間不足的問題很常見也很難避免。
舉例 :QA在驗某個功能時發現一直有問題,由于RD沒有寫單元測試,較久后才排查出原因,導致QA測試時間更加緊張
4、需求的滿足較慢
在上面已經說道,敏捷開發下的節奏整體是非常快的。對于RD而言,無論是需要 QA、OP、還是 SRE 等任何一方的需求,滿足速度雖然可能不慢,但還是不能支持敏捷開發下迭代的速度。
舉例1 :某個業務接口需要自動化測試支持,但 QA 可能需要排期才能完成
舉例2 :后端服務上線時可能由于配置缺少等問題發生 panic,需要 SRE 在 CI 中新增配置檢查,但需要排期才能完成
三、如何解決
在面對以上種種問題下,如果沒有一個完整健全的機制,是難以輕松應對敏捷開發這種快迭代速度的,交付質量也會大打折扣。
所以為了解決這個問題,我們開始運用 Devops 思想,基于 CI 來建立一套完整的、覆蓋廣的服務端自動化,打破不同部門之間的壁壘,適應快速迭代,滿足質量要求。
四、我們的實踐
1、設計方案
(1) 輕量化設計
在設計之初,最主要的目標就是輕量化 。輕量絕不代表著不完整或不成熟,反而是省去了所有的細枝末節后,用一種更少的成本,更快速的滿足實現了我們的需求。 所以在整個設計中,都會貫穿輕量化的思想。
- 僅依賴 Gitlab 現有的 Runner 機器作為服務器,沒有再使用額外的機器資源
- 沒有使用額外的后臺管理系統,直接選擇了 Gitlab Repository 作為托管服務,接口文檔分別放置在 Gitlab倉庫的 /apidoc.md 和 Wiki 中
- 雖然使用了第三方緩存倉庫,但為了速度足夠快,我們希望可以不使用50MB以上的工具。其實到目前為止,大部分使用的工具都在10MB以下。
(2) 多項目共用
在微服務架構下,一套代碼被劃分到多個代碼庫中,多個代碼庫下都有自己的 CI 代碼,一旦 CI 中任意一個流程有變動,那么所有項目都需要配合修改,造成的整體聯動調整過大。
為了解決這個問題,采用了如下多項目共用的設計。在這個設計中,不需要再把 CI 運行邏輯寫在 gitlab-ci.yml 文件中了,而是寫在 CommonCI 這樣的倉庫中,并由 start.sh 腳本啟動。
(3) 插拔式任務
基于插拔式擴展的思想,所有的任務也都是可插拔、易擴展且完全可控的,還可以實現多任務的編排。
(4) 第三方緩存
在運行過程中,雖然 gitlab CI 提供了如 cache 和 artifacts 這樣的中間產物功能,但它們會有很多限制,有時候可能無法良好滿足。所以會設計自己的第三方 Cache 庫,用來存放已經編譯好的二進制文件,加快 CI 的執行時間。
2、技術和思想
(1) 使用到的技術
- Gitlab 技術棧 :基于 gitlab CI 和 gitlab API 實現流水線的自動工作和相關托管功能
- Shell 編程 :為了實現流水線中不同任務的插拔和編排,需要使用大量的 Shell 編程
- Go 技術棧 :對于配置檢查、依賴檢查、接口文檔生成、自動化測試等一系列需要對業務代碼處理的工作,都依賴 Go 技術棧
- 容器技術棧 :雖然目前僅第三方緩存庫基于 Docker 對各種源碼包編譯實現,不過為了方便支持日后的服務容器化管理,容器技術棧會在計劃之中
(2) 使用到的思想
- Devops :「為什么要做服務端自動化」「這樣做的意義是什么」「如何去做」等等,它們的背后都是 Devops 思想
3、整體的架構
基于 CI 建立的服務端自動化架構如下所示,它一共分為三層 :
- 代碼倉庫層 :是代碼倉庫的總稱,包括業務和通用倉庫
- Runner 層 :是 Gitlab 配置的 Runner ,是實際運行 CI 的服務器
- CI 自動化層 :是對服務端自動化的抽象,包括了一系列的插拔式任務,它們共同構成了整個自動化的流水線。
4、代碼倉庫層
從上圖架構中的代碼倉庫層可知,除了業務代碼倉庫外,還有通用代碼倉庫,其中最重要的就是我們設計的 CommonCI 倉庫。CommonCI倉庫是解決多項目 CI 共用的核心實現,它的目錄結構如下所示。
- /.gitlab :包括通用方法和不同任務的 CI 目錄
- /start.sh :啟動 CI 的腳本。當執行這個腳本時,會自動執行 /.gitlab 目錄下的 *.sh 腳本文件 ,它們就是服務端自動化中的所有任務
5、CI 自動化層
(1) 預安裝
詳情請見源代碼 .gitlab/apidoc_gen.sh
在執行 CI 前,可能需要大量的安裝操作,我們都放在了 .gitlab/pre_install.sh 預安裝腳本中。
(2) 代碼檢查
詳情請見源代碼 .gitlab/check_code.sh
一般情況下,對于代碼檢查工具的使用,不僅僅是為了規范代碼,更多更強烈的需求是希望它能盡可能的幫助我們檢查出大部分的代碼錯誤。
通過代碼掃描、詞法/語法分析、控制流分析等技術實現程序的靜態分析,甚至還可以針對我們的業務做定制化的 Bug 分析。
所以我們在 CI 中添加了 golangci-lint 代碼檢查工具,讓RD可以更加放心的提交代碼。
① 定制化檢查
如以下代碼是在業務中比較常見且易犯的錯誤,基于定制化 Bug 分析的這種場景需求,還可以開發更加輕量的內部代碼檢查工具,它可以簡單的分析識別出以下語法錯誤。
// 正確
var logger = logging.For(ctx, "arg1", "value1", "arg2", value2)
// 錯誤
var logger = logging.For(ctx, "arg1", "value1", "arg2")
(3) 配置檢查
在開發階段,在測試環境添加了某個(Kafka、Redis、Server)的配置后,可能由于疏忽在線上忘記了添加對應的配置,導致服務一上線就發生 panic 或觸發某個邏輯后產生 panic ,嚴重可能會影響到業務。
為了避免這種情況,基于這種場景,我們基于 Go 開發了一個輕量的配置檢查工具,并通過 Shell 集成至 CI 中,它可以基于測試和線上兩種環境的配置內容,做出相應的 diff ,幫助我們檢查是否缺少相應的線上配置。
(4) 依賴檢查
在微服務架構下,一般每個業務至少都會有幾十個代碼庫,對于相似的邏輯,有時候避免不了需要跨項目甚至跨業務的復制粘貼,如果稍加不注意,就很容易出現把A項目的 package 錯誤添加到了B項目的依賴中。
如果你夠幸運的話,代碼可能會跑不起來,這時候就會發現原來是引入了錯誤的依賴,修改后即可避免一次錯誤。如果不幸運,代碼可能會運行起來,導致上線后可能才會在某個條件下觸發這個錯誤,進而影響業務。
為了避免這種情況,基于這種場景,我們基于 Go 開發了一個輕量的依賴檢查工具,并通過 Shell 集成至 CI 中,它會解析項目的 Godeps.json 文件,從中找出錯誤的依賴。
(5) 單元測試
詳情請見源代碼 .gitlab/unit_test.sh
相信大部分的 RD 都有這樣的經歷,開發了一期大需求,雖然QA也正常測試完成了,但看著幾十個文件、上千行代碼的 diff ,總感覺心里沒底。出現這個問題的大部分原因,是因為 RD 自己沒有做好單元測試。
由于 RD 的代碼對 QA 來說是透明的,他們根本不知道代碼的邏輯是什么,只能從用戶的使用角度去測試,但用戶的動作是無法枚舉完的,總會有想象不到的地方。
所以為了避免這個問題,也為了降低代碼改動對以前業務邏輯的影響,我們在項目代碼中編寫了較大量的單元測試并集成至 CI 中。
(6) 本地構建
詳情請見源代碼 .gitlab/local_build.sh
現代DevOps涉及軟件應用程序在整個開發生命周期內的持續集成和持續部署。所以我們在 CI 中集成了本地構建的任務,它會完成整個項目部署構建的過程,包括打包上傳等后續操作。
(7) 啟動自檢
詳情請見源代碼 .gitlab/health_check.sh
根據以往經驗,項目在線上或者測試環境部署時直接出現 panic 的情況是時有發生的。為了解決這個問題,上面的配置檢查是其中一個方案,但它還遠遠不夠,因為我們的服務并沒有真正的啟動起來,如果不啟動就無法確定服務是否真的可以正常運行。
所以為了避免這種情況,我們在 CI 中集成了服務啟動和自檢查的功能。
(8) 接口文檔生成
詳情請見源代碼 .gitlab/apidoc_gen.sh
眾所周知,接口文檔的治理一直是一個開發流程中非常難真正解決掉的問題,所以也由此催生很多和 Swagger 類似的接口文檔自動生成工具。
不過我們并沒有使用 Swagger ,雖然它足夠應對遇到的所有場景,但是它太過龐大,完全不適合我們的業務使用。
所以我們用 Go 開發了一個基于 Model 的 API Mock 工具,它是一個絕對輕量且能滿足業務需求的接口文檔生成工具,不但不需要服務器托管,還節省了大量成本。
(9) 接口自動化測試
詳情請見源代碼 .gitlab/api_test.sh
雖然我們已經有了單元測試,但單元測試只是對某個邏輯中細小的一個單元進行測試,無法確保整個接口層面的正常工作。
所以為了更高一層的服務穩定考慮,我們決定加入自動化測試,它不但可以替代一部分 QA 的工作,而且還可以提高服務的穩定性。
實現原理是我們創建了一個基于 Go 語言的接口自動化測試倉庫,RD 負責編寫相關的接口測試用例,最后通過 Shell 接入至 CI 中。
(10) 任務的添加刪除
當需要添加新的任務或刪除任務時,只需要在 start.sh 腳本中添加或刪除即可。
比如現在需要新增一個自動部署的任務,先添加 auto_deploy.sh 腳本 ,然后添加到 start.sh 腳本中即可 ,如下所示。
6、項目如何接入
在以往的項目中,當接入 CI 時,需要在 .gitlab-ci.yml 文件中寫大量復雜的代碼邏輯,可維護性非常差。為了支持多業務的快速接入,必須盡可能減小接入成本。
(1) 只需要創建一個配置文件
不同項目接入時,只需要創建一個非常簡單的 .gitlab-ci.yml 文件,然后按照如下模板化的方式配置業務方所需要的變量即可。
# this example yml file: https://jihulab.com/WGrape/apimock-example/-/blob/main/.gitlab-ci.yml
image: golang:1.17
variables:
# variable configuration for [your private gitlab host]
GITLAB_HOST: ""
GITLAB_API_TOKEN: ""
# variable configuration for [your project]
PROJECT_NAME: "apimock-example"
PROJECT_ID: 48845
# variable configuration for [DingDing WebHook]
DING_KEYWORD: "apimock-example"
DING_ACCESS_TOKEN: ""
DING_NOTICE_SWITCH: "off"
# variable configuration for [check code]
CHECK_CODE_SWITCH: "on"
# variable configuration for [unit test]
UNIT_TEST_TRIGGER_CMD: "cd mock && go test -v . && cd .. && \
cd service && go test -v . && cd ..
"
UNIT_TEST_SWITCH: "on"
# variable configuration for [apidoc generator]
APIDOC_TRIGGER_CMD: "cd mock && go test -v . && cd .."
APIDOC_FILE: "apidoc.md"
APIDOC_SWITCH: "off"
# variable configuration for [local build]
LOCAL_BUILD_TRIGGER_CMD: "go mod download && go build -o project && nohup ./project &"
LOCAL_BUILD_SWITCH: "on"
# variable configuration for [health check]
HEALTH_CHECK_TRIGGER_CMD: "curl -X GET 127.0.0.1:8000/ping"
HEALTH_CHECK_SUCCESS: "ok"
HEALTH_CHECK_SWITCH: "on"
before_script:
- echo '====== CIManager Start Running ========='
after_script:
- echo '====== CIManager Stopped Successfully ========='
stages:
- CIManager
CIManager:
stage: CIManager
script:
- git clone -b testing https://github.com/wgrape/CIManager.git ; cp -an ./CIManager/. ./ ; rm -rf ./CIManager ; bash start.sh
(2) 自動創建一個配置文件
創建一個配置文件的方式十分簡單方便,但這種方式還是需要相應的人工成本。
為了更加低成本的接入,可以使用我們的CLI工具,它基于 read -p 命令和模板替換的思想,通過人機交互的輸入,可以完成配置文件的自動創建。
(3) 配置文件的構成
從上面的配置文件中可以發現,它主要由 image 、variables 、before_script 、after_script 、stages 這5個部分構成。
- image :指定一個鏡像
- stages :定義了 uniteci 這唯一的一個 Stage
- before_script :在 UniteCI Stage 執行前需要執行的命令
- after_script :在 UniteCI Stage 執行后需要執行的命令
- variables :整個配置的核心,它定義了在 UniteCI Stage 中所有需要的變量
(4) 配置文件的特點
它不同于傳統配置內容主要體現在以下幾個方面。
- 只有一個Stage
- 主要是基于變量驅動的方式
- 極簡的配置設計,降低了編寫和接入的門檻,提高可維護性
7、緩存帶來的性能提升
一切的設計都是有原因的!之所以設計中使用第三方緩存,是因為在早期整個 CI 運行過程中,大量的耗時都在 golangci-lint 工具的下載和編譯上面,正常耗時都在5分鐘~10分鐘,有時甚至直接掛起到幾十分鐘以上,嚴重影響正常使用。
在嘗試了artifacts和cache方案無法解決時,決定開始使用第三方緩存,我們把下載編譯好的 golangci-lint 工具放在了第三方緩存庫中,這樣每次直接下載這個編譯后的二進制文件即可。
后期使用了緩存服務后,幾乎每次運行整個 CI 時,都可以在1分鐘內即可完成,速度提升了足足5倍以上!
五、我們的目標
在經過我們的實踐后,最終確立了最終要實現的目標 :保證服務穩定且高效的迭代
1、穩定
在整個項目過程中,特別是上線流程中,不出現各種低級錯誤導致的部署失敗問題,比如以下情況
- 缺少某個服務配置,導致panic
- 資源未初始化導致的panic,如map未初始化
- 代碼在修改時影響到了其他的邏輯,導致其他地方出現bug
- 代碼有低級錯誤,但是沒有自測,導致服務部署時根本無法部署成功
2、高效
在整個項目過程中,特別是開發聯調和測試跟進流程中,盡可能少出現低效率或工作進度阻塞的問題,比如以下情況
- 提測的項目根本無法達到提測標準,測試工作嚴重阻塞
- 開發過程中由于接口文檔等問題,導致前后端工作效率都受到較大影響
六、回顧和總結
閱讀至此,本文已經臨近結束了。下面我們再來回顧下主要內容 :
1、在敏捷開發的快速迭代下,我們必須選擇一種合適的服務端自動化方案,來提高整個開發周期的速度、質量、和流程規范化。
2、正是基于這個背景,我們才運用 Devops 思想,基于 CI 建立了一套完整的、覆蓋廣的服務端自動化。
3、在設計的過程中,我們的目標是更加的輕量、可擴展和低接入成本,方便隨時隨地快速迭代。
4、在實踐的過程中,我們遇到了如多項目共用、執行速度慢等諸多問題并逐一解決。
言而總之,本文從原因到方案,為大家分享了一個比較全面的《基于CI的服務端自動化》解決方案,希望對大家有所幫助。不過還有一點必須要清楚的是,我們做這個東西不是為了做而做的,而是有切實的背景需求。這樣即使在快節奏的迭代下,它也可以為整個開發流程提高效率和質量。