C++項目管理
該文章來自於阿裏巴巴技術協會(ATA)精選文章。
1. 編譯的嚴謹性
(1) 頭文件的正確性
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 裏的文件,並且這些文件是隻讀的(永遠不會更改內容)。
(2) 三方庫的菱形依賴問題
假如 MyClass.cpp 依賴了兩個三方庫 a 和 b,它們又都同時依賴第三個三方庫 c,此時我們必須保證 a 和 b 依賴的是同一個版本的 c,而不能是稍有差異的不同的 c,否則會出現難以查詢的 build problem,編譯會通過,但是生成的執行文件是有問題的。
alicpp 清楚記錄每一套三方庫的依賴關係,精確到版本號,以確保完全避免菱形依賴關係可能帶來的隱患 。
2. 鏈接的嚴謹性
(1) 動態庫的正確性
我們有沒有問過自己一個問題,那就是線上運行的時候找到的 .so 是否是我們研發或測試時使用的同一個 .so 文件?這些 .so 包括:
- linux 和 glibc 的,比如 libpthread.so, librt.so
- C++ 的,比如 libstdc++.so, libboost_xxx.so
- 三方庫的,比如 openssl 的 libcrypto.so
- 二方庫的
對任何這些機載的 .so 文件的依賴都會造成程序運行的不確定性,因為任何其他部門的人或任何人的錯誤操作都有可能對這些文件作變動。
前麵提到的三方庫菱形依賴問題在鏈接時依然存在,當兩個三方庫想要鏈接不同版本的 .so 時,它們在執行時是衝突和危險的。
(2) 全靜態鏈接
假如我們用靜態鏈接的方式鏈接所有依賴的庫,突然世界變的非常的美好,
- 我們發布的時候不再需要準備什麼 package,因為隻有一個執行文件需要 push 到目標機器
- 在目標機器上也不需要什麼 ldconfig 或 LD_LIBRARY_PATH,不會有 .so 查詢不到的問題
- 因此也就不會鏈接到錯誤的 .so
- 靜態鏈接的程序因為沒有 GOT (Global Offset Table) 的間接符號查詢,所以執行速度會快
我們對目標機器的依賴性因此降至最低,隻要是正確的 Linux 大版本,就不會出任何問題,所有其他團隊可以升級更換機器上的其他軟件而不受其影響。
當然,全靜態鏈接也有一些問題,
- 鏈接速度慢:下麵會提到解決方案
- 文件可能很大:其實今天來講 1GB 之內都是沒有問題的
- 執行時不能多個執行文件共享一個 copy 的 .so:但假如整個機器給了一個應用,那 .so 和 .a 是使用相同內存量的
- 在一些特殊情況下,不得已必須用 .so,比如動態生成的 .cpp 代碼,Oracle 未開源的代碼庫,等等特殊情況
無論如何,大家應該看到全靜態的美,盡可能的用靜態方式鏈接更多的庫,alicpp 幫助大家準備全靜態的鏈接指令。
(3) 半靜態鏈接
如果我們很在意線上運行時不能鏈接到錯誤的 .so,我們可以靠生成特殊的 .so 文件名來解決這個問題,比如:
pangu-trunk-3412562.so
<項目>-<repo>-<revision>.so
此時的 .so 就成為“隻讀文件”(意思是名字和內容是一一對應的,沒有人可以用同一個名字定義另一個改變了內容的文件),可以確保鏈接的絕對正確性。
3. 編譯和鏈接的速度
有了嚴謹的編譯和鏈接守則,我們就可以把大家統一到一個編譯和鏈接的標準和流程上來,我們也就可以統一的來解決我們的編譯和鏈接的速度問題。
(1) 編譯的速度
alicpp 將致力於建立一套完整和龐大的編譯係統來優化我們的日常編譯過程 T264,其中會用到,
- 大規模集群上的分布式編譯
- pre-compiled headers (PCH):因為我們完整的操控了所有 header files,我們可以提前編譯好這些頭文件
- cached .o:我們絕大部分編譯工作是和同事們編譯相同的 .cpp,我們可以把編譯結果記錄在類似於 ccache 的內存中
- offline compilations: 我們會在淩晨時分提前編譯好常用文件
(2) 鏈接的速度
alicpp 會嚐試 Google 的 Gold Linker,可以 5 到 10 倍的提高鏈接速度 。
4. 多模塊代碼共建
可以說我所看到的我們目前的 C/C++ 團隊和模塊之間的協作關係是混亂不堪的,因為我們沒有遵守應有的法則。這裏詳細的記錄和解釋了每一個步驟和理由:
IMPORTANT: 這裏的守則是“充分”和“必要”的,換句話講,沒有一個是不需要的,也沒有一個是沒有提到的。
(1) 模塊的依賴性
我們每一個團隊對其他團隊的依賴性都可以按照進度要求來分成三種情況,
(a) 弱耦合
我們對對方的進度要求不高,我們需要的功能目前已經提供了,如果將來有新版本的話,升級了當然好,但是不升級也問題不大,即使升級也是低優先級的工作。典型的例子是對大多數三方庫的要求。
(b) 強耦合
我們必須盡量跟上對方的進度,不然就造成軟件對接的諸多問題或是線上支持的困難,但是我們又擔心跟的太緊會看到對方不必要的新代碼帶來的 bug,此時我們要的是“盡量跟上,但並不要最新版本”。我們很多團隊之間的關係就是這樣的,比如 ODPS 軟件依賴底層的飛天係統,但是 ODPS 有自己的穩定性需求,不能對新寫的飛天代碼跟進太快。
(c) 強強耦合(一體)
我們必須和對方是相同進度,因為我們代碼的依賴性太大,同時雙方又在不斷做調整。我們小團隊之內就是這種情況。假如兩三個小團隊之間也相互依賴的非常緊密,也是處於一體狀態中。
(2) 代碼庫的結構
針對以上三種依賴性,我們就可以直接確立代碼庫的結構:
IMPORTANT: 明明是強耦合的情況卻自己定義成弱耦合是偷懶!明明是強強耦合的情況卻自己定義成強耦合是分裂主義!我們要盡可能的把依賴性朝著強的方向確立。
(a) 弱耦合
弱耦合的模塊可以在不同的 git repo(svn 庫)裏,比如 alicpp 的三方庫,甚至於一些二方庫,它們可以有自己的 git repo,隻要我們有辦法找到他們的 include 和 lib 就可以和它們對接編譯和鏈接,我們也可以從容的針對它們的不同版本進行引進,非常長期的做版本升級工作。
(b) 強耦合
強耦合的模塊必須在同一個 git repo(svn 庫)裏開發,編譯速度不是我們分屬不同 git repo 的借口,我們正在解決這個技術問題。代碼權限是人為的分屬不同 git repo 的障礙,我們正在解決這個行政問題。我們之所以說“必須”在同一個 git repo,是因為下麵會介紹到 git/svn 的命令在做代碼操作時,隻有在一個 git repo 裏才能最容易和自然的實現。
(c) 強強耦合(一體)
強強耦合的模塊必須在同一個 git branch(svn branch)裏進行,git branch 或 svn branch 是我們開發的最小單位,在同一個 branch 裏研發的人員應該坐在一起,有問題可以馬上解決,隻有這樣才能讓相互非常依賴的代碼以高速前進。
(3) 代碼周期
上圖中,"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 (代碼改動)采摘過來
IMPORTANT: 除了這些命令外,不再需要任何其他命令,更加進一步說,其他任何命令都是不允許的。
見上圖,多個團隊時,每個團隊都在做同樣的事,他們各自按照自己的進度 git merge 和 git cherry-pick。注意,
- 他們從不相互等待,隻看自己的代碼,穩定了就馬上 git merge
- 每個團隊永遠保持 master/trunk 的正確性和穩定性
那麼如何保證 git merge 後代碼是正確穩定的呢?靠兩件事,
- pass 所有自己的 unit tests
- pass 所有上層團隊的 unit tests
NOTE: 這就是為什麼我們要在一個 git repo 裏做強耦合的代碼開發,因為底層的改動必須自己編譯所有上層代碼,並負責跑通上層的人寫的保護自己的 unit tests。
假如 git merge 後 master/trunk 變的不正確不穩定了呢?那相互傷害的團隊必須同時補足各自的 unit test,因為雙方都有責任:
- 害人方沒有寫到一個 unit test 可以察覺錯誤
- 被害方沒有寫好一個 unit test 可以防止別人傷害到你
久而久之,日積月累,我們的 unit test 就會變的無比複雜和盤根錯節,變的讓 bug 無以遁形。
IMPORTANT: 代碼的穩定性來自於天長日久積累的 unit tests,不是靠戰戰兢兢的研發,慢慢悠悠的發布,代碼不是紅酒,不是放在那裏就會自己變好的,所以把發布時間拖長是不會讓代碼更穩定的。
(4) git merge 周期
一般來講,我們可以每星期或每半個月 git merge 一次,讓其他所有團隊看到自己的代碼變化。剛剛開始 git merge 時可能 break master (不是簡單的 compilation failure,而是邏輯錯誤) 很多次,那不是因為我們 git merge 太頻繁了,而是因為我們的 unit test 太少了,要在此期間為每一次 break 加 unit test,直到穩定為止。
IMPORTANT: 明明已經可以 git merge 而不做是偷懶!是耽誤其他所有人進度的不負責行為!
(5) master/trunk break
必須在所有團隊告誡大家 master/trunk break 是不可饒恕的錯誤!
- 最後 git merge 的人必須負責跑通所有已有的 unit tests
- 一旦 break 必須馬上 fix
- fix 裏必須有新的 unit test 去避免將來的類似錯誤
(6) Master + Delta
每個團隊發布的軟件都是 Master + Delta,"Delta" 是指自己團隊的代碼改動。不可以有任何其他的組合(比如 master + pangu branch + fuxi branch),原因很簡單,因為每次 git merge 時每個團隊已經努力確保 master 是正確的,而 pangu branch + fuxi branch 並不是 fuxi 團隊背書認可的組合。
5. 測試係統
對於 C/C++ 這門語言來說,再也沒有比 unit test 更能讓它穩定不出錯的了。unit test 就像馬路上的車一樣,而 bug 就像想要跑到路對麵的小老鼠一樣,我們在抱怨我們的軟件 bug 特別多,很簡單,因為我們的馬路上就沒有什麼車在跑,好的 C++ 項目 unit test 繁多,小老鼠根本沒有機會可以跑到路的對麵而不被撞到。
一個特別錯誤的認識就是把發布的時間拖長,認為這樣軟件問題就會減少。好吧,讓我們來分析一下,
- 時間拖的再長,其中做的測試有哪些?假如有更多的測試,或許等待是值得的,但是幾乎再多的測試一般來講一天是可以跑完的,那麼,假如一個軟件是一個月或半年才發布,隻有一天是有效的,其餘時間都是在無謂的等待。
- 時間拖的再長,其中的問題是不會自己解決的,我們無非是把解決問題的時間壓縮到更晚更短的時候而已。如果一個 bug 在我們頭腦剛寫完代碼時是最容易解決的話,為什麼要等到一個月後生疏了才去解決呢?
- 緩慢的節奏讓我們的程序員們變的技術遲鈍,無法在快速迭代中學到專業的 C++ 代碼研發
並不是讓我們每天都發布,適當的控製風險是必要的,但是可以認為任何超過一個月的發布都是拖遝的和緩慢的,我們控製風險靠的是測試集群的設立,盡可能模擬線上環境的測試,灰度發布,等等手段,其中沒有一個是“等待”。
IMPORTANT: “等待”就是浪費生命,浪費公司財產,是 C/C++ 代碼研發效率低下的表現,是不知道如何提高係統穩定性的懦弱做法。希望大家真正的提高我們的工作節奏,摒棄等待的消極做法。
IMPORTANT: “遲遲不敢跟進別的團隊的代碼”是我們長期沒有遵守以上共建守則造成的,希望大家達到共識後,敢於 git merge,在 break 時耐心增加 unit test 來鞏固我們的對 bug 的防守線,慢慢的我們就會對 master 建立足夠的穩定度和信任。
alicpp 將著手於建立 continuous build system (連續 build 係統)和 continuous test system (連續測試係統),真正建立一套完善的 C/C++ 測試係統。
6. 診斷係統
alicpp 會根據實際需要逐步補充各種線上診斷係統,比如,
- core dump 的自動收集和分析係統
- gdb 的自動符號查詢
- request capture/replay 係統,可以讓我們更容易的恢複現場
- memory leak detector,自動監測內存泄漏
最後更新:2017-04-01 13:37:08