天天看點

大型前端項目 DevOps 沉思錄 —— CI 篇

DevOps 一詞源于 Development 和 Operations 的組合,即将軟體傳遞過程中開發與測試運維的環節通過工具鍊打通,并通過自動化的測試與監控,減少團隊的時間損耗,更加高效穩定地傳遞制品。 本篇文章将着重探讨 DevOps 在 持續內建階段需要提供的能力,将對工作流的設計及流水線的優化思路做一個簡要講解。

随着項目規模越來越大,功能特性與維護人員越來越多,特性傳遞頻率與軟體品質之間的沖突日漸尖銳,如何平衡兩者成為了目前團隊亟需關注的一個重點,于是,落地一個完善的 <code>DevOps</code>工具鍊便被提上日程。

我們認為,從代碼內建、功能測試,到部署釋出、基礎設施架構管理,每一個環節都應該有全面且完善的自動化監控手段,并盡量避免人工介入。隻有這樣,軟體才能同時兼顧品質與效率,在提高釋出頻率的情況下保證可靠性。這是每一個成功的大型項目最終一定要實作的目标。

本篇文章将着重探讨 <code>DevOps</code> 在 <code>持續內建階段</code> 需要提供的能力,将對工作流的設計及流水線的優化思路做一個簡要講解。

CI(Continuous Integration),即持續內建,指頻繁地(一天多次)将代碼內建到主幹的行為。

注意,這裡既包含持續将代碼內建到主幹的含義,也包含持續将源碼生成可供實際使用的制品的過程。是以,我們需要通過 CI,自動化地保證代碼的品質,并對其建構産物轉換生成可用制品供下一階段調用。

是以,在 CI 階段,我們至少有如下階段需要實作:

靜态代碼檢查

這其中包括,ESLINT/TSLINT 靜态文法檢查,驗證 git commit message 是否符合規範,送出檔案是否有對應 owner 可以 review 等等。這些靜态檢查不需要編譯過程,直接掃描源代碼就可以完成。

單元測試/內建測試/E2E 測試

自動化測試這一環節是保障制品品質的關鍵。測試用例的覆寫率及用例品質直接決定了建構産物的品質,是以,全面且完善的測試用例也是實作持續傳遞的必備要素。

編譯并整理産物

在中小型項目中,這一步通常會被直接省略,直接将建構産物交由部署環節實作。但對于大型項目來說,多次頻繁的送出建構會産生數量龐大的建構産物,需要得到妥善的管理。産物到制品的建立我們接下來會有詳細講解。

在正式接入 CI 前,我們需要規劃好一種新的工作流,以适應項目切換為高頻內建後可能帶來的問題與難點。這裡涉及到的改造層面非常多,除了敦促開發人員習慣的轉變以及進行新流程的教育訓練外,我們主要關心的是源碼倉庫的更新觸發持續內建步驟的方式。

我們需要一個合适的組織形式來管理一條 CI 流水線該在什麼階段執行什麼任務。

市面上有非常多的 CI 工具可以進行選擇,仔細觀察就會發現,無論是 Drone 這樣的新興輕量的工具,亦或是老牌的 Jenkins 等,都原生或通過插件方式支援了這樣一個特性: <code>ConfigurationasCode</code>,即使用配置檔案管理流水線。

這樣做的好處是相當大的。首先,它不再需要一個 web 頁面專門用于流水線管理,這對于平台方來說無疑減少了維護成本。其次對于使用方來說,将流水線配置內建在源碼倉庫中,享受與源碼同步更新的方式,使得 CI 流程也能使用 git 的版本管理進行規範與審計溯源。

确立了流水線的組織形式後,我們還需要考慮版本的釋出模式以及源碼倉庫的分支政策,這直接決定了我們該以什麼樣的方式規劃流水線進行代碼內建。

在《持續傳遞 2.0》一書中提到,版本釋出模式有三要素: <code>傳遞時間、特性數量以及傳遞品質</code>。

大型前端項目 DevOps 沉思錄 —— CI 篇

這三者是互相制衡的。在開發人力與資源相對固定的情況下,我們隻能對其中的兩個要素進行保證。

傳統的項目制釋出模式是犧牲了傳遞時間,等待所有特性全部開發完成并經曆完整人工測試後才釋出一次新版本。但這樣會使得傳遞周期變長,并且由于特性數量較多,在開發過程中的不可控風險變高,可能會導緻版本無法按時傳遞。不符合一個成熟的大型項目對于持續傳遞的要求。

對于持續內建的思想來說,當我們的內建頻率足夠高,自動化測試足夠成熟且穩定時,完全可以不用一股腦的将特性全堆在一次釋出中。每開發完成一個特性就自動進行測試,完成後合入等待釋出。接下來隻需要在特定的時間周期節點自動将已經穩定的等待中的特性釋出出去即可。這對于釋出頻率越來越高,釋出周期越來越短的現代大型項目中無疑是一個最優解。

與大部分團隊一樣,我們原有的開發模式也是 <code>分支開發,主幹釋出</code>的思想,分支政策采用業界最成熟也是最完善的 <code>Git-Flow</code>模式。

大型前端項目 DevOps 沉思錄 —— CI 篇

可以看出,該模式在特性開發,bug 修複,版本釋出,甚至是 hotfix 方面都已經考慮到位了,是一個能應用在生産環境中的工作流。但整體的結構也是以變得極為複雜,不便管理。例如進行一次 hotfix 的操作流程是:從最新釋出前使用的主幹分支拉出 hotfix 分支,修複後合入到 develop 分支中,等待下一次版本釋出時拉出到 release 分支中,釋出完成後才能合回主幹。

此外,對于 <code>Git-Flow</code>的每一個特性分支來說,并沒有一個嚴格的合入時間,是以對于較大需求來說可能合入時間間隔會很長,這樣在合入主幹時可能會有大量的沖突需要解決,導緻項目工期無端延長。對此,做大型改造與重構的同學應該深有體會。

針對這一點,我們決定大膽采用 <code>主幹開發,主幹釋出</code>的分支政策。

我們要求,開發團隊的成員盡量每天都将自己分支的代碼送出到主幹。在到達釋出條件時,從主幹直接拉出釋出分支用于釋出。若發現缺陷,直接在主幹上修複,并根據需要 <code>cherry pick</code> 到對應版本的釋出分支。

大型前端項目 DevOps 沉思錄 —— CI 篇

這樣一來,對于開發人員來說需要關注的分支就隻有主幹和自己 working 的分支兩條,隻需要 push 與 merge 兩條 git 指令就能完成所有分支操作。同時,由于合入頻率的提高,平均每人需要解決的沖突量大大減少,這無疑解決了很多開發人員的痛點。

需要說明的是,分支政策與版本釋出模式沒有銀彈。我們采用的政策可能并不适合所有團隊的項目。提高合入頻率盡快能讓産品快速疊代,但無疑會讓新開發的特性很難得到充分的手工測試及驗證。

為了解決這一沖突點,這背後需要有強大的基礎設施及長期的習慣培養做支援。這裡将難點分為如下幾個類型,大家可以針對這些難點做一些考量,來确定是否有必要采用主幹開發的方式。

完善且快速的自動化測試。隻有在單元測試、內建測試、E2E 測試覆寫率極高,且通過變異測試得出的測試用例品質較高的情況下,才能對項目品質有一個整體的保證。但這需要團隊内所有開發人員習慣 TDD(測試驅動開發)的開發方式,這是一個相當漫長的工程文化培養過程。

Owner 責任制的 Code Review 機制。讓開發人員具有 Owner 意識,對自己負責的子產品進行逐行審查,可以在代碼修改時規避許多設計架構上的破壞性修改與坑點。本質上難點其實還是開發人員的習慣培養。

大量的基礎設施投入。高頻的自動化測試其實是一個相當消耗資源的操作,尤其是 E2E 測試,每一個測試用例都需要啟動一個無頭浏覽器來支撐。另外,為了提升測試的效率,需要多核的機器來并行執行。這裡的每一項都是較大的資源投入。

快速穩定的復原能力和精準的線上及灰階監控等等。隻有在高度自動化的全鍊路監控下,才能保證該機制下釋出的新版本能夠穩定運作。這裡的建設我會在之後的文章裡詳細介紹。

對于大多數項目來說,在代碼編譯完成生成産物後,部署項目的方式就是登入釋出伺服器,将每一次生成的産物粘貼進釋出伺服器中。生成的靜态檔案由于 hash 不同可以同時存放,html 采用直接覆寫的方式進行更新。

直接使用複制粘貼的方式來操作檔案的更新與覆寫,這樣既不友善對更新曆史的審計與追溯,同時這樣的更改也很難保證正确性。

除此之外,當我們需要復原版本時,由于伺服器上并沒有存放曆史版本的 html,是以復原的方式其實是重新編譯打包生成曆史版本的産物進行覆寫。這樣的復原速度顯然不是令人滿意的。

一個解決方法是,不要對檔案進行任何的覆寫更新,所有的産物都應該被上傳持久化存儲。我們可以在請求上遊增設一個流量分發服務,來判斷每一條請求應該傳回哪一個版本的 html 檔案。

對于大型項目來說,傳回的 html 檔案也不一定不是一成不變的。它可能會被注入管道、使用者自定義等辨別,以及 SSR 所需要的首屏資料,進而改變其代碼形式。是以,我們認為 html 檔案的制品提供方應該是一個單獨的動态服務,通過一些邏輯完成對模闆 html 的替換并最終輸出。

總結一下,在每次編譯完成後,産物将會進行如下的整理以生成最終的前端制品:

針對靜态檔案,如 CSS、JS 等資源将會釋出到雲對象存儲中,并以此為源站同步給 CDN 做通路速度優化。

針對 HTML 制品,需要一個直出服務做支撐,并打包成 docker 鏡像,與後端的微服務鏡像同等級别,供上遊的流量分發服務(網關)根據使用者請求選擇調起哪些服務負載進行消費。

對于一個好的工具來說,内部設計可以很複雜,但對于使用者來說必須足夠簡單且好用。

在主幹開發這樣高頻的持續內建下,內建速度即效率,流水線的執行時間毫無疑問是開發人員最關心的,也是流水線是否好用的決定性名額。我們可以從幾個方面着手,提高流水線執行效率,減少開發人員的等待時間。

對流水線各個階段需要執行的任務我們需要遵循一定的編排原則: <code>無前置的任務優先</code>, <code>執行時間短的任務優先</code>, <code>無關聯的任務并行</code>。

根據這一原則,我們可以通過分析流水線中執行的各個任務,對每一個任務做一次最短路徑依賴分析,最終得出該任務的最早執行時機。

Docker 提供了這樣一個特性:在 Docker 鏡像的建構過程中,Dockerfile 的每一條可執行語句都會建構出一個新的鏡像層,并緩存起來。在第二次建構時,Docker 會以鏡像層為機關逐條檢查自身的緩存,若命中相同鏡像層,則直接複用該條緩存,使得多次重複建構的時間大大縮短。

我們可以利用 Docker 的這一特性,在流水線中減少通常會重複執行的步驟,進而提高 CI 的執行效率。

例如前端項目中通常最耗時的依賴安裝 <code>npm install</code>,變更依賴項對于高頻內建來說其實是一個較小機率的事件,是以我們可以在第一次建構時,将 <code>node_modules</code>這個檔案夾打包成為鏡像供下次編譯時調用。Dockerfile 示例編寫如下:

我們給流水線增加一條檢查緩存命中的政策:在下次編譯之前,先查找是否有該鏡像緩存存在。并且,為了保證本次建構的依賴沒有更新,我們還必須比對本次建構與鏡像緩存中的 <code>package-lock.json</code>檔案的 md5 碼是否一緻。若不一緻,則重新安裝依賴并打包新鏡像進行緩存。若比對結果一緻,則從該鏡像中直接取到 <code>node_modules</code>檔案夾,進而省去大量依賴安裝的時間。

流水線拉取鏡像檔案夾的方法示例如下,其中 <code>--from</code> 後跟的是之前緩存建構鏡像的别名:

同理,我們也可以将這一特性擴充到 CI 過程中所有更新頻率不高,生成時間較長的任務中。例如 Linux 中環境依賴的安裝、單元測試每條用例運作前的緩存、甚至是靜态檔案數量極多的檔案夾的複制等等,都能利用 Docker cache 的特性達到幾乎跳過步驟,減少內建時間的效果。由于原理大緻相同,在此就不贅述了。

衆所周知,流水線的執行時間一定會随着任務數量的增多而變慢。大型項目中,随着各項名額計算的接入,各項測試用例的數量逐漸增多,運作時間遲早會達到我們難以忍受的地步。

但是,測試用例的數量一定程度上決定着我們項目的品質,品質檢查決不能少。那麼有沒有一種方法既可以讓項目品質得到持續保障的同時,減少開發者等待內建的時間呢?答案就是分級建構。

所謂分級建構,就是将 CI 流水線拆分為主建構和次級建構兩類,其中主建構需要在每次送出代碼時都要執行,并且若檢查不通過無法進行下一步操作。而次級建構不會阻塞工作流,通過旁路的方式在代碼合入後繼續執行。但是,一旦次級建構驗證失敗,流水線将會立即發出通知告警,并阻塞其他所有代碼的合入,直到該問題被修複為止。

對于某任務是否應放入次級建構過程,有如下幾點原則:

次級建構将包含執行時間長(如超過 15 分鐘)、耗費資源多的任務,如自動化測試中的 E2E 測試。

次級建構應當包含用例優先級低或者出錯可能性低的任務,盡量不要包含重要鍊路。如果自動化測試中的一些測試用例經過實踐發現失敗次數較高,應當考慮增加相關功能單元測試,并移入主建構過程。

若次級建構仍然過長,可以考慮用合适的方法分割測試用例,并行測試。

工欲善其事,必先利其器。騰訊文檔項目高頻穩定釋出的背後,必定需要擁有強大基礎設施的支援。

本篇文章僅主要介紹了持續內建階段對項目進行的改造,持續部署、持續營運等階段的具體改造思路将在筆者接下來的文章中詳細說明。也歡迎大家多多探讨,對其中需要改進或有誤的部分提出建議與斧正。

《持續傳遞 2.0》—— 喬梁 著

https://www.redhat.com/zh/topics/devops/what-is-ci-cd

https://www.36kr.com/p/1218375440667012

更多關于雲原生的案例和知識,可關注同名【騰訊雲原生】公衆号~

福利:

①公衆号背景回複【手冊】,可獲得《騰訊雲原生路線圖手冊》&amp;《騰訊雲原生最佳實踐》~

②公衆号背景回複【系列】,可獲得《15個系列100+篇超實用雲原生原創幹貨合集》,包含Kubernetes 降本增效、K8s 性能優化實踐、最佳實踐等系列。

③公衆号背景回複【白皮書】,可獲得《騰訊雲容器安全白皮書》&amp;《降本之源-雲原生成本管理白皮書v1.0》

【騰訊雲原生】雲說新品、雲研新術、雲遊新活、雲賞資訊,掃碼關注同名公衆号,及時擷取更多幹貨!!
大型前端項目 DevOps 沉思錄 —— CI 篇

繼續閱讀