
作者:marinewu,騰訊 PCG 工程效能平臺部專家
騰訊 PCG 工程效能平臺部自 2020 年開始進行大倉基本能力建設,并在 2021 年與工蜂合作成立了代碼大倉研效聯合專案組,在此, 我們想分享大倉/單倉踩過的坑,我們認為這些坑是真實存在且很難避免的,不是小馬過河,
單倉并不簡單,成功的單倉所帶來的效果絕不止簡單的代碼聚合,但成本是大量的工具支持以及工程實踐,單倉像放大鏡,可以將優秀的工程實踐以極低的成本推廣,但同時也會將錯誤迅速放大,同時,向單倉遷移的程序也有相當程度的風險,本文會詳細討論單倉的益處、挑戰,以及我們對挑戰的應對之道,以供參考,
單倉/大倉是什么?
-
單倉:指Monorepo,或單一倉庫,是指將多個專案放在同個倉庫中的版本控制策略,單倉在實踐中可能有不同規模,如中心級、部門級、BG 級、公司級單倉,業界最著名的單倉是Google 的 google3,除一些開源專案,Google 內其它所有代碼都放置于其中,有超過 20 億行代碼和超過 86T 的容量,目前公司內部成功應用的單倉大多在部門級及以下,
-
大倉:指大型規模的倉庫,“大型規模”并沒有嚴格的定義 -- 在本文中,我們將大倉定義為“單機已無法承受其容量的倉庫”,一個粗略的估計:大于五千萬行代碼 或 容量超過 100G 的倉庫即為普遍認可的大倉,
單倉是我們的目的,而大倉則是(終極)單倉的必然結果,
為什么要發明單倉?
單倉的設計核心起源于"One Version"的哲學,并內化了規模化的思想,
Scalability 可規模化
在軟體工程中,我們希望“解決一次,解決全部”,即,任何的一個良好的工程實踐,都以自動化、規模化的手段幾乎零成本地推廣到所有的團隊及代碼,這樣可以減輕開發人員的心智負擔,使之將更多的精力放在創造性的作業上,典型的例子包括代碼靜態檢查、自動化測驗及持續集成、開發流程標準化、工具統一化等等,
單倉極大地簡化了規模化:當所有代碼都放置在一處,并且高度統一,則所有的工具都可以規模化地在單倉上作業,
Single Source of Truth ->One Version 從單可信源到單一版本
Single source of truth(SSoT)原則,是指開發人員在任意時間可以確定代碼倉庫內的哪個分支是唯一可信依賴源(SSoT),在 CVS 中,單一來源是核心原則;在 DVS 中,如 git,在現代的業界實踐中也采取了該原則,即要求永遠都只有一條主干,且所有的分支(除了發布分支)最終都會被收攏回主干里,
單一版本(One Version)則更進一步,是指在任意時間,代碼庫內的每一份組件、每一個依賴只有一個版本,
-
對內部庫而言,這意味著使用主干開發(見下),并且必須在主干 HEAD 上依賴,這是一個非常強的約束——這意味著除了終端制品,任何一個內部被依賴的庫都不能通過分支發布,而必須保持自己在單倉的主干上一直是發布狀態,
-
對外部依賴而言,同一個第三方庫在單倉中永遠只會引入一個版本,
不強制 SSoT/One Version 的版本控制策略往往通過制品/版本分支發布,這意味著在整個依賴關系圖之上還有一個版本的維度,這也是在開源社區/SDK 發布商通常采用的策略,其根本原因在于并非完全掌握下游用戶的情況,這樣除了導致較高的維護成本,還會導致依賴關系難以滿足(Dependency hell, 依賴地獄),
主干開發 Trunk-based development
基于版本控制的協作模式一般分兩種:
-
分支開發:每個分支對應一個功能,開發者在這個分支上開發,直到最后完成功能后合并回主干,這是現在小倉下的主流模式,
-
主干開發:每個分支只對應一個簡單的修改,每個開發者在分支上完成修改后經過 CR 盡快合入主干,這是在單倉下推薦的開發模式,
兩者的主要區別在于分支存活時間:保持主干始終健康,將所有的 commits 盡快小批量合入的是主干開發;以 feature 為單位,當 feature 完成之后再重新合入的是分支開發,
雖然聽上去差異微小,但從分支開發遷移到主干開發對研發模式的影響深遠,請務必確保您的團隊深入理解前置需求(挑戰)和長期影響(益處),下文將分別闡述,
Case: API Deprecation
考慮以下常見場景:一個公共庫的作者如何 deprecate 舊的 API 并提供新的 API 作為替代品?
小倉場景:這個公共庫按版本分支或按制品發布,API 可以在下一個版本直接更新,并強制 API 呼叫方對 API 進行更新,這樣的好處是 API 提供方的責任較簡單,壞處是每一個 API 呼叫方都需要自行更新代碼,并且 API 提供方無法保證自己的新版 API 已經被使用,
單倉/主干場景:公共庫只提供原始碼依賴,并不按版本發布,這樣,我們需要保證公共庫在主干上始終是正確的,API deprecation 時,API 提供方可以查找所有 API 呼叫方,并且發動大規模自動修改,原子性地將所有的舊 API 呼叫更新至新的 API,這樣的好處是 API 呼叫方的責任更簡單,但是這種修改只有在能夠查找 API 的所有用戶、并且整個持續集成水平較高時才有可能,
從上面的例子可見,在單倉/主干的場景下,很多代碼的維護作業可以左移并規模化,減少重復勞動、大規模的工業化,是我們啟用單倉的原動力,
益處
以下的益處基于開發者視角和代碼維護者視角,
再次提醒, 單倉想要達成預期效果需要工具、流程和文化三方面的準備,以及長期大量持續維護,貿然遷移到大倉不但不會帶來收益,反而會導致專案和代碼管理徹底混亂,請確保您對挑戰部分有所準備,
主干開發
主干開發是為了進行持續集成,頻繁地、小批量地構建/測驗是持續集成的關鍵,通過主干開發可以:
-
避免較長的穩定期,使持續集成可以持續運行,均攤每次合入的風險
-
使不同特性的開發成員可以及早地了解其它團隊可能對代碼結構的影響
-
避免合流日必須大量合并沖突的痛苦
-
降低 CR 的難度和成本:主干開發下每個合入主干的提交更小,而 CR 的難度隨提交修改量高于線性增長,因此 CR 的難度均攤更小,另外,強制小批量合入主干的提交可以將 CR 的責任從分支所有者轉移到代碼所有者,這更利于對代碼質量的長期維護,
更容易的代碼復用與分享
在大倉下,所有的代碼會變得更公開透明,這便于我們抽象出共同的需求,建立公共庫與公共框架,也便于我們學習更優秀的代碼,以及尋求常見問題的解決方案,另外,我們對公共代碼的呼叫可以變得更簡單:直接從代碼層面依賴,而非必須使用包管理/制品/跨倉庫的依賴,
例子:查詢 API 的使用方法
需要一個呼叫非常復雜(例如一個有幾百個 fields 的 protobuf)的 API,如何才能正常呼叫?Stackoverflow 里不存在答案,而碼客似乎也不適合提問,難道只能找 API 的原作者詢問用法?
如果在大倉中,可以查找該 API 已有的在生產環境中的呼叫作為極好的參考,相反,在紛亂的小倉,缺少好的代碼檢索工具,很難確定 API 到底在哪里被呼叫,
需要:
-
工具 編譯系統:統一的優雅的編譯系統可以降低代碼的復用的作業量,
-
工具 權限控制
-
工具 代碼瀏覽工具
開放協作的文化
大倉模式下會帶來/誘發合作模式的改變,我們認為現代軟體開發關鍵在于團隊合作,因此一個能夠增加透明度、允許更高層面合作的開發模式能夠促成更透明更親密的合作,更進一步,根據Conway's law,一個所有人的代碼都公開透明、親密協作的組織能夠產生更加公開透明、結構親密協作的產品(注意:這里協作不代表耦合),
在大倉模式和主干開發的模式下, 我們可以更方便地主動去為其他組的專案提交代碼,例如修復缺陷或實作我們所需的新功能,這個組可能是姐妹組,也可能是相隔甚遠的專案組,
我們認為作者對代碼有太強的歸屬感,即有強烈的領地意識,對開放協作文化有害,隱藏代碼或不允許別人觸碰自己撰寫的代碼是反模式,允許別人修改,自己由 CR 做最終決定,是更為開放和有效的合作模式,
例子:公共庫/框架
一個公共庫/框架的誕生往往通過兩種途徑 -- 自頂向下和自下向上,自下向上指通過逐步聚合抽象公共需求和組件,例如一個頂目的開發程序中,基于實踐抽象出自己專案的公共庫;慢慢地,該公共庫與其它專案的公共庫的共同部分會產生相當程度的重疊,我們便將這部分交集進行整理和泛化,升級為更高級別(如產品線)的公共庫,經過更進一步的抽取和整合,最終形成公司級別的公共庫,如果沒有足夠的透明度和協作,這種聚合幾乎是天方夜譚,
需要:
-
流程·代碼評審:開放協作的文化必須要通過嚴格的流程來制約
-
流程·結構管理:需要規范來控制大倉的目錄結構,否則隨意的創建目錄會非常快速增加倉庫熵以致于不可控
原子修改 Atomic Commits
原子修改指我們可以在一個提交中修改在單倉中的多個專案的代碼并同時生效,這對倉庫始終保持在一致狀態至關重要,與此相反,在小倉場景下,我們需要在每個需要修改的倉庫發送提交,這意味著原子性不能保證,即我們無法保證這些提交同時生效,
一個典型的例子是同時修改 API 介面、實作以及所有的 API 呼叫者,如此,我們就可以避免同時維護同一介面的兩個版本,
注意:原子修改也無法解決 C/S 不統一的問題,即對 API 的更改,即使對實作和呼叫的更新和 API 更新一起是原子提交,也無法解決 C/S 面對的介面不一致的問題,因為 C/S 的發布周期不會因為原子提交而實作原子化,
大規模修改 Large Scale Change
由于開發人員可以訪問所有專案,因此我們可以批量地進行大規模的自動化的簡單修改,例如 API 更新、自動清理長期未被參考代碼等,以維護代碼質量,這種由工具團隊發起的大規模的修改在大倉開發模式下十分常見,
需要:
-
工具·持續集成:只有保證足夠高質量和覆寫率的自動化測驗、足夠優秀的持續集成才能讓我們有足夠的工程信心進行大規模的修改,
-
工具·大規模修改:為了能自動化進行大規模修改,需要能夠自動按規則大范圍修改代碼、自動格式化代碼的工具,
代碼治理簡單
代碼單一的存放位置使得代碼作業更加簡單,
例子:代碼掃描
我們希望建立知識庫,因此希望掃描所有相關倉庫內的 markdown 工程檔案,小倉場景下,我們需要維護所有相關倉庫的串列,克隆并掃描每個倉庫,單倉場景下,只掃描一個倉庫即可,
依賴管理簡單
在大倉里,第二方和第三方依賴可以直接依原始碼匯入,并且只有一個版本,這樣可以極大地簡化我們對依賴的治理,而且也可以減少已經標準化的專案對引入第三方依賴的成本,因為這些引入的依賴需要標準化,如 Bazel 化,
注意:第三方依賴的治理本身是需要大量作業的,詳見[挑戰·依賴管理],
挑戰
以下挑戰基于單倉維護者視角,
這些挑戰是我們在以往大倉開發/大倉遷移程序中真實踩過的坑,而非臆想,我們鼓勵所有考慮遷移單倉的團隊在遷移前認真檢查自己是否對以下挑戰有所了解并做好準備,另外請注意,以下尚未窮舉所有挑戰,您需要隨時準備迎接新的挑戰,
單倉不是銀彈,事實上,單倉/主干開發不應該做為一個孤立的工程實踐,而更應該被視為一個工程實踐的放大器 -- 除了固有的挑戰,它可以實作工業化,放大其它的流程/工具的收益;但是如果沒有足夠有效的工具和嚴格的流程及實踐控制,大倉幾乎就是災難的代名詞,下文中“單倉所需的工程實踐”并非嚴格的限制,但這正是陷阱所在 -- 不滿足這些前置的單倉還不如小倉,小倉雖然也需要這些實踐,但是至少可能不會有這么強的擴散效應,如果妥協,即在單倉內完全采取和小倉一樣的嚴格隔離專案并采取分支發布的策略,則遷移單倉的意義基本被消解,
我們將挑戰分為兩類:
-
單倉:由于多個專案在同個倉庫中所帶來的治理挑戰
-
大倉:由于倉庫的體量導致的可擴容性挑戰
所有大倉引起的挑戰在下文有特殊標明,如果您的單倉規模較小可暫時忽略,但請做好倉庫規模增長的準備,
權限控制
代碼遷移到單倉會引入對權限控制的新要求,
提交權限
目前我們的提交權限以 Git 倉庫為最低顆粒度,在單倉中,倉庫級別的權限控制不足以支持多個團隊、多個專案在單倉內作業,因此“專案”級別的權限控制是必須的 -- 在單倉場景下,這需要目錄作為最小顆粒的權限控制,
同時,我們不建議照搬小倉的提交權限機制,即“只有目錄的 owner 才能在該目錄下提交”,我們認為提交權限需要在 CR 層面解決,即并非“只有有權限的人才能提交”,而是“只有有權限的人作為作者,或者作為評審者同意提交,才能提交”,
我們的實踐:
引入類似于Chromium 的 OWNERS機制,機制的關鍵在于:
-
OWNERS,而非 commit 的作者,為代碼質量負責,
-
OWNERS 機制需要具有高度的靈活性,默認,OWNERS 的權限為所在目錄樹,但是 OWNERS 也可以為檔案(per-file)級別提供顆粒度,
-
OWNERS 的定義不應該在倉庫根目錄,而應在所在目錄,并且,OWNERS 檔案應該是可繼承的,例如,/foo/bar/...的 OWNERS 應該是:
我們與此同時將設立一個工具以自動為 commit 自動分配 reviewer,因為在大倉下尋找正確的 reviewer 會相對更困難,
可見權限
一個不太常見的、與大倉的設計目的相反的需求是隱藏代碼,有一些代碼是必須需要維持私密性的,例如與權限相關的代碼或者具體高商業價值的代碼,所以這些代碼需要只對一部分人可見,并需依賴大倉其它代碼進行編譯,這里有幾個選項:
-
在大倉內提供更細顆粒度(如目錄級別)的可見性控制,需要代碼托管方的支持,
-
私密代碼放入小倉中,以小倉的權限控制來保證保密性,大倉保持所有代碼的可見性,注意:這意味著可能有小倉向大倉的依賴,可能需要版本控制工具的支持對大倉的依賴,
我們的實踐:暫時未遇到,視情況解決,
清除提交
一個更加少見的需求是清除一個提交,例如,一個提交泄露了高商業價值資訊,需要及時清除,如何從倉庫歷史里以及從開發者本地的克隆里及時清除是一件困難的事情,在小倉場景下,我們可以簡單地將提交所在倉庫歸檔為私有,將該提交之前的所有歷史復制到另一個新的倉庫,將新的倉庫公開并洗掉舊的倉庫,在單倉場景下,其影響面會比原來大,
我們的實踐:暫時未遇到,視情況解決,
巨大代碼存盤空間
大倉需要的存盤空間可能會非常大,以 Google 為例,2015 年時已經有 20 億行代碼,倉庫超過 86T,這個大小對版本控制造成了非常大的挑戰,尤其是默認 Git 會下載整個倉庫,這對代碼托管方、開發、代碼分析方(如 CI/資料分析/...)等原本可以單機承載單倉容量的系統都會造成更大的壓力,
我們的實踐:我們與代碼托管方,即工蜂,合作成立了代碼大倉研效聯合專案組,以解決擴容問題,
可擴容性
(該部分為大倉引起的挑戰,如果您的單倉規模較小可暫時忽略,但請做好倉庫規模增長的準備)
可擴容性的核心挑戰在于,如果工具是以倉庫為操作單元,則其隨開發人員數量的增長往往不是線性的,而是以不低于二次方增長的,舉例, 考慮每次持續集成都編譯整個倉庫,則如果人員增長 10 倍,可以粗略假設倉庫大小增長 10 倍,提交頻率增長 10 倍,則整個編譯量會增加 100 倍,
我們認為所有的所有開發工具和流程都應該優化,使之隨倉庫的規模上升線性增長,如果無法保證,請保證您的單倉較小,
版本控制
版本控制工具將主要面對兩個挑戰:
-
代碼存盤空間大
原生 Git 對倉庫的最小操作單位是倉庫,即不支持部分檢出,這有其設計目的和歷史原因,因此原生 Git 并不適合大型倉庫,為了緩解這個問題,Git 本身做了很多嘗試,如支持 Git-Lfs, Git Submodule, Git sparse checkout, Git partial clone 等,但是更像是在打補丁,
-
主干進展迅速
大倉乃至單倉的提交非常頻繁,主干進展很快,因此需要頻繁進行 rebase,這對 rebase 的性能有所要求,并且可能需要您提交鎖的設計,
如果使用 Git 作為版本控制工具,在遷移到大倉之前,請確保您對倉庫大小有所控制,包括:
-
使用 Git-LFS,盡可能地把非代碼的大檔案,如資源,托管到 LFS,
-
定期歸檔 Git 歷史,保持.git 檔案大小可控,
-
強制采取 Squash and merge 方式合并,保持主干簡潔,
我們的實踐:我們對 Git 進行了改造,開發了基于懶加載的檢出方案,基于前文所提到的 Code access API,我們實作了對檔案的按需檢出,在初次 clone 時,所有的檔案都只有元資料被下載,只有當需要獲取檔案內容時才會真正下載檔案,
構建系統
我們強烈推薦在單倉使用統一的構建系統,至少,每門語言應該使用統一的構建系統,否則,您可能使用的不是一個單倉,而是一個將眾多小倉放在一起的空間,
我們認為一個適合大倉的構建系統應該滿足以下要求:
-
構建可以是區域的,整個大倉應該作為一個方便彼此之間的依賴,但是在構建時應該允許只編譯當前感興趣的模塊,
-
構建可以是增量的,有限的修改應該只觸發有限的重建,并且可以保證正確性,
-
構建的配置應該是顯式的、代碼化的,理想狀態下, 一個 repo + branch + commit + build target 即可以完整地、唯一地確定編譯產物,這對 CI/CD 非常重要,即 CI 的正確性可以保證制品的正確性,
-
依賴關系應該是顯式的,包括傳遞依賴也應該是顯式的,這樣可以方便我們對系統模塊化,并且更好地管理依賴,包括第一方以及第三方依賴,
一個常見的錯誤是,把構建速度作為選擇構建系統的唯一指標,另外,我們推薦統一每個語言的編譯器以及對應版本,
我們的實踐:我們采用Bazel作為唯一的構建系統,Bazel 有以下優點:
-
Bazel 的編譯是區域的,
-
Bazel 的編譯是增量的,并且提供了分布式中間結果快取以及分布式中間結果構建介面,搭建分布式快取和分布式構建集群對 Bazel 的編譯速度提升效果非常明顯,
-
Bazel 的編譯宣告是顯式的,并且所有的依賴關系也是必須的顯式的,
-
Bazel 是跨語言的:Bazel 的核心是一個構建引擎,所有的語言支持都是以 DSL 的擴展實作的,
如果要建立單倉,推薦考慮 Bazel 作為構建系統,但是請注意:
-
Bazel 本身對構建系統需要有較深理解,我們推薦在您的組織內培養一定數量的專家對構建進行維護,
-
請不要使用 Bazel 4.0 之前的版本,Bazel 4.0 是第一個 LTS 版本,在此之前的版本 Bug 甚多,另外請盡量只使用 LTS 的版本,
(大倉體量下的)持續集成
一個能夠支持大倉體量的、可伸縮的持續集成系統是高度不平凡的,在設計持續集成系統,請確保已考慮以下可能需要解決的問題:
-
持續集成的自動測驗應該測驗哪些目標
a.全量測驗:樸素地編譯整個大倉里的所有專案、執行所有測驗可能并不是一個可行的方案,因為耗時可能過長,
b.人工精準測驗:人為定義一個“專案”,當專案內的檔案被修改時,執行所有專案相關的測驗,但是該策略可能難以保證所有被影響的目標都會被測驗到,尤其當修改的代碼是公共庫時,
c.精準測驗:從構建系統出發,計算被修改的檔案所可能影響到的所有測驗,但是這需要構建系統的支持(這也是為什么我們推薦 Bazel 的原因之一),另外,大型測驗,如需要搭起多個服務的系統測驗,依然需要一個不基于依賴關系的測驗策略,即需要人工精準測驗,
-
自動構建/測驗的體量可能超過單機承載量,即使已經采取了精準測驗,您可能需要將測驗任務分片,
-
持續集成的構建機需要快速拉取代碼進行構建和測驗,請注意在大倉的體量巨大時,構建機可能每次構建都重新下載整個倉庫的代碼可能并不可行,您可能需要在構建機上快取代碼或者采取其它的裁剪策略保證您的持續集成不會因為下載代碼花費過長時間,
-
您可能需要嚴格執行持續集成的“主干永遠是綠的”政策,即,主干上持續集成變紅/變黑之后,受影響的團隊需要立刻修復問題,或者,無法快速修復時,還原相關修改,
a.您可能希望通過"PreMR 100%成功"保證主干永遠是綠的,遺憾的是,這在大倉中無法做到,由于主干進展很快,我們在執行 PreMR 集成測驗時的主干 HEAD 與實際合入時的 HEAD 可能相差甚遠,這可能導致中間會由于被依賴檔案的修改而發生合入之后的測驗失敗,因此您始終需要 PostMR,且 PostMR 需要更大范圍的測驗,因為這種情況只有在 PostMR 的自動化測驗才能發現,
b.您需要對主干上“失敗肇因識別(culprit finding)”準備策略,這是指,PostMR 由 commit A 失敗的測驗(記為 T)未必是由 A 導致的,如果 T 上次測驗通過的 commit 是 B,則所有在(B, A]之間的所有 commit 都可能是肇因,您可能需要對所有測驗目標一段時間持續集成執行歷史的記錄,
-
并發的執行自動測驗的請求可能超過構建機的數量,您可能需要機器鎖,或者一個更好的構建佇列調度器,當 PreMR 和 PostMR 同時在排隊時,您可能會需要給 PreMR 更高的優先度以不阻礙開發人員的作業,
我們的實踐:我們設計了新的持續集成服務,
質量保證所必需的工程實踐
主干開發
注意從分支開發到主干開發需要大量的學習成本,不可能一蹴而就,未實操過主干開發的開發者很可能難以想像如何采用這種方式作業,我們推薦進行大量的培訓,甚至類似于導師制,來培養團隊主干開發的習慣和能力,我們強烈不建議使用考核等行政手段作為唯一的推進手段,因為向主干開發的切換并非只是意愿問題,
為達成主干開發,除了開發人員的培訓,您需要在單倉中以下工具/實踐:
-
小批量開發,
-
開關系統,當一個特性尚未開發完成,您需要在主干中通過開關將該特性屏蔽,使用戶和其它特性完全不受該特性影響,開關系統分為兩類:編譯時開關和運行時開關, 區分在于開關可以判斷狀態的階段,我們推薦:
-
通過編譯時開關進行主干開發協作;
-
通過運行時開關進行上線發布以及 A/B 實驗,這并不局限于大倉,
-
-
高優先度的代碼評審:相比于開發,CR 的優先應該更高,保持 CR 流程的暢通是主干開發的必要前置,
-
保持主干健康,當主干上出現了構建/測驗失敗,需要開發者停止當前作業并立即修復 CI 問題,
-
持續集成,見下,
(單倉中的)持續集成
主干開發和持續集成是相輔相成的關系,盡快往主干的提交使得持續集成可以更細地觸發,而持續集成是使主干代碼始終保持在可發布狀態的主要保證,
-
在單倉中,請確保測驗的質量及覆寫率足夠支撐持續集成,持續測驗是持續集成的主體,如果沒有足夠的測驗,持續集成能起到的作用非常有限,
-
持續集成應該標準化,您不會希望看到單倉內的每個專案都有獨立的幾十條流水線,整個單倉有幾萬條流水線,這樣無法達到持續集成所希望達成的質量保證,
-
持續集成需要盡快得到結果,保持持續集成的暢通是主干開發的另一必要前置,
我們的實踐:
-
我們要求測驗質量和覆寫率必須達標,我們將測驗的質量和覆寫率置入 EPC 指標,
-
我們對流水線進行集中治理和標準化,
代碼評審
代碼評審在大倉+主干開發的模式下非常重要,因為大倉的代碼的合作程度更高,依賴關系會更加復雜,在大倉內的壞提交可能會比原來在小倉內有數百倍計甚至更高的破壞性,在維護左移與加強協作的情況下,代碼的維護者很可能不是代碼變更的作者,而是代碼變更的評審者,如果沒有足夠良好的代碼評審文化和實踐,如果代碼評審形同虛設,就無法通過代碼評審來保證代碼變更質量,則您可能需要考慮先培養代碼評審的文化,
高頻次的細致的代碼評審對代碼評審工具有更高的要求,
-
代碼評審工具需要和作業流程以及其它工具,如在線 IDE、靜態檢查、持續集成和持續發布等等,
-
代碼評審的記錄是寶貴的工程資料和技識訓累,需要永久保留并且需要便于審閱回顧,
-
代碼評審的流程需要清晰簡潔,代碼評審頁面應該直觀、回應速度快且無 bug,否則,開發人員可能會在被評審要求反復改代碼之時遷怒于代碼評審工具,
我們的實踐:我們采取了嚴格的 Code Review 的政策:
-
創立了 PCG 工程技能認證體系(https://iwiki.woa.com/space/SkillsCertification), 引入了 Readability 機制,
-
我們投入了大量精力培養工程師的代碼評審文化,并且提供了大量的 Code Review 的培訓,
-
Code Review 的質與量作為考核標準,
結構管理
大倉的結構管理需要慎重設計 ,隨著開發人員的增加和倉庫規模的增長,需要考慮如何將維持大倉目錄結構,使得各團隊在合作的同時又不失結構,一個好的大倉的目錄結構應該是易于在未知情況下定位專案所在目錄的,并且比較穩定,
我們推薦在設計結構目錄時考慮以下問題:
-
考慮不同語言的代碼命名規范、存放慣例以及定位原則,假如,C++路徑與引入完全一致,而與此相反,Java 沒有類似的強制規定,但是慣例以 com.tencent...為 package 前綴,它們應該被放在何處?
-
API 的放置,
-
第二方、第三方依賴的放置,
-
是否提供每個專案的目錄結構的設計準則,是否提供腳手架,
從經驗上來看,與其說設計結構目錄是設計一個最好的,不如說是設計一個最不壞的,
依賴管理
正向依賴:需要認真治理所有的第三方依賴,在大倉里如果不進行第三方依賴的治理,大倉/主干開發的優勢就會慢慢消散,第三方依賴主要有兩個治理方向:
-
依賴可信:指所有的第三方源應該是可靠的,當前,我們對外部依賴并沒有明確的限制,內部倉庫會從 Github 甚至其它低可信源不受限地添加依賴,由于缺乏足夠的代碼審查,且匯入眾多且難以追蹤,當前我們缺乏工具和流程解決第三方依賴的安全問題,如對高危漏洞的修復以及對外部源投毒的防御等,另外,這導致我們無法審查外部代碼引入的 LICENCE 是否合規,
-
依賴發散:指一個第三方庫可能有同時有多個版本,或者一個版本的多個拷貝,或者一個版本的不同生成產物,在生產環境中使用,依賴發散可能會導致依賴地獄:指通過版本號來進行包管理時,由于依賴關系過于復雜,導致能夠滿足所有包之間依賴關系的版本配置不存在,
一個理想的單倉是自包含的,即,所有的外部依賴都以原始碼的形式被引入了單倉,單倉自身能夠完成對所有專案的構建而不需要構建時下載外部代碼/制品,與此同時,您可能需要思考第二方庫在大倉中的位置,
反向依賴:大倉往往難以被外部倉庫原始碼依賴,如果大倉內的庫需要被反向依賴,請確保您有應對策略,通常有兩個方案:
-
允許外部倉庫以輕量的方式依賴大倉,如制品依賴、或者按需依賴等;
-
將被依賴的代碼同步到一個小倉中,以供外部依賴,
推薦實踐及工具
以下并非硬性要求,但是我們仍然積極推薦您準備以下實踐及工具,另外請注意,我們推薦您制定并維護嚴格的工程規范,例如命名、分支拉取、發布策略、比騰訊代碼規范更詳細的語言規范,
靜態檢查
靜態檢查 (static analysis),包括基于規則的代碼掃描、基于抽象語法樹的靜態分析和基于編譯插樁的分析工具,靜態檢查可以提升代碼質量,并減少代碼評審的作業量,
靜態檢查的范圍可以比現有代碼掃描廣泛得多,比如:
-
API deprecation and auto replacement
-
Dead code detection and auto removal
-
Error prone code suggestion
我們追求的目標是:將盡可能多的CR 意見轉化為靜態檢查,靜態檢查一般在請求 MR 時觸發,若有可能,盡可能將靜態檢查左移,在開發/編譯時觸發,
我們的實踐:
我們將在持續集成階段執行標準化的靜態檢查,提高靜態檢查報告的強制力,并在代碼評審階段要求開發人員做出反應,同時,我們將提供 IDE 插件以實作靜態檢查左移,
輕量的代碼瀏覽/檢索
當我們擁有一個大型倉庫,有一個良好的代碼瀏覽器可以減少代碼閱讀的成本,可以更好地達到代碼的復用和分享,我們希望擁有一個代碼瀏覽器,并做到:
-
輕量, 理想情況下零設定成本;
-
完整, 能夠看到整個大倉的代碼以及歷史版本的代碼,以及 blame 資訊;
-
有用, 具有語意索引,能夠看到所有的參考與被參考,以及實作跳轉;
-
可以檢索,
但是,現有的瀏覽代碼的方案都不完美:
-
工蜂尚缺少語法索引
-
IDE 等本地閱讀工具需要下載并進行本地索引
我們的實踐:我們希望有一個帶有語意索引的代碼瀏覽器,類似于https://cs.opensource.google,
Web IDE
Web IDE 與代碼瀏覽器一起可以降低代碼修改和提交的成本,對于一些小的修改,例如糾正 typo,輕量的工具可以極大地增加提交者進行修改的意愿,
大遷移
遷移總是困難的, 尤其是與開發者日常作業息息相關的大規模的倉庫遷移,當您要制定將眾多小倉遷移至單倉的計劃,請確保您考慮過以下核心問題:
1.您是否需要提供緊急回遷方案?
提供回遷方案會極大地增加遷移復雜度,如果您的單倉規模較可控(如 < 500 開發),可以考慮通過一些其它手段增加遷移成功的成功率,而不提供回遷方案,
2.您準備先將倉庫遷移到單倉再完成治理(即“挑戰”中所提到的必須實踐),還是先治理再遷移單倉,抑或不進行治理?
同時,請確保您考慮過以下風險:
-
專案代碼所在目錄會因遷移改變,從而可能導致代碼需要改變,如,從@fiber://src/遷移到@depot://devtools/fiber,相對的 package 可能會發生變化,以下解決方案均非完美:
a.直接平移代碼:可能會導致代碼不符合組織規范,或導致無法編譯/運行時錯誤
b.對代碼進行自動化修改(如 copybara):可能導致無法編譯/運行時錯誤
c.代碼所有人自行維護:相當于把風險委任給代碼所有人,但是若要長期保持遷出與遷入倉的同步,需要大量的努力,
-
倉庫間的依賴關系會極大增加復雜度,
a.一個簡單的例子:小倉 A 依賴小倉 B,兩者均需要遷移,則您需要 :
i.將小倉 A 遷移至大倉 A,仍然依賴小倉 B,歸檔小倉 A
ii.將小倉 B 復制到大倉 B
iii.將大倉 A 的依賴轉移至大倉 B
iiii.此時方可歸檔小倉 B
換言之,被依賴的倉庫需要后遷移,這意味著,如果是一次性遷移多個小倉,您需要整理所有的依賴關系圖,拓撲 排序使入度為 0(即不被依賴)的倉庫先遷移,以此逐階段遷移,
b.如果一個小倉被單倉外的倉庫依賴,您需要長期保證這個小倉依然可以存在并被依賴
-
開發團隊可能需要準備一個從分支開發到主干開發的全程序,并且確保主干開發有足夠的測驗和 CI,在 CI 尚未準備好且開發團隊經驗尚淺的情況下,風隙訓很高,
-
專案的流水線需要遷移,將小倉的流水線遷移到大倉,需要保證大倉上遷移過去的流水線可以作業,
-
其它工具也同流水線一樣,可能需要適配,
我們的實踐:相對效率,我們的遷移方案更強調安全性,
-
提供緊急回遷方案
-
先將小倉遷移至大倉之后再進行標準化
-
預先建立大倉內的開發規范,強制所有遷入大倉的專案在標準化后符合開發規范
結語
十年磨一劍,霜刃未曾拭,感謝 PCG 兩年多的效能改革為如今的單倉提供了可能,
路漫漫其修遠兮,大遷移已經啟航,我們會前進,
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/290709.html
標籤:其他
