開發團隊的代碼管理

簡介

源代碼根據不同使用者的需求有著不同的指標要求, 針對這些要求和工程成本之間的權衡, 可以使用分支和TAG策略, 並結合DevOps等思想, 對代碼進行工程化的管理, 以實現高效的開發, 測試, 部署. 基本思想來自A successful Git branching model, 主要的分支除master外應包括:

Feature: 單一功能的開發, 測試分支, 對代碼等級要求較低.

Dev: 開發團隊用以合併功能的分支, 對代碼的最低要求為可開發, 可復現, 可檢驗.

Release: 用以交付的分支, 對代碼的最低要求為可開發, 可復現, 可檢驗, 可回滾.

Hotfix: Release的一種特殊類型, 常見於對舊系統的臨時性修復, 但是其代碼需要向dev和master進行合併.

使用以上分支模式, 並對其的檢出, 提交, 合併加以許可權控制, 使用自動化工具進行嚴格的流程安排, 來滿足不同使用者的要求.

前言

對於軟體開發團隊來說, 源代碼是其最重要的產出和資產, 但是對源代碼的管理遠遠沒有一套標準的流程. 當然大部分公司都會有自己的一套管理規範, 但實際上往往流於形式, 真正跑起來那真是天馬行空, 瀟洒無比.

當然, 各家SCM系統已經運行了很多年, 無論CVS, SVN, Perforce還是GIT都有一些最佳實踐. 網上有很多適用於不同場景的模式可供參考, 大家可以根據自己的需要和團隊的情況進行選擇. 本文真是根據個人的經驗對於源代碼的管理提出一些看法.

目標

源代碼即是團隊的產出, 也是團隊的用來進行生產的工具和資本. 我很喜歡的一個關於軟體開發的類比就是電影的生產, 源代碼即是原始膠片, 程序員則按照劇本和自己的了解在代碼中記錄將用戶的需求進行演繹. 而在最終剪輯(編譯, 鏈接)之前, 誰也不知道結果會是怎樣.

很難想像一家電影廠的原始膠片就那麼隨隨便便的堆在屋子裡, 誰想扔點什麼進去就扔進去, 你是導演你不砍人? 但是儘管源代碼如此的重要, 很多公司對代碼的管理是非常粗陋的, 僅僅停留在文件共享這個層面上. 就算有各種管理章程, 很多管理的主要目標也是保證代碼的安全, 毫無疑問這也是由於長期以來對代碼本身的漠視所導致的.

實際情況中, 大部分團隊認為代碼編譯出的軟體才是產出. 因此最起碼的會有各種tag或者版本的管理. 這很正確, 但是遠遠不夠, 代碼更為重要的作用是團隊用來生產的原材料: 新的功能, 更高效的演算法, 都是基於現有的代碼而進行開發. 因此, 對此種數字資產的管理, 也需要有著嚴格的使用上的界定和相應的措施.

我認為代碼的使用人員可以分為四類, 即:

  1. 開發人員: 負責添加/修改/刪除代碼, 對代碼的直接操作最多.
  2. 測試人員: 負責保證代碼可以實現預期的功能, 以及不會出現預期以外的行為.
  3. 產品人員: 是用戶需求在團隊內的代理, 他們往往從最終用戶的角度對代碼提出修改意見.
  4. DevOps: 對上述三類角色進行居中調度, 溝通, 幫助, 貫徹技術棧. 也要負責開發團隊和運維團隊的對接工作

而代碼管理的目標, 即是最大限度的對以上四類人員提供便利, 提高效率. 或者按照人月神話中的說法, 降低團隊的溝通成本, 是團隊可以通過增加人力來提高生產力. 那麼這四類人在代碼上的主要痛點都有哪些呢? 以下是我在這麼多年的工作中碰到的一些問題:

  1. 開發: 盡量避免代碼的merge, 或者說開發人員彼此之間的並發乾擾帶來了很大成本: 辛辛苦苦寫好的代碼, 被別人衝掉了都不算嚴重的問題, 好用的工具和流程可以很大程度避免本地開發的一些合併損失. 真正可怕的是安安靜靜引入bug, 往往要到最後的集成測試階段才會發現.
  2. 測試: 回歸測試問題, 今天開發Fix掉的bug, 後天會不會被其他人再次引入; 當然實際上測試環境不穩定, 以及如何reproduce一個bug也是非常頭痛的事情.
  3. 產品: 這幫程序員到底搞出了什麼東西? 如何確認當前的進度? 如果定期向用戶演示以保證用戶的參與感和認同度?
  4. DevOps: 跟所有人對接的痛苦就在於信息太多, 難以提煉重點. 你們到底想要什麼? 可不可以用相同的語言, 文件格式來闡述各自的需求? 開發環境運行好好的代碼, 生產環境怎麼就出錯了呢? 代碼改怎麼回滾, 依賴項都在哪兒? 是快照還是發布版本? 我怎麼和運維人員進行對接?

所以對於代碼的管理就應該從以上的問題出發, 儘力加以解決.

指標:

既然確定了目標, 那麼我們就可以以此來對代碼進行管理. 而任何的管理都可以看成對KPI的監控和干預, 對應代碼管理, 我認為有如下方面的指標可供參考:

對於開發, 代碼的擁有者, 主要的問題就在於如何與他人合作修改, 解決開發工作上的並發衝突, 此時對代碼的指標要求是:

可開發性:

  1. 代碼是可編譯的/沒有語法錯誤的: 各種IDE或者Debug工具可以編譯或運行此份代碼.
  2. 代碼是可檢出的:從任意版本可以被檢出或rebase為新分支, 不會出現某個提交在邏輯上是不完整的..
  3. 代碼是可信賴的: 被檢出的代碼是不存在Bug的, 任何錯誤都只能由新添加的代碼所引入.

這三個指標可以認為是可開發性的三個級別. "可編譯"是其中非常基本的需求, 一旦低於這個標準, 基本就是全組開罵的場面. 但"可檢出的"可能大家沒有注意過, 但實際上我們經常遇到. 舉個例子, 我和大牛在開發一個功能, 我手賤了, 先提交一個不完整的介面聲明, 心裡想著明天上班了在改成正式的. 可大牛晚上2點加班, 以為這是終版, 檢出到dev分支, 寫了一個晚上的代碼然後又被全組檢出到各自的feature. 你說第二天我把介面改回來會不會被追殺整條街?

對於如何滿足代碼可檢出到可信賴, 那就是引入各種代碼審核工具和相應的分支策略. 儘可能的為每個人開闢獨立的開發空間, 同時使用自動化的審核和合併工具來規範各獨立Repo的同步. 當UT的覆蓋率達到相應的水平, 基本上可以保證代碼是可檢出的.

而可開發的最高級別, 完全可信賴的代碼則是一個非常理想化的情況, 這要求程序員任何一次提交都應當是完美的, 不含邏輯錯誤的原子提交. 事實上我認為光憑開發人員是無法高效滿足這一要求的, 強行要求的話會造成開發成本上升到不可接受的底部. 我們只能再團隊情況, 時間約束的前提下儘可能逼近這個標準. 具體細節的話我得開個單章來講, 叫做<代碼質量及其他>, 這個話題下各種工具, 各種討論, 各種撕逼, 一言難盡~~~

對於測試, 怎麼解決環境問題和回歸問題? 此時其對代碼的要求是:

可測試性:

  1. 代碼是可以運行的: 無論在正常的運行環境, 測試環境還是樁環境下, 代碼需要可以被運行.
  2. 代碼是可以被追溯的: 任何一次代碼的運行, 都可以定位到相應的代碼版本.
  3. 運行可以被複現的: 無論任何時刻, 任何相應代碼版本都可以定位其彼時的依賴, 數據, 做到對測試環境的精確復現.
  4. 代碼是被保護的: 一旦測試完成, 已經被修復的bug將不會再次出現.

同樣, 這四個級別是依次遞增的. 可運行是對測試來說的基本要求, 可追溯是報告bug的必要條件, 可復現是修復bug的先決條件, 而第四個級別的代碼則是可以將回歸測試的成本降到最低. 相應的其成本也會不斷攀升.

對於代碼的可運行, 一般的做法是建立Acceptance Testing或者叫做Smoking Test, 建立一套基本的測試邏輯, 自動化的對代碼進行檢測, 不滿足最低要求(編譯無錯誤, 可部署, 可運行, 通過最基礎的運行要求, 如界面被渲染, 基本功能通過, 等等)則不對測試環境進行更新, 甚至阻止代碼的合併.

而追溯和復現一般的情況是什麼呢? 其實也很簡單, 團隊只維護一個分支和一個環境; 只有當代碼可以運行時才開始進行測試, 不可以就歇著; 也不存在追溯, 因為任何時刻都是HEAD; 也不存在復現, 因為只有一個環境. 至於第4個級別就只能看程序員的自律.

但實際上大型項目的追溯和復現特別複雜, 一個分支同時進行開發和測試是非常低效的, 開發的提交要遠遠超過測試部署的速度, 使得報Bug時候的代碼和程序員看到Bug時候的代碼往往是不一致的. 而不斷變化的測試環境也給復現帶來極大的困擾, 因為最可怕的bug就是那種無法復現的, 最討厭的情況是環境每次都崩潰的. 解決的辦法往往是引入複雜的版本控制機制和相應的日誌機制, 與之相對應的就是Bug描述的高度格式化文本(環境, 代碼, 數據的相應版本). 而我之前曾經通過Feature Branch模式, 部署Docker化來解決這個問題, 事實上效果不錯, 每個功能和迭代都有其相應的部署和測試環境, 將干擾降到最低.

事實上對於第四個級別, 也就是如何儘可能降低回歸測試的成本, 哪怕完全引入自動化測試也無法徹底解決. 我們只能儘可能避免此種問題, 比如各種凍結策略(需求凍結, 功能凍結, 開發凍結, 代碼凍結), 比如各種衝突監測(代碼依賴靜態分析等等).

相對於這種保守的策略, 也有一些較為激進的嘗試, 比如任何新引入的代碼都要受到多重的檢查以及相應的回歸測試, 結合全面的自動化測試手段, 儘可能的保護代碼收到後續代碼的污染. 效果還可以, 但是對測試人員和產品人員提出了相當高的要求. 並不是可以輕易適用於所有的團隊. 強行做到也能: 要求代碼全分支覆蓋測試, 全邏輯覆蓋測試, 嗯, 每天能寫5行不?

這個其實看行業, 我覺得NASA的代碼達到第四個級別應該是可以的, 也是必須的.

對於產品人員, 傳統上他們是不關心代碼的, 只需要維護相應的產品需求文檔和開發合同即可. 但是隨著敏捷, 全棧團隊等開發概念的深入, 產品人員越來越直接的介入開發工作. 此時, 對於他們來說, 如何"檢驗"代碼是最重要的, 即:

可檢驗性

  1. 可上線: 每個被測試通過的功能都應該可以上線.
  2. 可實時上線: 每個被測試通過的功能都應該立刻上線.
  3. 可回滾: 任意一個功能可以被下線.

換句話說, 任何一個被QA確認的功能都應當可以被從用戶的角度進行檢驗. 功能A做好了, 但是代碼裡面還有其他的半成品代碼, 這是最好了還是沒做好? 不能上線的功能就是未完成的功能.

而且不僅應該可以部署, 新完成的功能也必須立刻上線, 隨時等待用戶(產品和甲方)的審核, 此時可以儘快的了解實現和用戶需求之間的偏差, 第一時間發現溝通上的問題. 不要讓後續的開發基於錯誤的假設而進行.

但是檢驗的結果往往並不是總是積極的, 一個功能被確認無效甚至錯誤, 需要從代碼中刪除, 此時需要良好的代碼管理機制來實現代碼的回滾甚至是修改不會引入其他的錯誤. 雖然在傳統軟體的瀑布流程中並不常見, 但是在現在這個互聯網時代, 無論對於A/B測試還是灰度發布都是必要的操作. 這個問題也並不是一個單純的代碼問題, 往往涉及到整個系統的架構和模塊間的調用模式.

最後, 讓我們需要來解決DevOps的問題, 作為團隊的協調者, 貫穿產品, 開發, 測試到部署及運維的人員, 他需要什麼呢?

可維護性:

  1. 代碼是自治的: 所有編譯, 運行, 部署的信息都可以從代碼直接或間接得到.
  2. 代碼是自我驗證的: 測試用例應當是代碼的一部分, 結合CI流程以自動化的方式來對代碼進行自我約束.

和前者不一樣, 這兩者之間不是遞增的關係, 而是並列的. 能做到的越多, 越好, 對DevOps來說可以施展的餘地就越大.

換句話說, 代碼需要可以被準確的理解和被操作. 以下情況代碼就是不可被維護的:

  • 代碼沒有第三方依賴關係: 只仍源代碼, 以來的jar包是什麼, 版本是什麼?
  • 代碼裡面有冗餘: 源代碼Repo有兩個工程, 一個叫package1, 另外一個叫package2, 99%代碼都一樣, 毫無疑問是懶的改了直接copy了一份重來. 但是你得告訴我輸出用哪個啊.
  • 以上缺失的信息都有, 但是在另外一個word文檔上或者哪位的腦子裡.
  • 完全沒有部署或發布的信息.

以上這些情況DevOps都會比較頭大, 因為這些情況會導致一個很嚴重的問題, 無法自動化或者建立自動化流程需要進行複雜調整. 難道每次打包都要問一遍責任人打哪個嗎?

而且在各種自動化手段非常成熟的今天, 無論採用Ivy, Maven, NPM還是其他的依賴工具和結構工具, 主流的IDE和CI工具都可以進行自動的識別和管理. 可維護的代碼將極大的提升自動化的效果.

所以針對這些問題, 我們就要引入各種工具. 對於依賴, 推薦Maven, 畢竟這個用戶最多, 搭建私有Repo也異常便捷. 對於配置管理工具, Java世界自然是Gradle, JS世界就呵呵大家隨意, 我是webpack+grunt. Restful服務的話, 請寫好swagger. 其他服務請按照各自的標準布局來寫, 這樣可以降低各種Customize操作的成本, 也方便新成員的接手, 降低團隊風險.

管理方案

綜上, 對一份源代碼的評價的指標如下:

  • 可開發性, 級別遞增為: 可編譯 < 可檢出 < 可信賴;
  • 可測試性, 級別遞增為: 可運行 < 可追溯 < 可復現 < 可保護
  • 可檢驗性, 級別遞增為: 可上線 < 可實時上線 < 可安全回滾
  • 可維護, 按照實現與否: 自治, 自我驗證.

按照理想的模式, 當然希望所有方面都做到最好. 但很可能這會讓生產效率低到無法忍受的局面: 每寫一行代碼開4天會, 寫5頁文檔, 召集4輪專家評審, 完全沒問題, 一定能做到完美的代碼. 但我們是工程師, 需要在成本和收益之間取得平衡, 因此我們就需要容忍那些不完美, 於此同時在必要的地方做到完美.

因此, 我們將使用各種SCM來對代碼進行管理, 並結合各種CI/CD流程及工具來儘可能的實現自動化, 降低人員成本和誤差. 為了簡單起見(其實並不簡單...), 使用GIT作為例子來說明(CVS和SVN沒有cherry-pick, 負分...).

其實上面那麼多字描述的場景的就是兩件事情: 不要一起改代碼, 會衝突; 人是會犯錯的, 任何時候留記號, 留退路. 那麼對應到代碼裡面, 就是兩個方案: 分支和TAG.

為了解決開發性的問題, 可以使用複雜的開發分支模式, 我知道的有三種:

  1. 所有的開發在不同的release上進行, 設定不同的Milestone來設定分支所扮演的角色.
  2. 所有的開發在feature-branch上進行, 使用rebase/merge策略組裝dev分支, feature凍結後dev檢出為release分支, 也就是題圖的模式.
  3. 所有開發在master上進行, 使用複雜的cherry-pick策略組裝release分支. 聽說是G家採用的模式. 看起來很粗暴, 但是據說有及其複雜的CI流程進行輔助, 大概就是自己造了套自動SCM的意思, NB.

方案一, 實際上是使用Milestone來標記單分支的等級, 是需要額外的信息對代碼進行管理, 實際上是通過團隊合同(Team Agreement)和流程式控制制來管理, 這也是早年SCM的經典模式, 運行了幾十年, 服務過無數的項目. 但此種模式對測試來說還是標準的單分支模式, 對DevOps來說就很更難辦了, 此時代碼的維護性幾乎沒有, 往往需要專職的Build團隊來負責. 但是優勢在於較為簡單易懂, 開發/測試人員不需要過多的知識, 過程較為直觀.

方案二則是目前的互聯網的主流模式, 因為複雜的分支模型可以提供不同的代碼等級:

  • release: 最高等級要求的分支, 任意版本或tag都應該滿足:
  1. 最低為可檢出, 儘可能的可信賴: 出現任何問題都可及時啟動hotfix分支對其進行修復.
  2. 代碼必須處於被保護狀態: 被至於release分支的代碼, 未經嚴格的回歸測試不得添加新的新的提交
  3. 實時上線: 活躍的release分支必須與線上部署保持一致, 否則這個分支的存在就沒有意義.
  • master: 對DevOps的分支, 這個分支是所有分支的源頭, DevOps將創建這個分支並將其初始化為可維護的狀態, 滿足自治, 自我驗證的要求. 此外這個分支還將保存所有重要的TAG, 不同的TAG可以被檢出為獨立的release分支, 滿足維護上各種回滾的要求. 也是latest部署所使用的的分支. 基本上禁止開發人員對master的提交, 只有DevOps和Bot程序可以向其提交或合併代碼. 但是在項目啟動階段也只有在啟動階段, master可以處於較低的代碼等級.
  • dev: 對開發的分支, 也應當是可被產品人員檢驗的分支, 需要達到可檢出, 可復現, 實時上線. 敏捷開發中可以將這個分支部署為review.
  • feature: 幾乎可以認為是開發和對應測試人員的私人分支, 基本上就是單分支模式的開發模型. 在滿足可測試性的要求下幾乎可以隨意提交, 甚至在前期階段都可以不滿足可測試性的最低要求. 但是在合併回dev的時候, 必須將代碼等級提升至對應標準.

代碼之間的合併需要使用CI工具來自動完成, 這樣在每個合併環節就可以增加檢測工具來保證代碼的等級要求, 而這就要求代碼必須是可維護的. 他們之間的合併方向如下:

  1. master: 只能由release向其合併, 每個commit都應該由明確的TAG進行說明.
  2. dev: 只能由master檢出, 只能由branch合併, 合併前需要進行代碼等級檢測.
  3. release: 只能由dev檢出, 只能由dev合併, 合併前需要進行代碼等級檢測.
  4. feature: 只能由dev分支檢出, 不得從其他分支合併, 而只能從upstream進行rebase. 合併前需要進行代碼等級檢測並滿足fast-forward.

為了加強理解, 再放一下題圖:

在這種模式下, 一方面我們可以保證開發的高效性而允許低質量的代碼進入系統以進行合作, 另外一方面也可以通過層層檢查來保證代碼的級別.

而方案3, 據說也是方案2的變種, 實際上master被當做dev來用, 使用各種自動化工具來維護一個虛擬的master和之上的版本. G家的方案實在不可以常理度之, 不過聽說最近也在轉GIT.

分支管理的工程實現

方案二當然是比較美好的方案, 依託於GIT可以達到很好的效果. 但是缺點相對於方案一甚至單分支方案是什麼呢?

在於增加了很多開發代碼以外的成本.

理論上來講這不算什麼, 但是實際情況是大部分程序員知道怎麼寫代碼, 甚至有很多知道怎麼寫出好代碼; 但是對於如何對一個項目進行工程化的管理, 保持開發的持續性, 保證團隊的穩定性是沒有概念的. 因為做這些事情並不能直接讓功能完成的更好, bug解決的更快.

而且事實上額外的工作也會極大影響程序員的開發效率, 比如額外的rebase代碼如同例行會議一樣, 是對思路和工作狀態的極大幹擾.

因此, 使用複雜的代碼管理必須引入自動化工具, 無論是CI, CD還是自動化測試, 都需要以實時的方式對開發人員, 測試人員, 產品人員進行反饋. 這就需要大量的DevOps的介入, 也就是為什麼要強調代碼的可維護性. 總的來說, 就是基於現有的軟體和功能, 對代碼的提交進行不同級別的自動review和人工review. 細節可以請見我另外一篇短文, 理想化的DevOps流程. 其要點在於:

  1. 分支的合併許可權禁止授予開發人員, 而是賦給Bot, 在review流程後進行自動合併.
  2. Review流程需要自動化, 具體Review的要求視團隊實際情況而有所調整.
  3. Remote Repo的分支檢出操作應由Bot完成, 以保證分支的代碼水平.
  4. master, 各活躍release, dev, 以及處於測試狀態的feature分支, 應該都有其對應的CD部署, 且所有的部署都應該與對應分支的head同步.

而除了使用工具外, 也必須配合相應的管理工具和流程, 比如上述的檢出或合併操作可以與相應的JIRA事務通過分支和提交注釋來建立關聯, 讓開發團隊中不同角色都可以從中受益, 而不是為了流程而流程. 畢竟產生可以正常工作的代碼才是一切開發活動的最終目標.

以上.

季長冰.

2017. 12. 21. 凌晨

推薦閱讀:

先來改一波submit function.

TAG:DevOps | 源代碼 | 代碼質量 |