Blog

91APP Backend RD 的日常

knight.jpg
Knight Huang2020/06/24


本文跟大家分享的主題是 Backend RD 的日常,身為大型電子商務系統的後端工程師平時究竟都在做些什麼樣的工作呢?軟體開發工程師只要寫寫程式碼就好了嗎?我們是不是應該更宏觀的來看待整個產品生產的過程。

今天的大綱分為三個部分,分別是:

  • 怎麼讓 Code Review 的效率提升?
  • 如何使程式碼更健康?(Unit Test)
  • 面臨多個環境的 Deploy 情境怎麼做?

上述這三個環節是開發人員從開始提交程式碼異動一路到部署至正式環境的過程,其中都有一些我們逐步累積的經驗與成果要跟大家分享。以前的我們只要關心功能開發,專注在程式碼相關的問題,現在的我們則是從需求一路到交付、維運都與我們息息相關,密不可分。


怎麼讓 Code Review 的效率提升?

首先我們來聊聊關於提交程式碼異動後會發生的事 - Code Review。程式碼審查通常會需要一些作業時間,怎麼讓審查的時間縮短、效率提升?

Git Flow

在講到程式碼審查之前,先聊聊我們是如何在 Git 上作業的。一開始先簡要的說明我們所使用的 Git 分支策略 Git Flow。

GitFlow

如果有在關注 Git 領域相關資訊的人,這張 Git Flow 分支的示意圖相信對你並不陌生,Git Flow 中常見的幾個主要分支分別是:

  • master 分支我們用來部署在正式環境
  • develop 分支用來部署在測試環境,並整合所有 feature 分支的異動
  • feature 分支作為開發提交新的異動,是開發人員最常使用的分支
  • release 分支在正式部署前的 Pre Production 環境部署測試,在準備部署到正式環境前會由 develop 分支來建立 release 分支
  • hotfix 分支則用來快速修復正式環境的線上問題所使用

以上就是 Git Flow 當中幾個重要的分支。

GitHub Flow

講完了 Git Flow 後,接著要介紹的是 GitHub Flow 的部分,其中幾個主要的階段如下圖所示:

GitHubFlow

  • Create Branch 開發人員在開始異動前先由 develop 分支建立新的 feature 分支
  • Commits 在 feature 分支進行開發並提交程式碼異動
  • Pull Request 將異動提交合併請求
  • Code Review 由審查人員進行程式碼審查並給予開發人員修正建議
  • Merge 直到審查通過後才合併回到 develop 分支

這就是 GitHub Flow 中主要的幾個步驟。

提交程式碼 (Pull Reqeust)

接下來要說明的是 Pull Request,我們平日的作業情境就是將 Git Flow 與 GitHub Flow 合並來運用:

PullRequest

首先 origin 是開發人員自己的儲存庫,而 upstream 則是公司所擁有的儲存庫。所有的開發人員都是基於 upstream 的內容來建立新的分支進行開發,開發完成後再透過 Pull Request 來提交新的異動內容,整個過程都是基於 Git Flow 及 GitHub Flow 來運行,這當中的 Pull Request 則是開發人員在開發完成後的必經之路。

程式碼審查 (Code Review)

Pull Request 之後最重要的關卡就是程式碼審查了。審查人員會針對開發人員本次所異動的內容進行審查,來決定是否允許本次的異動合併到 upstream 當中,審查人員會審視程式碼的寫作風格、程式邏輯及商業邏輯等並給予開發人員回饋及建議。

如果有需要修正,開發人員則需根據建議進行調整後再行提交,直到沒有問題的程式碼才允許合併進到 upstream 儲存庫當中。

Code Review 的過程也許很快,也可能會很漫長。開發人員如果一次交付太大量的異動內容也會讓整個審查的時間拉長,持續的小增量疊代交付會是個不錯的方式。

CodeReview

Linter 參與程式碼審查

接著我們來談談 Linter,它可以怎麼幫助我們。為了讓程式碼審查的效率與品質提升,在 Linter 上我們會建立許多規則來針對程式碼的異動進行檢查,並且在開發人員發出 Pull Request 後經由 Webhook 驅動 Linter 來執行程式碼檢查。

檢查的內容可能 MVC 站台 Controller 上的 Action 是否有正確的使用指定的 Attribute 或者是 SQL 命令的撰寫方式是否合乎規範來進行審查。 透過 Linter 我們可以達到:

  • 自動化
  • 提升程式碼品質
  • 改善效率
  • 降低人為錯誤
  • 經驗、知識數位化

LinterCodeReview

在程式碼審查的過程中,有了 Linter 的幫助開發人員可以更迅速的得到回饋進行調整,常見的問題在交付初期就可以被檢驗出來,對開發人員甚至對審查人員來說都有很大的幫助。

每一次的往返都是時間成本,對開發人員是,對審核人員也是。

這一個段落討論的是從開發到提交程式碼的過程,從建立分支到提交合併請求,在程式碼審查階段可以透過 Linter 建立對應的檢查規則來提示程式碼撰寫時應該注意的事項,或提醒不洽當的寫作方式。開發人員可以快速的得到回饋,避免時間上不必要的浪費。


如何使程式碼更健康?

第二個部分要講的是單元測試的部分,測試的目的不外乎就是要確保系統本身的運作正常,那如何使程式碼更健康?

複雜的系統

我們的系統只有一套程式碼 Single Code Base,但是我的面對的是如下圖所示的複雜使用情境。

SingleCodeBase

上述的這些還僅僅只是正式環境上的數據,其他還有測試環境跟 Pre Production 環境,在這樣的條件之下怎麼確保系統的品質及正確性,是我們很重要的課題。

真的不考慮寫 Unit Test 嗎?

沒有測試過的東西可以交付嗎?為了提升產品的品質,有很多種方法,投入大量的測試人力資源也可以達到一樣的效果,但這樣做有辦法變"快"嗎?

開發人員自己寫的程式有自己測試嗎?你是怎麼測試自己寫的程式碼的呢?

  • 手動測試:異動、建置 (Full)、執行、驗證
  • 單元測試:異動、建置 (Partial)、驗證

一般來說最直覺得測試,就是透過萬能的雙手,一步一腳印的操作來驗證,但是也許一個案例就需要花費你 3 ~ 5 分鐘的時間,這樣你時間夠用嗎?

單元測試可以只針對局部的方法進行驗證,僅需要較短的時間就能驗證成果,這樣真的不考慮撰寫單元測試嗎?

測試金字塔 (Test Pyramid)

講到單元測試就不免俗的要提到測試金字塔來說說:

testPyramid

測試金字塔的構成由下至上分別是:

  • Unit Test 針對類別的方法進行測試
  • Service Test 整合多個類別進行服務之間的互動測試
  • UI Test 與使用者一樣從系統介面進行操作來測試

測試速度越靠近金字塔頂端越慢,越靠近頂端關係到使用者介面及多個的服務則越複雜。橫軸是測試數量分佈的多寡,越底端發生的錯誤可以越快定義問題與解決問題。靠近頂端的問題,則牽涉的範圍較廣要確切的找到問題發生的位置就需要花費更多的時間。一般來說以金字塔的形狀來配置上述三種測試類型的數量會是比較好的方式。

另外相反的模式則是像冰淇淋甜筒般的配置測試:

software testing icecream cone antipattern

先大量的手動測試,其次是自動化測試,再來是整合測試,最後是較少量的單元測試,在這樣的測試配置下要花費更多的時間來測試及解決問題。

Unit Test 衡量指標

如果要透過單元測試來提升產品的品質,那怎麼衡量將會是首先要考慮的。

究竟我們現在單元測試做的程度如何?是變好了?還是變差了?為了量化所以我們要定義衡量好與壞指標是什麼?

  • 測試案例數量:透過常見的三正一反或邊界值測試來增加測試案例,數量越多越好。
  • 測試通過率:用通過率做為指標!? 出現測試失敗的程式碼可以交付嗎???
  • 測試含蓋率:程式碼有被單元測試所執行過的範圍。

以測試含蓋率為指標是我們認為較能呈現出程式碼異動與單元測試之間的關連性,一但程式碼有調整,涵蓋率就有可能隨之下降。透過測試涵蓋率的觀察,我們可以知道程式碼是否充分的受到單元測試的保護及其成長的趨勢究竟是上升還下降。但是特別要提醒的是即使測試涵蓋率 100% 也不代表產品 100% 沒問題,這只是我們用來衡量趨勢用的指標。

Code Coverage Tool

既然我們要以測試涵蓋率為指標,這邊就列舉了幾個測試涵蓋率的分析工具:

  • Visual Studio:但是需要 Enterprise 等級以上才可以使用
  • dotCover:包含在 ReSharper 之下
  • AxoCover:目前在 Visual Studio Marketplace 上的說明僅支援到 Visual Studio 2017
  • OpenCover:Open Source
  • Coverlet:跨平台且支援 .Net Core

透過這些工具可以讓我們獲得程式碼的測試涵蓋率,以及知道程式碼的那些段落是沒有受到單元測試保護的部分。

核心方法 (Core Method)

系統運行的監控可以幫助我們了解線上系統是否有無異狀,那程式碼是不是健康的狀態有被關心嗎?接下來要說明的是我們怎麼樣監測測試涵蓋率。

由於程式碼的數量龐大我們沒有辦法投入所有的資源來撰寫單元測試,所以將有限的資源集中在幾個重要的商業邏輯上。我們列舉了系統當中重要的核心方法,這些方法是相對於產品當中最核心的功能,特別針對些方法來監測其測試涵蓋率的變化,並加入我們的持續整合流程當中,針對提交的程式碼進行驗證,一但有程式碼合併後便會開始執行測試涵蓋率分析。幾個主要的步驟如下:

  • 系統建置
  • 測試涵蓋率分析
  • 核心方法的測試涵蓋率檢核及記錄
  • 如果有測試涵蓋率下降則通知團隊

CoreMethod

透過這個檢核機制來確保這些核心方法都有受到單元測試的保護。一但有測試涵蓋率未達標準的方法出現,將透過 Slack 的訊息來主動通知團隊進行維護,如圖所示:

SlackNotify

訊息當中包含了

  • Repository Name:儲存庫名稱
  • Project Name:專案名稱,此專案下未通過的數量有 6 筆
  • Method Level Coverage:Update 方法的測試涵蓋率為 33% 低於目標值 100% 及其所屬團隊為 g11n
  • Namespace Level Coverage:命名空間層級的測試涵蓋率,一但 Namespace 下的程式碼有異動未加上測試就會反映在涵蓋率上
  • Project Level Coverage:專案層級的測試涵蓋率

Code Coverage Log

透過涵蓋率的歷史記錄我們可以清楚的知道各方法的測試涵蓋率變化情形,在這邊列舉兩個核心方法測試涵蓋率的歷史記錄:

CodeCoverageLog

X 軸是時間以週為單位,而 Y 軸則呈現的是測試涵蓋率的百分比,以紅色線條 Method 2 的例子來說,在開發初期就已經加入核心方法的清單,所以涵蓋率由 0% 開始上升到 81% 隨著後續的程式碼異動導致涵蓋的下降,再經由收到通知的團隊後補上單元測試來提高測試涵蓋率,藉由涵蓋率歷史紀錄的歷程可以幫助我們了解該方法變化趨勢為何。

當然更好的做法應該是採用測試驅動開發 ( TDD ),讓程式碼在開發當下就充分的受到測試的保護,持續的維持測試涵蓋率在 100% 的狀態,但這就很仰賴開發人員個人的功力。

這個段落說明的是,我們推動單元測試的過程無法全面性的加上測試,所以把目標放在重要的核心方法上,並加以監測這些方法的測試涵蓋率變化。


面臨多個環境的 Deploy 情境怎麼做?

講完單元測試後,第三個部分要提到的是我們的部署流程的演化過程。

多市場的部署

首先先說明我們在部署上遇到了什麼樣的問題。

MultipleMarket

一開始我們只有一個市場,位於東北亞的區域開始營運,我們建立了屬於這個市場的 Jenkins 服務來進行系統部署。

幾年後在東南亞成立第二個市場,我們複製第一個市場的經驗來建構環境,也配置了另一座 Jenkins 服務來部署系統,但這意味著我們要維護 Jenkins 服務的成本提高了。

又過了一陣子東北亞出現了第三個市場,但是分屬在不同的環境底下,導致做法有別於第一個市場,又由於環境配置的問題,部署流程略有差異存在。

不久後在東南亞也開設了第四個市場,又針對這個市場建置了專屬的部署流程,在這樣的因果關係之下,使得我們的系統在部署上遇到了瓶頸,圖上還僅只是正式環境上的呈現,另外還有多達十幾座的測試環境也都有部署上的需求,Jenkins Job 的數量跟維護成本也隨之升高。

持續整合、交付 (CI / CD)

我們透過 Jenkins Job 來實現持續整合及交付,初期的做法是一條龍的把 CI 跟 CD 放在同一個 Job 當中一次完成整個流程,但隨著系統的擴展變化我們遇到了維護上的困難。因此,我們調整了部署策略來改善維護上的問題,讓整合與交付分開來運行來實現 Single Build, Multiple Deploy. 並以 Artifacts Management 做為整合與部署之間的介面,如此一來我可以更彈性的來設計整個整合部署的流程。

CI CD

接下來我們就將整個部署流程分成建置與部署兩個部分來說明。

Jenkins - Build

首先是建置的部分,在我們的 CI / CD 流程當中引用了一些新的機制,其中包含 Jenkins 的分散式建置來提升效率,整個部署流程當中系統建置、測試與驗證是最占用資源的項目,因此透過分散式的建置以分散原本只有單一座 Jenkins 伺服器的負擔,也只需要維護同一份 Job 組態設定。Configuration Management 主要用於多環境的組態管理,用來解決每個環境的不同設定值的問題,在建置完成後我們將進行測試及驗證,驗證單元測試是否有失敗的測試案例或是核心方法的測試涵蓋率是否有低於標準,未通過驗證將會中斷我們的建置流程。Artifacts Management 則用來將我們建置好的應用程式打包並上傳進行存放,好做為部署時期的來源資料以解決多環境跨區域的問題。

Build

Jenkins - Deploy

接著說明的是部署階段,由於建置階段已經將應用程式打包並上傳至 Arctfiacts Management,接下來的 Jenkins Job 只需要將應用程式部署到指定環境的伺服器即可。透過 Artifacts Management 來做為建置與部署之間的介面,在跨區域、跨市場的部署上就相對單純多了,不同環境有著一致的運行方式,維護的成本也下降了。

Deploy

我們可以更有彈性的來設計整合及部署流程,進而優化部署流程如,藍綠部署、金絲雀部署等。這個段落說明的是我們整個部署的架構演化過程,從一個市場成長到四個市場,並引入新的機制來改善這過程中所遇到的困境。


總結

這次提到的幾個主題分別是:

  • Code Review 透過 Linter 可以加快整個審查的過程
  • Unit Test 透的監測機制來確保程式碼的品質
  • 面臨複雜的多環境 Deploy 流程改善
    • Jenkins Distributed Builds
    • Configuration Management
    • Artifacts Management

這不一定是最好的方法 但是持續的藉由回饋來改善將會一直的疊代下去...

身為 Backend RD 我們所關注的問題不再只是程式碼本身,而是整個產品從開始到交付的過程怎麼更快、更好、更快速的因應市場的改變。


參考資料

© 2021 91APP, Inc. 版權所有
* Version v0.9.6 * 2021/05/29 14:44