B 站基於大倉庫的 CI/CD 及微服務實踐
作者簡介
毛劍 B站 平臺架構師&EP負責人
Agenda
我是在大概2015年的時候加入B站,之前是負責整個B站的後端,大概在2018年的時候轉架構師,監管一個EP的團隊。其實之前雖然沒有做EP的一些事情,但是在轉團隊的時候,其實也進行了這樣的實踐。自己也有一些想法,如何快速交付、如何監管程式碼質量、如何測試等等。
我的分享會分為三大塊, 首先什麼是大倉庫。
這個東西是大概一兩年前,我當時聽別人一個分享,介紹的是他們把所有的程式碼放在倉庫裡面如何工作。後來我也看到一些文章包括一些論文,有一篇文章非常經典,叫做谷歌為什麼把數十億程式碼放在一個倉庫,包括陸陸續續我發現有一些國外在矽谷創業的公司,都在做這方面的實戰,我就在思考包括自己的一些痛點,後來就走到這個方向。
第二塊在實施工具鏈上的一些建設。
第三個如何結合大倉庫,做一些CI/CD。
1. Mono-Repo
首先介紹一下背景,2015年加入的時候,我們當時基於一個叫織夢(音),所以我們要做轉型,包括引發一些開發人員在這個專案更好地迭代,因為當時的程式碼就是B站KFC全家桶,所有的程式碼都在裡面。
後來我們做微服務演進的時候,發現我們的基礎庫經常跟著我們的業務發展或者需求不斷優化或者疊加。老實話來說,我們一開始做基礎庫的時候,或者我們的抽象不是很合理,或者破壞了一些原則等等。其實基礎庫都會頻繁升級。
我印象比較深,之前是靠管理手段推技術庫,包括所有的分倉庫的基礎庫的程式碼推進也好,其實都是靠管理手段。比如我在群裡面,幾月幾號必須在幾點鐘把基礎庫全部升級,這次變更可能是破壞性的,意味著我們的業務程式碼可能也會跟著被破壞,這個其實是比較糟糕的體驗。包括我自己,每天在群裡面靠吼,這個是非常低效的。
我當時管了大概有五六十個人,其實我每個同事的一些程式碼我都會關注。雖然有口號,但是流程是不統一的,基本也是靠每個組長去要求團隊,但是整體不夠自動化。
第三點我們移動端,小bug非常多。我們大量測試的工作,積壓在發版前,所以說大家其實一開始都是在憋大招,憋到最後要發版的時候,在上車前大家全部提測,這也是非常糟糕的。因為你沒有一個持續的,測試的工作也是無法前進的,就導致交付的速度低下。
最後一個問題版本管理非常複雜,包括後端相對來說還好,因為基礎庫相對統一以後,這個版本的1.0版本號,另外一個基礎庫的2.0非常複雜。我當時看的時候,一個超大的圖,各種版本號,元件是不統一的,非常混亂。
我們有一個B站的評論模組,一個評論元件,可能因為平臺他在做這個需求的時候,沒有考慮的那麼周全,我一個業務使用的時候,發現你功能不齊,我是不是可以程式碼改一改,這種情況非常常見。這些程式碼分散在各個倉庫裡面,所以這種拷貝程式碼的情況非常多。
所以我們交付到安卓渠道的時候,因為只有一個包,你要把所有程式碼合成打一個包,所以他的量是非常大的。這些都是我們在這兩三年的過程中遇到的問題。所以我們後來引入了Mono-Repository,只有單一產品的倉庫包含了多個基礎庫,應用等等。我們是把所有的程式碼託管到叫平臺的一個庫,我們取了一個名字叫做Kratos。
2. Mono-Repository Toolchain
Mono-Repository 優勢
說一下為什麼轉型到大倉庫,有哪些優勢?我們一一講一下。
首先第一個覺得比較爽的,有一個一致的版本。以前我們基礎庫可能1.0、2.0,非常煩,每次不同的業務要釋出的時候,都要依賴不同的版本號,這都是比較麻煩的。使用大倉以後,第一個體驗就是一致的版本,我們基礎庫的升級,版本是通透透明可見的,版本是一致的。
間接的我們的程式碼始終保持先進性。為什麼這麼說?比方說我的基礎庫從1.0迭代到2.0,如果說只能靠行政手段去吼,我的架包到2.0,你去申請一下。其實對業務方來說,他其實是沒有動力的。一般都會有所謂的變更管理的思路,我就不改,但是你必須要保證我的相容性。
第二個就是極致的程式碼複用,程式碼拷來拷去,非常混亂。我們現在合完大倉以後,如果一個團隊想要依賴另外一個團隊程式碼,其實就是跨目錄的依賴,沒有所謂的用某個jar包幾點零,這個是比較麻煩的。
如果我有一個好多的倉庫,對於一個新人來說,就像一個大城市一樣,他進去以後找不到他在哪裡。
目前我們一個新同學入職以後,只要開一個倉庫的許可權,他所有的東西都可以瀏覽到,這個其實是非常簡單的。我們另外一些同事,可能要開發基礎功能的時候,也會傾向先在大倉庫裡面找有沒有已經存在的功能。
第三個點簡單的依賴管理,怎麼講?我們構建系統,可以輕鬆地在目錄之間掛上程式碼,整個依賴非常簡單。這裡面還有些特殊,因為我們可能還會引用一些第三方的開源庫,我們現在也是在嘗試像go 1.1的功能,目前我們是把第三方的庫,使用版本號下載下來,再提交到大倉庫裡面。
還有一個優勢會簡單的依賴管理,我以前也經常遇到過移動端的同事。比如說A這個庫會依賴B和C,B和C再依賴D。我們發現有可能出現依賴D的庫會有兩個版本,這兩個版本可能是不相容的,包括服務端其實也遇到過。比方說卡夫卡有一次升級,他程式碼就做了一些破壞性的變更。這個時候我們要升級的話,其實就比較難推動的了,有些人用的比較老,有些人用的比較新,類似這樣的。
當然對於我們自己的組建庫來說也是一樣的,有些人用的新的,有些人用的老的,引入第三方的庫,在編的時候其實就沒有辦法處理了,這個時候就要做一些取捨。我們移動端因為分倉庫開發了,每個業務線在自己的倉庫下開發,一開始使用的平臺公共組建,有些人是1.0,有些人是1.1,這個時候在合包的時候也容易出錯。
最終合入總包的時候,也會導致一些庫的變更,又要重新測,這也是得不償失的。
我們發現在其他語言或者是,比方說以go為例子,為什麼會比較推崇靜態連線方式?因為你相當於交付二進位制,其實我覺得思路上是和大倉庫的思路一致的,就是我把所有東西自我包含,我就可以很簡單地交付出去。所以他是可以簡單的依賴管理。
我們看一下還有一個他有一個原子化的程式碼更改,我們開發人員在一致的操作中,原子化的操作中,可以對程式碼庫裡的數百甚至數千的檔案進行重大更改。
比如我們包的名字處理不是特別好,假如我是在分開的倉庫中,我要把包名改一下,我們會把這一次變更為什麼這樣做,涉及的影響會是什麼,由誰破壞他的,誰就要修復他,我們用這種思路引導所有人配合技術部的迭代和升級。
還有更好的支撐大規模程式碼庫的重構和更新,我在單一的程式碼庫裡面可以捕獲所有的依賴關係,我可以大膽地刪除API。我們在一個目錄遷移的時候,就做了一個計劃。
首先叫大倉庫一鍵包裝,把包名的路徑改了,第二個保留老的程式碼一個月。有些人因為是分支開發,有些老的主幹時間特別久,所以我們把老的保留時間長一點,一個月。
一個月以後我們會把老的程式碼標記為過期,合入的時候會告訴他這是過期的不可以合入。這樣的話就可以完成基礎庫的迭代和更新。
靈活的團隊界限以及程式碼歸屬權。首先程式碼庫裡面,一般會有業務的名字或者部門的名字,我們其實要使用別人的API非常簡單,我只要找到他的目錄在哪裡,就能看到他所有的API,我就可以非常簡單地使用他的API。
包括有一些業務,因為組織結構在調整,團隊經常變更,其實只要做目錄的操作,就可以從一個部門劃到另外一個部門。
團隊之間的更好合作,由於程式碼庫的結構,以前開發人員需要決定程式碼庫的邊界,比如我是開發分享組界,你是開發評論組界。現在不需要這種共享式的開發,因為現在非常方便了,我們只要把目錄調整一下,各自的歸屬就可以變更了,所以說也是非常好合作的。
另外最大化的程式碼透明度,以及自然而然的按團隊劃分名稱空間。這個變成了你API的路徑或者名稱空間。比如我們內部主要使用GRPC,那我們API路徑統一為什麼?比如他是在一個應用,他是一個會員服務,他的API對應的是GRPC還是什麼,他是一個V1版本。
我們其實已經很少去使用文件,我記得以前對接的時候就是別人問我要請求什麼介面,然後我就各種找,找到我自己的文件分給他,然後這個文件可能過段時間還忘記更新了,這個事件經常發生,最終可能有一些程式碼生成器之類的,保證我們的文件更新。
這種工作模式,會導致一個開發人員頻繁的上下樓切換,就是一種找文件、一種找程式碼。我們現在其實就是文件接程式碼,他也可以非常清楚地告訴你我這個介面是幹什麼的,裡面有什麼引數,都描述的非常清楚。也不會到處問了,基於我想要的東西,比如我想獲得VIP的狀態,我可能在整條程式碼倉庫裡面搜VIP這個檔案我可能就能找到,也不用到處找文字了。
2. Mono-Repository Toolchain
Mono-Repository 問題
我們使用大倉庫以後,雖然解決了一些問題,但是也帶來一些問題。
首先非常複雜的就是構建系統,早期的時候體驗非常糟糕的,因為我那個時候是比較急切地引入這麼一個思路,但是我們團隊大概50多個人。在整體協作上其實非常困難,為什麼?經常會出現某一個開發同學被預設到主幹了,我們雖然會跟他說你提交程式碼的時候或者合併到主幹的時候一定要點主幹,他這個就是編輯全部。
這個因為需要大量時間,我整個程式碼規模非常大,可能要花非常長的時間,就導致開發人員不願意做這個事情。後來就想能不能整合到一個Gitlab的CI/CD裡面,我們後面做了一些改進,我們先沒有用那個Gitlab的CI/CD,而是我們基於Gitlab hook的API,我們做了一個事件的觸發。我們當時的指令叫加默值,就會觸發一個HOOK,這個hook會做什麼?我們做了幾件事情。
首先我們用Gitlab的那個包,能把他的依賴關係整個原檔案解析出來,我就知道他破了哪個包。把這些依賴關係,這些資料存到依賴的一個圖,我們每一次在加默值這個操作觸發以後,就會查這個依賴數,那麼我就知道你哪些被依賴的需要被重新構建,所以我們就做了一個增量變異,我們當時其實是自己實踐的。
後來我們也發現越來越多的公司,包括後來我聽facebook使用一個構建系統,我們就發現其實谷歌內部有一個叫Bazel的一個東西,我們後面就逐漸從自己模改的方式切換到Bazel。
為什麼我會去推進Bazel?我發現各個團隊都有自己的語言,如果基於每一個語言做同樣的事情其實成本會比較高。所以說我們後來發現Bazel其實可以跟語言結合的。
你可以認為在每個目錄裡面有個bulid檔案,這個bulid檔案會把這個目錄描述出來我依賴誰。那麼通過這種方式,我就可以知道全域性的依賴關係,他還有個好處,就是所有的構建方式是統一的,所有語言都支援。你只要實現自己語言的錄入就行了。
還有個好處,我們分析出他的依賴關係以後,其實就可以做一個增量變異。比如說我改了A庫,A其實只有B在使用,我用那種增量變異的話,我只需要變異B,其他是不需要的。第二他有一個工具支援多個語言。
第三就是他可擴充套件,像Bazel為什麼會出來?就是因為谷歌內部也是一個超大倉庫,這是第三點。第四你可以擴充套件他,因為你可以編寫自己的,所以我們目前在IOS上的大倉實踐做的還不錯。
大倉還有一個就是良好的目錄結構和依賴規範,這個看起來很簡單,實際我們在實踐過程中,我自己包括我們團隊都犯了很多錯。
現在可以簡單看一下IOS的倉庫有點類似像這個樣子,他可能有多個包,還有各個基礎庫,還有第三方的依賴庫。
我們現在在go的倉庫裡面也會類似,有三個庫,一個是第三方的庫、基礎庫、APP,APP對後端來說像管理後臺、微服務、閘道器等等一些模組。
當我們寫程式碼的時候,庫與庫之間的依賴關係處理不好的時候,你會發現改一個倉庫,會導致整個倉庫全變,你變異的快取中間結果特別容易失效,就會導致每次都要重疊。
我們為此修復了很長時間,各種各樣的歷史原因,比如發現某一些庫被所有人使用,他可能會被經常破壞,你就要考慮是不是出現一些問題。還有就是CHANGELOG,簡單的依賴環境。
還有大倉另外一些問題,就是超高程式碼質量要求。我們之前犯過一些錯,就是基礎庫一些小的變動,導致所有都受影響。
假設你有一個bug被合流主幹那麼就會有風險,因此我們在大倉庫以後,就更有理由或者要求我們的同學寫更多的自測,包括程式碼規範,這個要求都是非常嚴格的。
同時我們也會利用Bazel跑一個增量的UT。另外如果我們現在是全量的UT開發人員可能都出去玩了,我們也做了一個增量的UT。
另外我們以前這種人肉的REVIEW,這種工具我們不僅僅引入了很多業界經常用的,我們還做了很多定製的開發,把這些庫打到流程裡面,變成一種強制的手段。
剛剛說了我們即使有大量的UT,很多靜態掃描工具,仍然避免不了我們犯錯。所以我們同學非常喜歡寫UT,即使是這樣我們還是會出現一些bug。
我們後來使用一個灰度機制,首先希望我們這次改動能夠儘快合入主幹,但是我不希望他立馬生效,所以我們通過一個Feature Flags。
比如下面有一個Feature gates,結合我們的paas平臺,通過這種方式啟用。當我們這種基礎庫的灰度三四天或者七天我們發現比較穩定的時候,才會把flags去掉。
另外我們需要大量工具鏈的投入,比如我們的Bazel你是不是足夠了解他,還有管理Bazel遠端的快取。
另外程式碼託管,如果你的程式碼倉庫超級大,是不是需要一個儲存。或者如果你是一個超大倉庫,你是不是需要一個IDE,這些都是你供需的投入,都需要時間和人力。另外你test-infra非常複雜的。另外重點強調一下CodeReview,我們很鼓勵溝通的。
CodeReview有幾個好處,我自己感觸比較深的首先他是可以保證程式碼質量的,第二他是人員的備份。我們鼓勵跨團隊,其實目前B站以前是一個部門內是跨組的Review,現在我們有跨部門的Review,其實是一個非常好的人員備份工具。當然任何一個人,其實你的程式碼好多人都讀過,好多人都授信,任何人都可以補上來。
另外就是說他是一個知識共享和教學工具,因為他是一個良性的吐槽別人程式碼寫的不好,因為我去看你的程式碼,我經常會在下面說你這寫的不好,那寫的不好,其實我是有壓力的。
我自己就會說寫的好點。那麼我再看你的程式碼,因為你老噴我,所以我也會噴你,所以這種良性迴圈,會促使這個程式碼越寫越好。另外還有一個非常好的是一個知識共享,可以發給不同的同事分享。
像之前管理職責非常重,我也會看程式碼。因為有些新同學可能在某些點上,他可能寫出了你自己沒想到的一些亮點。另外我們通過CodeReview,強調的是每個人對自己程式碼一定要負責,不僅僅是你要寫出來,你要對他的質量複雜。

後面再講一下CodeOwner,你對整個生命週期負責,沒有運維,沒有測試,所有人都是工程師。首先大倉放棄了對讀許可權,因為大倉基本上大家進去什麼程式碼都能看到,所以他對讀許可權基本是放棄了,但是他是對寫許可權進行了一些微控制,你不是什麼目錄都可以隨意破壞別人程式碼的。我們怎麼做的呢?
我當時想的比較簡單,當時我就想是不是在每個目錄放一個Owner檔案描述一下,後來我才發現像谷歌內部好多專案都已經採用這種機制了。後來我看了一下別的程式碼,他也是加工的檔案。可能有一些細節的差異,我們內部定義了三個角色,一個是Owner,第二誰來負責,第三是誰編寫的。
另外我們怎麼去整合起來,後來我們使用Gitlab的一個to do。我們養成一個習慣,我們進介面裡面,我們先點一下看我今天有什麼工作要做,有點像騰訊的一個產品,不是每天在群裡面吼。
另外就是郵件,這也是溝通的方式,大家在郵件裡面形成很長的對話。當然我們比較緊急的一些通知也會發送企業微信。因為我們剛說了通過一個加默值的指令,gitlab master這種屬於比較緊急的機制,這個也需要有的。
最後再講一下還有一些比較複雜的問題,一個完整的持續整合,我是把他推崇的圖拿出來的。首先我們有提交程式碼、釋出程式碼,會有幾個流程。
我覺得比較好的首先Gitlab的CI/CD特別友好的UI。在我們討論區,會提示你這一次的程式碼變更,我們會提示是這些人,這些資訊都會增加 Gitlab todo裡面。
3. Mono-Repo CI/CD
還有一個非常重要的一點,我見過無數公司做了好多CI/CD的系統,都是脫離開發場景。為什麼這麼說?我們非常鼓勵只在一個平臺做所有的事情,因為你不需要切各種各樣的系統,因為這些都是有成本的,而且會導致一個開發人員不斷切換,特別麻煩。所有的CI/CD流程都不應該被定製,而是說自由發揮。
我一開始不是特別理解為什麼喜歡這種方式,後來想想因為在谷歌內部,所有人都是工程師,都可以使用程式碼。所以我覺得這是非常好的,包括大家有沒有發現一些檔案都是偏宣告式的描述,本質上是希望自我都可以去描述的。
另外我們做的一個hook的外掛,這個外掛解釋每個目錄的一些資訊。比方說這個目錄需要誰來,我們這裡有個小箭頭,可以點贊,在Gitlab上只要點個贊,只要拿到以後我們就知道,那麼這個就被合併到主幹了。
我們在使用CodeOwner的時候也會發現一些問題,首先我們不是技術型的專家,非常難維護。還有他的CodeOwner基本不需要裝什麼東西,我們把所有變異的環境全部做成了容器。所以我們所有語言變異的環境,全部是有映象(音)的。我們後來越來越鼓勵你自己製作映象,我們內部也會有一個私有的,你可以自己去構建你的環境。
我們後來發現GitlabCI/CD有什麼問題呢?第一個是分支親緣性排程,你需要不斷拖程式碼,這些檔案因為都很小,他要發好多次網路請求。還有我們需要更進一步的抽象CI/CD。
後來我們發現其實K8S有一個Prow,目前我們也是逐步往這個方向演進。
這是一個Prow的一個架構,他其實多了一層抽象,除了hook抽象出來以後,他又單獨出了一個模組。目前我們已經跟Prow官方溝通了,所以目前做了很多Gitlab的一些程式碼的植入,這個我們也是持續地跟官方在溝通。時間關係這個圖簡單來說就是分為排程、分為處理任務等等。
我們還有一個非常有意思的話題,我們後來發現他裡面有一個叫Helm的工具。因為UT覆蓋率很高,基礎庫不用說,但是業務程式碼真正做好UT是非常困難的。我們依賴的中介軟體越來越多,其實非常複雜,你要把所有的引擎全部程式碼化,我覺得非常複雜,而且有各種各樣的語言。
所以我們一開始在想自己造一個東西,後來發現有點傻,工作量非常大,是不是可以把他容器化掉。所以我們資料操作層,他依賴的資源,我們也是上容器的。
我們依賴一個物理機,你跑進去資料放進去了,如果你不清理非常麻煩。helm就是幫你管理這些映象,幫助你升級,他也是自描述的檔案表示,他依賴什麼版本,應該在什麼時候被啟動,我覺得這是非常好用的。
我們在合程式碼的時候,因為只有一個主幹,其他都是分支程式碼。在合的時候,首先一定要通過第一個合完以後,進主幹的程式碼要重新跑一遍Pipeline,因為有可能會失敗。只跑一次Pipeline,包括整合測試也是跑一次。這樣的話後來我看了一個風險資料,他以前一天只能合可能幾個或者十幾個,他現在一天可以合幾十個了。
歡迎加入嗶哩嗶哩的工程效率團隊,大家可以掃我。B站工程團隊比較有意思,因為我本來是做業務以及做其他架構師的,我們公司也非常重視工程效率。
本文為毛劍老師在 GOPS 2018 · 上海站的分享,經毛劍老師稽核授權釋出。
如果您想進一步瞭解嗶哩嗶哩工程效率實踐
您有一個好機會!
毛劍老師會在 GOPS 2019 · 深圳站 做 B站高可用實踐之路的分享,敬請期待~
更多 GOPS 2019 · 深圳站精彩內容,點選 閱讀原文