天天看點

C++項目管理

c++ 采用 "separate compilation" (分離式編譯)意思就是說在編譯一個 foo.cpp 時,唯一的對其他依賴代碼的要求就隻是看到它們的頭檔案 (header files),是以,隻要每次編譯時可以確定 foo.cpp 和它 include 的所有header files 都是一緻的就可以了。但是,我們目前并沒有做到這一點,因為,

一個員工不同時候的編譯

不同員工的編譯

不同機器上的編譯

在以上的各種情況下,這些 header 檔案有可能不同或被其他人更改而無法察覺:

linux headers

glibc headers

gcc headers

三方庫 headers

alicpp 意在解決這個問題,因為在 alicpp 環境下編譯時,所有以上檔案,甚至包括編譯器本身,都是 alicpp git repo 裡的檔案,并且這些檔案是隻讀的(永遠不會更改内容)。

C++項目管理

假如 myclass.cpp 依賴了兩個三方庫 a 和 b,它們又都同時依賴第三個三方庫 c,此時我們必須保證 a 和 b 依賴的是同一個版本的 c,而不能是稍有差異的不同的 c,否則會出現難以查詢的 build problem,編譯會通過,但是生成的執行檔案是有問題的。

alicpp 清楚記錄每一套三方庫的依賴關系,精确到版本号,以確定完全避免菱形依賴關系可能帶來的隐患 。

我們有沒有問過自己一個問題,那就是線上運作的時候找到的 .so 是否是我們研發或測試時使用的同一個 .so 檔案?這些 .so 包括:

linux 和 glibc 的,比如 libpthread.so, librt.so

c++ 的,比如 libstdc++.so, libboost_xxx.so

三方庫的,比如 openssl 的 libcrypto.so

二方庫的

對任何這些機載的 .so 檔案的依賴都會造成程式運作的不确定性,因為任何其他部門的人或任何人的錯誤操作都有可能對這些檔案作變動。

前面提到的三方庫菱形依賴問題在連結時依然存在,當兩個三方庫想要連結不同版本的 .so 時,它們在執行時是沖突和危險的。

假如我們用靜态連結的方式連結所有依賴的庫,突然世界變的非常的美好,

我們釋出的時候不再需要準備什麼 package,因為隻有一個執行檔案需要 push 到目标機器

在目标機器上也不需要什麼 ldconfig 或 ld_library_path,不會有 .so 查詢不到的問題

是以也就不會連結到錯誤的 .so

靜态連結的程式因為沒有 got (global offset table) 的間接符号查詢,是以執行速度會快

我們對目标機器的依賴性是以降至最低,隻要是正确的 linux 大版本,就不會出任何問題,所有其他團隊可以更新更換機器上的其他軟體而不受其影響。

當然,全靜态連結也有一些問題,

連結速度慢:下面會提到解決方案

檔案可能很大:其實今天來講 1gb 之内都是沒有問題的

執行時不能多個執行檔案共享一個 copy 的 .so:但假如整個機器給了一個應用,那 .so 和 .a 是使用相同記憶體量的

在一些特殊情況下,不得已必須用 .so,比如動态生成的 .cpp 代碼,oracle 未開源的代碼庫,等等特殊情況

無論如何,大家應該看到全靜态的美,盡可能的用靜态方式連結更多的庫,alicpp 幫助大家準備全靜态的連結指令。

如果我們很在意線上運作時不能連結到錯誤的 .so,我們可以靠生成特殊的 .so 檔案名來解決這個問題,比如:

<code>pangu-trunk-3412562.so</code>

<code>&lt;項目&gt;-&lt;repo&gt;-&lt;revision&gt;.so</code>

此時的 .so 就成為“隻讀檔案”(意思是名字和内容是一一對應的,沒有人可以用同一個名字定義另一個改變了内容的檔案),可以確定連結的絕對正确性。

有了嚴謹的編譯和連結守則,我們就可以把大家統一到一個編譯和連結的标準和流程上來,我們也就可以統一的來解決我們的編譯和連結的速度問題。

alicpp 将緻力于建立一套完整和龐大的編譯系統來優化我們的日常編譯過程 t264,其中會用到,

大規模叢集上的分布式編譯

pre-compiled headers (pch):因為我們完整的操控了所有 header files,我們可以提前編譯好這些頭檔案

cached .o:我們絕大部分編譯工作是和同僚們編譯相同的 .cpp,我們可以把編譯結果記錄在類似于 ccache 的記憶體中

offline compilations: 我們會在淩晨時分提前編譯好常用檔案

alicpp 會嘗試 google 的 gold linker,可以 5 到 10 倍的提高連結速度 。

可以說我所看到的我們目前的 c/c++ 團隊和子產品之間的協作關系是混亂不堪的,因為我們沒有遵守應有的法則。這裡詳細的記錄和解釋了每一個步驟和理由:

我們每一個團隊對其他團隊的依賴性都可以按照進度要求來分成三種情況,

我們對對方的進度要求不高,我們需要的功能目前已經提供了,如果将來有新版本的話,更新了當然好,但是不更新也問題不大,即使更新也是低優先級的工作。典型的例子是對大多數三方庫的要求。

我們必須盡量跟上對方的進度,不然就造成軟體對接的諸多問題或是線上支援的困難,但是我們又擔心跟的太緊會看到對方不必要的新代碼帶來的 bug,此時我們要的是“盡量跟上,但并不要最新版本”。我們很多團隊之間的關系就是這樣的,比如 odps 軟體依賴底層的飛天系統,但是 odps 有自己的穩定性需求,不能對新寫的飛天代碼跟進太快。

我們必須和對方是相同進度,因為我們代碼的依賴性太大,同時雙方又在不斷做調整。我們小團隊之内就是這種情況。假如兩三個小團隊之間也互相依賴的非常緊密,也是處于一體狀态中。

針對以上三種依賴性,我們就可以直接确立代碼庫的結構:

弱耦合的子產品可以在不同的 git repo(svn 庫)裡,比如 alicpp 的三方庫,甚至于一些二方庫,它們可以有自己的 git repo,隻要我們有辦法找到他們的 include 和 lib 就可以和它們對接編譯和連結,我們也可以從容的針對它們的不同版本進行引進,非常長期的做版本更新工作。

強耦合的子產品必須在同一個 git repo(svn 庫)裡開發,編譯速度不是我們分屬不同 git repo 的借口,我們正在解決這個技術問題。代碼權限是人為的分屬不同 git repo 的障礙,我們正在解決這個行政問題。我們之是以說“必須”在同一個 git repo,是因為下面會介紹到 git/svn 的指令在做代碼操作時,隻有在一個 git repo 裡才能最容易和自然的實作。

強強耦合的子產品必須在同一個 git branch(svn branch)裡進行,git branch 或 svn branch 是我們開發的最小機關,在同一個 branch 裡研發的人員應該坐在一起,有問題可以馬上解決,隻有這樣才能讓互相非常依賴的代碼以高速前進。

C++項目管理

上圖中,"aliyun"(阿裡雲)是多個強耦合團隊的總和項目,是一個 git repo(svn 庫),"pangu"(盤古)是阿裡雲的一個負責底層庫的團隊,是一個 git/svn branch,這張圖裡列出了所有維護代碼庫需要的 git/svn 指令,

git branch: 一次性的,盤古創始人執行這個指令後,從此所有盤古團隊的人就都在這個 branch 裡寫代碼

git merge: 經常性的,盤古團隊負責人負責定期的将盤古的代碼 merge 進入 master/trunk,也同時把 master/trunk 的代碼 merge 進入盤古 branch

git cherry-pick: 偶爾性的,有的時候另一個團隊的 bug fix 或小改動是急需的,就隻把那個diff (代碼改動)采摘過來

C++項目管理

見上圖,多個團隊時,每個團隊都在做同樣的事,他們各自按照自己的進度 git merge 和 git cherry-pick。注意,

他們從不互相等待,隻看自己的代碼,穩定了就馬上 git merge

每個團隊永遠保持 master/trunk 的正确性和穩定性

那麼如何保證 git merge 後代碼是正确穩定的呢?靠兩件事,

pass 所有自己的 unit tests

pass 所有上層團隊的 unit tests

假如 git merge 後 master/trunk 變的不正确不穩定了呢?那互相傷害的團隊必須同時補足各自的 unit test,因為雙方都有責任:

害人方沒有寫到一個 unit test 可以察覺錯誤

被害方沒有寫好一個 unit test 可以防止别人傷害到你

久而久之,日積月累,我們的 unit test 就會變的無比複雜和盤根錯節,變的讓 bug 無以遁形。

一般來講,我們可以每星期或每半個月 git merge 一次,讓其他所有團隊看到自己的代碼變化。剛剛開始 git merge 時可能 break master (不是簡單的 compilation failure,而是邏輯錯誤) 很多次,那不是因為我們 git merge 太頻繁了,而是因為我們的 unit test 太少了,要在此期間為每一次 break 加 unit test,直到穩定為止。

必須在所有團隊告誡大家 master/trunk break 是不可饒恕的錯誤!

最後 git merge 的人必須負責跑通所有已有的 unit tests

一旦 break 必須馬上 fix

fix 裡必須有新的 unit test 去避免将來的類似錯誤

C++項目管理

每個團隊釋出的軟體都是 master + delta,"delta" 是指自己團隊的代碼改動。不可以有任何其他的組合(比如 master + pangu branch + fuxi branch),原因很簡單,因為每次 git merge 時每個團隊已經努力確定 master 是正确的,而 pangu branch + fuxi branch 并不是 fuxi 團隊背書認可的組合。

對于 c/c++ 這門語言來說,再也沒有比 unit test 更能讓它穩定不出錯的了。unit test 就像馬路上的車一樣,而 bug 就像想要跑到路對面的小老鼠一樣,我們在抱怨我們的軟體 bug 特别多,很簡單,因為我們的馬路上就沒有什麼車在跑,好的 c++ 項目 unit test 繁多,小老鼠根本沒有機會可以跑到路的對面而不被撞到。

一個特别錯誤的認識就是把釋出的時間拖長,認為這樣軟體問題就會減少。好吧,讓我們來分析一下,

時間拖的再長,其中做的測試有哪些?假如有更多的測試,或許等待是值得的,但是幾乎再多的測試一般來講一天是可以跑完的,那麼,假如一個軟體是一個月或半年才釋出,隻有一天是有效的,其餘時間都是在無謂的等待。

時間拖的再長,其中的問題是不會自己解決的,我們無非是把解決問題的時間壓縮到更晚更短的時候而已。如果一個 bug 在我們頭腦剛寫完代碼時是最容易解決的話,為什麼要等到一個月後生疏了才去解決呢?

緩慢的節奏讓我們的程式員們變的技術遲鈍,無法在快速疊代中學到專業的 c++ 代碼研發

并不是讓我們每天都釋出,适當的控制風險是必要的,但是可以認為任何超過一個月的釋出都是拖沓的和緩慢的,我們控制風險靠的是測試叢集的設立,盡可能模拟線上環境的測試,灰階釋出,等等手段,其中沒有一個是“等待”。

alicpp 将着手于建立 continuous build system (連續 build 系統)和 continuous test system (連續測試系統),真正建立一套完善的 c/c++ 測試系統。

alicpp 會根據實際需要逐漸補充各種線上診斷系統,比如,

core dump 的自動收集和分析系統

gdb 的自動符号查詢

request capture/replay 系統,可以讓我們更容易的恢複現場

memory leak detector,自動監測記憶體洩漏

繼續閱讀