卞中杰
(上海甚解信息技術(shù)有限公司,上海 200235)
隨著交通運(yùn)輸行業(yè)的飛速發(fā)展,道路貨物運(yùn)輸量逐年上升,為了幫助運(yùn)輸企業(yè)降低運(yùn)輸過(guò)程中的事故率,出現(xiàn)了由第三方企業(yè)開展的針對(duì)道路貨物運(yùn)輸?shù)娘L(fēng)險(xiǎn)控制業(yè)務(wù);然而由于風(fēng)控軟件的特殊性,軟件發(fā)布需要做全套的單元測(cè)試、集成測(cè)試、系統(tǒng)測(cè)試、回歸測(cè)試,傳統(tǒng)的人工測(cè)試無(wú)法在有限時(shí)間和資源內(nèi)確保測(cè)試的全面性和準(zhǔn)確性,同時(shí)伴隨長(zhǎng)鏈路的業(yè)務(wù)流程越來(lái)越多,人工測(cè)試在數(shù)據(jù)準(zhǔn)備和業(yè)務(wù)驗(yàn)證方面效率低下[1]。因此,自動(dòng)化測(cè)試是一種能夠保證測(cè)試正確執(zhí)行的可行方法,只有保證自動(dòng)化測(cè)試的正確性,才能實(shí)現(xiàn)系統(tǒng)后續(xù)的自動(dòng)部署和快速迭代。
持續(xù)集成和持續(xù)部署(CI/CD)作為關(guān)鍵內(nèi)容,包含多種實(shí)現(xiàn)方式,比如通過(guò)Jenkins 和Ansible 實(shí)現(xiàn)軟件的自動(dòng)化編譯和部署[1];通過(guò)GitLab 的Pipeline 功能和Terraform 結(jié)合實(shí)現(xiàn)基礎(chǔ)設(shè)施的自動(dòng)化創(chuàng)建和部署[3];基于容器的DevOps 實(shí)施方案和實(shí)踐[4]。
容器(指Docker、Podman 等容器技術(shù))的出現(xiàn)使得DevOps 更容易被實(shí)現(xiàn),DevOps 往往需要多種軟件協(xié)同實(shí)現(xiàn),由于容器天生具備隔離性,可使得不同版本的軟件互不影響地在同一宿主機(jī)上運(yùn)行,相比虛擬機(jī)而言,啟動(dòng)速度更快、資源占用更小。此外,容器的打包方式可實(shí)現(xiàn)開發(fā)環(huán)境和生產(chǎn)環(huán)境的軟件版本、依賴項(xiàng)等高度統(tǒng)一,打包完成的鏡像易于分發(fā)。
Kubernetes 的出現(xiàn)改變了容器編排方式,能從更高維度管理容器的生命周期,將服務(wù)器作為一整個(gè)集群統(tǒng)一管理,使得容器可靈活地在集群中被啟動(dòng)和調(diào)度。Kubernetes 可以幫助基于容器分發(fā)的軟件獲得高可用、可擴(kuò)展、可漂移的能力,并具備細(xì)粒度的容器管理能力。本文探討的CI/CD 以及自動(dòng)化測(cè)試的實(shí)現(xiàn)均基于Kubernetes,充分利用容器的能力和特性,實(shí)現(xiàn)從開發(fā)到部署的自動(dòng)化。
對(duì)于CI/CD 流程而言,觸發(fā)整個(gè)流程開始的事件通常是開發(fā)人員提交代碼或者代碼被合并到測(cè)試或生產(chǎn)分支。一旦代碼提交,如果GitLab 發(fā)現(xiàn)代碼倉(cāng)庫(kù)根目錄中存在.gitlabci.yml 文件,GitLab 會(huì)通知獨(dú)立部署的GitLab Runner 組件,依次運(yùn)行該文件內(nèi)定義的流程任務(wù),CI/CD 的整體流程見圖1。
圖1 CI/CD 整體流程設(shè)計(jì)
CI/CD 流程腳本都寫在.gitlab-ci.yml 文件中,在文件的開頭往往會(huì)定義一些變量為后續(xù)步驟服務(wù),同時(shí)該文件也定義了運(yùn)行每個(gè)步驟默認(rèn)使用的容器(文本使用docker:stable 鏡像)以及整個(gè)流程的3 個(gè)流程節(jié)點(diǎn)(又稱Stage):test、build 和deploy。
在流程的第一步中,編譯測(cè)試會(huì)被首先執(zhí)行,可確保新的代碼能夠被編譯通過(guò)。在這之后,會(huì)根據(jù)不同項(xiàng)目類型執(zhí)行單元測(cè)試、集成測(cè)試、系統(tǒng)測(cè)試等,在所有測(cè)試通過(guò)以后,到下一步鏡像打包階段,如果測(cè)試失敗,則流程終止。
鏡像打包階段軟件需要被打包成Docker 鏡像,由于每一步流程都是在一個(gè)容器中啟動(dòng),此方式被成為DinD(Docker in Docker)。運(yùn)行DinD 需要priveleged 權(quán)限,出于安全考慮,選用kaniko 鏡像來(lái)實(shí)現(xiàn)鏡像打包且無(wú)需額外權(quán)限即可使用。
鏡像打包并上傳到鏡像倉(cāng)庫(kù)以后,在測(cè)試環(huán)境或預(yù)發(fā)布環(huán)境中進(jìn)行部署,而Kubernetes 中,部署方式通常有兩種:使用原生部署配置文件或Helm 工具。Helm 通過(guò)向模板注入不同的配置和參數(shù),來(lái)生成不同的部署文件,其靈活性優(yōu)于Kubernetes 原生部署文件;并且Helm 具備部署的版本管理功能,便于版本切換,故使用Helm 進(jìn)行部署,配置如下:
Helm 從app/test 下載模板文件,并且結(jié)合代碼倉(cāng)庫(kù)下. /deploy / test / values . yaml 的屬性更改,會(huì)在Kubernetes的test 命名空間(namespace)下,部署一個(gè)名為test-app 的應(yīng)用。至此,測(cè)試、預(yù)發(fā)布程序已經(jīng)完成部署。對(duì)于測(cè)試環(huán)境則流程結(jié)束;而對(duì)于正式環(huán)境,可在預(yù)發(fā)布環(huán)境做最后的核對(duì)和測(cè)試,當(dāng)測(cè)試完成后,繼續(xù)后續(xù)的發(fā)布到生產(chǎn)環(huán)境的流程。
在整個(gè)自動(dòng)化部署流程中,測(cè)試是最為關(guān)鍵的一環(huán)。測(cè)試方法和用例的設(shè)計(jì)以及用例是否能被正確執(zhí)行,決定了能否自動(dòng)執(zhí)行后續(xù)的發(fā)布流程。在測(cè)試執(zhí)行過(guò)程中,面臨著兩類問(wèn)題:(1)多人提交代碼導(dǎo)致測(cè)試并發(fā)執(zhí)行,引起數(shù)據(jù)沖突使得測(cè)試失敗;(2)由于前一次的測(cè)試用例修改了測(cè)試數(shù)據(jù),再次執(zhí)行測(cè)試導(dǎo)致測(cè)試失敗。下文將介紹通過(guò)容器技術(shù)和測(cè)試管線設(shè)計(jì)來(lái)解決以上兩個(gè)問(wèn)題。
代碼在提交、合并或進(jìn)入發(fā)布通道時(shí)均可設(shè)置不同的測(cè)試環(huán)節(jié)。當(dāng)軟件通過(guò)了所有的測(cè)試,流程自動(dòng)進(jìn)入到后續(xù)的發(fā)布階段,而測(cè)試中有某一項(xiàng)未通過(guò),則發(fā)布流程終止。如果流程涉及合并請(qǐng)求,可以駁回并自動(dòng)關(guān)閉本次合并請(qǐng)求。自動(dòng)化的測(cè)試流程分為4 個(gè)階段:創(chuàng)建和啟動(dòng)測(cè)試環(huán)境;導(dǎo)入和初始化測(cè)試數(shù)據(jù);執(zhí)行測(cè)試;銷毀測(cè)試環(huán)境。當(dāng)環(huán)境和數(shù)據(jù)準(zhǔn)備完成之后,執(zhí)行所有測(cè)試并判斷結(jié)果;當(dāng)測(cè)試完成以后,銷毀測(cè)試環(huán)境,執(zhí)行后續(xù)流程。
高質(zhì)量、可復(fù)用的測(cè)試數(shù)據(jù)除了能夠支撐后臺(tái)軟件測(cè)試,同樣要能夠?yàn)榍岸藴y(cè)試提供服務(wù)。單一存儲(chǔ)的測(cè)試數(shù)據(jù)會(huì)被測(cè)試樣例在運(yùn)行期間刪除、修改,導(dǎo)致測(cè)試無(wú)法重現(xiàn),并且在多人并發(fā)測(cè)試場(chǎng)景下,如果某一方修改了測(cè)試數(shù)據(jù),會(huì)導(dǎo)致其他依賴該測(cè)試數(shù)據(jù)的測(cè)試無(wú)法進(jìn)行。因此,測(cè)試數(shù)據(jù)必須被獨(dú)立抽取出來(lái),在測(cè)試流程開始時(shí)完整地恢復(fù)到測(cè)試環(huán)境中,形成可復(fù)用的測(cè)試數(shù)據(jù)。在該項(xiàng)目中,測(cè)試數(shù)據(jù)按照生產(chǎn)環(huán)境要求和格式預(yù)先被設(shè)計(jì)好并存放在MySql 中,使用MySql 的dump 命令將測(cè)試數(shù)據(jù)全部抽取形成文件,并存放到源代碼管理系統(tǒng)中。例如對(duì)于MySql5.7 版本,把App 數(shù)據(jù)庫(kù)中的users 表數(shù)據(jù)全部導(dǎo)出為users.sql 文件,可使用如下命令:mysql _user USER _password PASSWORD _host HOST dump _database App _tables users > users.sql,其中USER、PASSWORD、HOST 分別需要替換為真實(shí)的數(shù)據(jù)庫(kù)用戶名、密碼和數(shù)據(jù)庫(kù)服務(wù)器地址。要把該導(dǎo)出文件恢復(fù)到測(cè)試環(huán)境數(shù)據(jù)庫(kù)中,則對(duì)應(yīng)的恢復(fù)命令是:mysql _user USER_password PASSWORD _host HOST < users.sql
在后續(xù)的測(cè)試指令執(zhí)行完成后,本次Stage 內(nèi)啟動(dòng)的所有容器及其中數(shù)據(jù)都會(huì)被刪除,不會(huì)造成數(shù)據(jù)和環(huán)境殘留。
由于整個(gè)CI/CD 流程均在基于容器的環(huán)境中進(jìn)行,因此可以使用容器在測(cè)試環(huán)節(jié)中啟動(dòng)測(cè)試該業(yè)務(wù)所需的所有配套設(shè)施。
3.3.1 后臺(tái)測(cè)試環(huán)境配置和啟動(dòng)流程
以測(cè)試一個(gè)微服務(wù)為例,需要啟動(dòng)的依賴設(shè)施有MySQL 數(shù)據(jù)庫(kù)和Flask 服務(wù)器。在Pipeline 中對(duì)應(yīng)的Stage配置如下:
這段配置文件首先修改了默認(rèn)運(yùn)行環(huán)境為:${TEST_ENV_IMAGE},這是一個(gè)變量,指向開頭定義的對(duì)應(yīng)的鏡像名,該鏡像是一個(gè)安裝了flask 和pytest 的帶有python3.7 運(yùn)行時(shí)環(huán)境的容器。services 語(yǔ)句定義了除運(yùn)行主容器以外,在這個(gè)Stage 中還需要運(yùn)行一個(gè)MySql5.7 的容器,該容器和主容器之間可進(jìn)行網(wǎng)絡(luò)通信。接下來(lái)before_script 屬性的值描述了容器啟動(dòng)后先運(yùn)行的腳本,這里是向MySql 導(dǎo)入測(cè)試數(shù)據(jù)的合適時(shí)機(jī)。再接下來(lái)的script屬性定義了環(huán)境準(zhǔn)備完畢后運(yùn)行測(cè)試的指令,在這里運(yùn)行的是pytest 測(cè)試指令,可以根據(jù)實(shí)際需要定義若干條測(cè)試指令。因此,一個(gè)完整的測(cè)試環(huán)境在容器中得以運(yùn)行,該微服務(wù)所依賴的所有外部服務(wù)同時(shí)也以容器方式啟動(dòng),在所有測(cè)試語(yǔ)句運(yùn)行完畢以后,這些容器都會(huì)被銷毀,不會(huì)留下數(shù)據(jù)和配置殘留。
3.3.2 前端測(cè)試環(huán)境配置和啟動(dòng)流程
前端軟件的測(cè)試會(huì)和后臺(tái)服務(wù)軟件測(cè)試存在差異。除了小游戲、工具型軟件等無(wú)需和后臺(tái)交互的程序外,與業(yè)務(wù)結(jié)合的前端軟件絕大部分依賴于后臺(tái)服務(wù)才能運(yùn)行,因此在測(cè)試前端的過(guò)程中,后臺(tái)服務(wù)環(huán)境也要一并啟動(dòng)。單元測(cè)試由于不依賴于后臺(tái),只需啟動(dòng)一個(gè)帶有headless 瀏覽器的容器(本文使用cypress/browsers:node16.13.0-chrome95-ff94 鏡像),在該容器內(nèi)可以運(yùn)行前端的單元測(cè)試代碼;但是e2e 測(cè)試由于需要最大程度地模擬用戶真實(shí)使用場(chǎng)景,且為了確保測(cè)試可靠性,整個(gè)流程需要真實(shí)后臺(tái)數(shù)據(jù)參與,在e2e-test該環(huán)節(jié)中的services 中定義了需要額外啟動(dòng)2 個(gè)容器,分別是MySql 和后臺(tái)微服務(wù)。通過(guò)啟動(dòng)后臺(tái)微服務(wù)確保前端測(cè)試匹配對(duì)應(yīng)版本后臺(tái)邏輯,測(cè)試過(guò)程中可與真實(shí)測(cè)試數(shù)據(jù)進(jìn)行交互,而MySql 則是運(yùn)行后臺(tái)微服務(wù)所需要的依賴項(xiàng)。
在代碼通過(guò)測(cè)試后,自動(dòng)化流程來(lái)到集成和部署階段。CI/CD 的實(shí)現(xiàn)因不同軟件打包方式、系統(tǒng)運(yùn)行環(huán)境和部署方式而不同,由于本系統(tǒng)全部運(yùn)行在基于容器的Kubernetes之上,因此軟件的分發(fā)需要把軟件打包成Docker 鏡像,并在Kubernetes 上進(jìn)行部署。在.gitlab-ci.yml 中,可以分成2 個(gè)Stage 來(lái)進(jìn)行:(1)在build 這個(gè)stage 中,使用Docker-in-Docker 的方式編譯打包微服務(wù)的Docker 鏡像,并上傳到一個(gè)私有的鏡像倉(cāng)庫(kù)。而在build-test-mysql 中,基于MySql 鏡像打包了一個(gè)帶有測(cè)試數(shù)據(jù)的鏡像,可作為一個(gè)帶有初始數(shù)據(jù)的數(shù)據(jù)庫(kù)鏡像靈活地運(yùn)用在所有需要測(cè)試數(shù)據(jù)的環(huán)節(jié)中。(2)在deploy 這個(gè)stage 中,使用kubernetes 原生工具kubectl 通過(guò)配置文件部署應(yīng)用,除了前文提到的Helm,如果不需要每次提交都對(duì)部署文件進(jìn)行改動(dòng),而只是進(jìn)行簡(jiǎn)單的版本更新操作,則此方式更為便捷。
從圖2 可以看出,從代碼提交到測(cè)試,再?gòu)能浖虬讲渴鸪晒?,整個(gè)流程并無(wú)人工參與,只耗時(shí)3 分32 秒(當(dāng)中還包含每個(gè)Stage 的啟動(dòng)耗時(shí))。如果項(xiàng)目使用敏捷開發(fā)模式,每天均可發(fā)布新的版本,這種方式能極大地提高軟件測(cè)試和部署效率且避免人工操作錯(cuò)誤,從而提高流程可靠性。
圖2 GitLab 中后臺(tái)服務(wù)CI/CD 效果圖
本文通過(guò)實(shí)現(xiàn)DevOps 以及使用Kubernetes 作為生產(chǎn)環(huán)境,軟件的測(cè)試和發(fā)布效率都相比傳統(tǒng)方式有了大幅度的提升。在本文中基于容器和自動(dòng)化腳本的測(cè)試環(huán)境可以方便地啟動(dòng)和生產(chǎn)環(huán)境相同的組件,但這種測(cè)試環(huán)境追求的是最小化的類生產(chǎn)環(huán)境,因此該測(cè)試方法適用于測(cè)試業(yè)務(wù)的正確性。由于受限于測(cè)試環(huán)境所啟動(dòng)的單機(jī)資源限制,在自動(dòng)化Pipeline 中啟動(dòng)的測(cè)試環(huán)境一般達(dá)不到和生產(chǎn)環(huán)境等同的資源和性能,如果生產(chǎn)環(huán)境是分布式、大數(shù)據(jù)系統(tǒng),則啟動(dòng)相同形式的最小測(cè)試環(huán)境也會(huì)占用相當(dāng)大的資源,會(huì)限制可同時(shí)進(jìn)行的測(cè)試數(shù)量。除此以外,測(cè)試環(huán)境的特性決定了并不適合進(jìn)行一些極端的壓力測(cè)試,因?yàn)閱螜C(jī)資源存在瓶頸且本地回環(huán)網(wǎng)絡(luò)和真實(shí)服務(wù)器之間的物理網(wǎng)絡(luò)吞吐能力都存在很大不同,這就造成性能測(cè)試結(jié)果無(wú)法代表軟件在真實(shí)的生產(chǎn)環(huán)境中的性能表現(xiàn),對(duì)于如何解決以上這些問(wèn)題,仍需進(jìn)行更進(jìn)一步的研究與實(shí)踐。