高德引擎構建及持續集成技術演進之路

  • 2019 年 11 月 6 日
  • 筆記

 

01 背景

由於導航應用中的地圖渲染、導航等核心功能對性能要求很高,所以高德地圖客戶端中大量功能採用 C++ 實現。隨著業務的飛速發展,僅地圖引擎庫就有40多個模組,工程配置極其複雜,原有的構建及持續集成技術已無法滿足日益增長的需求變化。

除了以百萬計的程式碼行數帶來的複雜度外,高德地圖客戶端中的 C++ 引擎庫工程(以下簡稱引擎庫)的構建和持續集成還面臨以下幾個挑戰:

  • 支援多團隊協作:多團隊意味著多作業系統多 IDE ,降低不同作業系統和不同 IDE 下的工程配置的難度是重點要解決的難題之一;
  • 支援多業務線訂製:引擎庫為手機、車機、開放平台等業務線提供支援,而各個業務線的訴求不同,所以需要具備按功能構建的能力;
  • 支援車機環境:在諸多業務線中,高德地圖有一個非常特殊的業務線,即車機(AMAP AUTO)。車機直接面對各大車廠和眾多設備商,環境多為訂製化,構建工具鏈各式各樣。如果針對每個車機環境都訂製一套構建配置文件,那麼其維護成本將非常高,所以如何用一套構建配置滿足車機的多樣化構建需求成為亟需解決的問題;

此外,由於歷史原因,引擎庫中源碼和依賴庫混雜,都存放於 Git 倉庫中,這樣會帶來兩個問題:

  • 隨著構建次數不斷增加,Git 倉庫越來越大,程式碼與依賴庫檢出越來越慢,極大影響本地開發以及打包效率;
  • 缺乏統一管理,依賴關係混亂,經常出現因為依賴問題而導致的構建失敗,或者雖然構建成功但運行時發生錯誤的情況;

上述的挑戰和歷史遺留問題嚴重阻礙了研發效能的提升。為此,我們對現有的構建及持續集成工具進行了深入的研究和分析,並結合自身的業務特性,最終發展出高德地圖 C++ 本地構建工具 Abtor 和持續集成工具 Amap CI 。

02 本地構建

現有工具分析

C++ 是一門靠近底層的語言。不同的硬體、作業系統、編譯器,再加上交叉編譯,導致 C++ 構建的難度非常高。針對這些問題,C++ 社區湧現出許多優秀的構建工具,比如大名鼎鼎的 Make 和 CMake 。

Make,即 GNU Make ,於1988年發布,是一個用來執行 Makefile 的工具。Makefile 的基本語法包括目標、依賴和命令等。使用過程中,當某些文件變了,只有直接或者間接依賴這些文件的目標才需要重新構建,這樣大大提升了編譯速度。

Make 和 Makefile 的組合可以看作項目管理工具,但它們過於基礎,在跨平台的使用方面有很高的門檻和較多的限制,此外大項目的構建還會遇到 Makefile 嚴重膨脹的問題。

CMake 產生於2000年,是一個跨平台的編譯、測試以及打包工具。它將配置文件轉化為 Makefile ,並運行 Make 命令將源碼編譯成可執行程式或庫。CMake 屬於 Make 系列,配置文件比 Makefile 具有可讀性,支援跨平台構建,構建性能高。

但是 CMake 也有兩項明顯不足,一是配置文件的複雜度遠高於其它現代語言,對於 CMake 語法初學者有一定的學習成本,二是與不同 IDE 的配合使用不夠友好。

可以看出 Make 和 CMake 的抽象度還是比較低,從而對構建人員的要求過高。為了降低構建成本,C++ 社區又出現了一些新的 C++ 構建工具,現在使用較廣泛的包括 Google 的 Bazel 和 Ninja ,以及 SCons 。這些工具的特點和不足如下:

111.jpg

經過上述對現有 C++ 構建工具的研究和分析,可以得出每個工具既有所長又有不足的結論。再考慮到高德地圖引擎庫工程面臨的挑戰和歷史遺留問題,我們發現以上工具沒有一個可以完美契合業務需求,且改造成本非常高,所以我們決定基於 CMake 自建 C++ 本地構建工具,即現在引擎庫工程使用的 Abtor 。

Abtor

首先,我們需要解釋一個問題,即 Abtor 是什麼?

Abtor 是一個 C++ 跨平台構建工具。Abtor 採用 Python 編寫構建腳本,生成 CMake 配置文件,並通過內置 CMake 組件生成構建文件,最終產出可執行程式或庫。它抽象出構建描述,使得複雜的編譯器和連接器對開發者透明;它提供強大的內置功能,從而有效的降低開發者編寫構建腳本的難度。

其次,我們需要闡述一個問題,即Abtor的構建流程是什麼?

222.png

如上圖所示,Abtor 構建的整個流程為:

  • 編寫 Abtor 構建腳本;
  • 解析 Abtor 構建腳本;
  • 檢測依賴關係,識別衝突,並從阿里 OSS 上下載所需依賴;
  • 生成CMakeLists.txt,並通過內置的 CMake 生成 Makefile 文件;
  • 編譯,鏈接,生成對應平台的目標文件;
  • 將目標文件發布到阿里 OSS ;

除此之外,還增加了控制訪問發布庫許可權的功能,用於保證發布庫的安全。

最後,我們需要探討一個問題,即Abtor解決了什麼?

在開篇背景中,我們提到阻礙研發效能的一些挑戰和問題,這就是 Abtor 需要解決的,所以 Abtor 具備以下特點:

  • 更廣泛的跨平台:支援 MacOS 、iOS、Android、 Linux、Windows、QNX 等平台;
  • 有效的多團隊協作:較好得與 IDE 結合,並支援一套配置生成不同項目工程,從而達到工程配置一致化;
  • 高訂製化:支援工具鏈及構建參數的靈活訂製,並通過內置工具鏈配置為車機複雜的構建提供強有力的支援;
  • 源碼與依賴分離:支援源碼依賴與庫依賴,源碼通過Git管理,構建庫存放於阿里雲,源碼與產物完全分離;
  • 良好的構建性能:快速構建大型項目,從而提高開發效率;

從上述特點可看到,Abtor 有效地解決了已有的構建工具在高德業務中面臨的痛點。但是冰凍三尺,非一日之寒,Abtor 也是在不斷地完善中,下面重點介紹一下 Abtor 發展過程中遇到的三個問題。

工程配置一致化

在日常開發過程中,工程項目的調試工作尤為重要。高德地圖客戶端中的 C++ 引擎庫工程的開發人員涉及幾個部門和諸多小組。這些組擅長的技術棧,使用的平台和習慣的開發工具都大為不同。如果針對每一個平台都單獨建立相應的工程配置,那麼工作量及後續維護成本可想而知。

基於以上原因,Abtor 內置與 IDE 結合的功能,即開發者可以通過一套配置並結合 Abtor 命令一鍵生成工程配置,實現在不同平台的工程配置的一致化。工程配置一致化為引擎庫開發帶來以下幾個收益:

命令簡單,降低學習成本,開發者只需熟記 abtorw project [IDE name];

配置文件不會因為 IDE 的增加而迅速膨脹,開發者更換構建命令,比如 abtorw project xcode 或者abtorw project vs2015,即可生成對應的項目工程;

有利於部門間的協作及新人的快速融入,開發者可以根據喜好選擇 IDE 進行開發,大大提高開發效率;

目前Abtor支援的IDE有 Xcode、Android Studio、Visual Studio、Qt Creator、CLion等。

複雜車機環境的構建

作為高德地圖一條非常重要的業務線,車機面對的構建環境複雜多變,廠商往往會自行訂製工具鏈。如果每接入一個設備,所有工程項目都需要修改配置文件,那麼這個成本還是非常高的。為了解決這個問題,Abtor 提供兩種做法:

內置工具鏈配置:對於開發者完全透明,他不需要修改任何配置即可構建相應平台的產物;

支援自定義配置插件:開發者按照規則編寫配置插件,構建時 Abtor 會檢測插件,並根據設置的工具鏈及構建參數進行構建;

除此之外,我們對所有的車機環境進行了 Docker 化處理,並通過 Docker 控制中心統一管理車機 Docker 環境的上線與下線,再利用上述 Abtor 的內置工具鏈配置功能內置車機構建參數,實現開發者無感知的環境切換等操作,有效地解決了複雜車機環境的構建問題。

基於 Docker 的車機構建主要步驟如下:

  • 工具鏈安裝:一般由廠商提供,我們會將該工具鏈安裝到基礎 Docker 鏡像中;
  • Docker 發布:將鏡像發布到 Docker 倉庫;
  • Abtor 適配:一次性適配工具鏈,並內置配置,開發者可通過 Abtor 版本升級使用該配置;
  • 服務配置更新:由 Jenkins 管理,支援分批更新 Abtor 版本,不影響當下編譯需求;
  • 服務監控: 由 Jenkins 管理,定時檢測服務狀態,異常態的 Docker 服務將自動被重啟;

基於Docker的車機構建關係圖如下:
333.jpg

依賴管理

依賴問題是所有構建工具都避免不了的問題,在這其中,菱形依賴問題尤為常見。如下圖所示,假設 A 依賴了 B 和 C ,B 和 C 又分別依賴了不同版本的 D,而 D 之間只存在很小的差異,這是可以編譯通過的,但最終在運行時可能會出現意想不到的問題。

如果沒有一種機制來檢測,菱形依賴是很難被發現,而產生的後果又可能是非常嚴重的,比如導致線上出現大面積的崩潰等。所以依賴問題的分析與解決非常重要。

444.png

當下,市面上 Java 有比較成熟的依賴管理解決方案,如 Maven 等,但 C++ 並沒有。為此 Abtor 專門建立依賴管理的機制來確保編譯的正確性。

Abtor 的依賴管理是怎麼做的呢?這裡提供一個思路供大家參考:

  • 建立 Abtor 服務端,用做庫發布,以及處理依賴關係;
  • 每個庫在雲端構建完,都會把庫依賴的版本資訊存放於雲端資料庫中;
  • 本地/雲端構建前 Abtor 會解析出所有依賴庫的版本資訊;
  • 遞歸查找這些子庫對應的依賴資訊,即可羅列出所有依賴庫的資訊;
  • 檢測依賴庫列表中是否存在不同版本號的相同庫名:
  • 如果沒有相同庫名,則繼續執行構建;
  • 如果有相同庫名,則說明依賴庫之間存在衝突問題,此時中斷構建,並顯示衝突的庫資訊,待開發者解決完衝突後方可繼續執行構建;

根據上述思路,我們保證了庫依賴的一致性,避免了菱形依賴問題。另外,如果某個庫被其它庫所依賴且有更新,那麼依賴它的庫也應當隨之構建,以確保依賴的一致性。這種對依賴構建的觸發更新我們放到 Amap CI 上實現,在第三節會進行詳細介紹。

工程實踐

在介紹完 Abtor 的一些基本原理後,我們將介紹 Abtor 在日常開發中是如何使用的。

下圖是 Abtor 工程項目的目錄結構,其中有兩類文件是開發者需要關心的,一類是源文件目錄(src),一類是 Abtor 核心配置文件(abtor.proj)。

├── ABTOR
│   └── wrapper│       ├── abtor-wrapper.properties # 配置文件,可指定Abtor版本資訊
│       └── abtor-wrapper.py         # 下載Abtor版本並調用Abtor入口函數
├── abtor.proj                       # Abtor核心配置文件
├── abtorw                           # Linux/Mac下的初始執行腳本
├── abtorw.bat                       # Windows下的初始執行腳本
└── src
   └── main.c                       # 要編譯的源文件

源文件目錄的組織形式與 Make 系列構建工具沒有太大區別。下面重點看一下Abtor核心配置文件:

# -*- coding: UTF-8 -*-# 以下內容為python語法# 指定編譯的源碼header_dirs_list = [abtor_path("include")]    # 依賴的頭文件目錄binary_src_list = [abtor_path("src/main.c")]  # 源碼cflags = " -std=c99 -W -Wall "cxxflags = " -W -Wall "# 指定編譯二進位abtor_ccxx_binary(  name = 'demo',  c_flags = cflags,  cxx_flags = cxxflags,  deps = ["add:1.0.0.0"],                       # 指定依賴的庫資訊
 include_dirs = header_dirs_list;  srcs = binary_src_list
)

從上圖可以看出,Abtor核心配置文件具有以下幾個特點:

  • 採用Python編寫,易上手;
  • 抽象類似 abtor_ccxx_binary 等的構建描述,降低使用門檻;
  • 提供諸如 abtor_path 等的內置功能,提高開發效率;

通過以上的對源文件目錄組織及 Abtor 核心配置文件編寫,我們就完成了項目的Abtor配置化,接著可以通過Abtor內置的命令構建、發布或直接生成項目工程。我們相信,即使開發者不是很精通構建原理,依然可以無障礙地使用Abtor進行構建與發布。

03 持續集成

面臨的問題

如下圖所示,整個開發工作流程可分為幾個階段:編碼->構建->集成->測試->交付->部署。在使用Abtor解決本地構建遇到的一系列挑戰與問題後,我們開始將目光轉移到了整個持續集成階段。

555.png

持續集成是指軟體個人研發的部分向軟體整體部分交付,頻繁進行集成以便更快地發現其中的錯誤。它源自極限編程(XP),是 XP最初的12種實踐之一。對於引擎庫來說,持續集成方案應該具備一次性批量構建不同平台不同架構目標文件的能力,同時也應當具備運維管理和消息管理的能力等。

最初高德引擎庫使用 Jenkins 進行持續集成。因為引擎庫開發採用在 Git 倉庫上拉取分支的方式進行版本管理,所以每次版本迭代都需要手動建立 Jenkins Job,修改相應腳本,另外還需要額外搭建一個依賴庫關係的 Jenkins Job 做聯動編譯。

假設有100個項目,那麼每個版本迭代都需要手動創建101個 Jenkins Job 。每次版本迭代都重複類似的操作,中間需要大量的協調工作,隨著迭代版本越來越多,這些 Jenkins Job 變得不可維護。這是 Jenkins 持續集成方案在高德引擎庫開發過程中遇到的非常嚴重的問題。

基於上述原因,我們迫切得需要這樣一個持續集成系統:開發者不用維護Jenkins,不需要部署構建環境,可以不了解構建細節,只需要通過某個觸發事件就能夠構建出所有平台的目標文件。於是我們決定自建持續集成平台,即 Amap CI。

Amap CI

Amap CI 平台使用Gitlab的Git Webhook實現持續集成。其中,Gitlab 接收開發者的 tag push 事件,回調 CI平台的後台服務,然後後台服務根據構建機器的運行情況進行任務的分發。當構建任務較多時,CI平台會等待直到有構建資源才進行任務的再分配。

Amap CI 平台由任務管理、Jenkins管理、構建管理、通知管理、網頁前端展示等幾部分組成,整體架構圖如下:

666.png

通過 Amap CI 平台,我們達到了以下幾個目的:

可擴容:所有構建機器通過註冊的方式接入,構建機器擴容變得非常容易,減輕構建峰值帶來的壓力;

可視化:Abtor Server 對於開發者是透明的。CI 平台與 Abtor Server 交互,為開發者提供衝突檢查、依賴查看及庫下載等可視化功能;

智慧化: CI 平台內置標準的 Jenkins Job 構建模板。開發者不感知這些模板,也無須做任何的修改。他們只需要通過 Git 提交一個 tag 資訊即可實現全平台的構建,從而實現一鍵打 tag 構建;

自動化:服務分析 Gitlab hook tag 的 push 資訊並拉取程式碼,然後解析對應的配置文件和要構建的所有平台資訊。根據這些資訊CI平台分配構建機器,並執行 Abtor 命令進行構建與發布。所有這些皆自動完成;

即時性:構建啟動後會發送釘釘消息,消息除了概要資訊外還附加了構建的鏈接等,開發者可以點擊鏈接跟蹤進度情況。構建成功或失敗也都會發送消息,從而使得開發者可以及時進行下一步工作或處理構建錯誤;

可擴展:CI平台提供可擴展的對接方式,方便高德或阿里的其它平台對接,比如泰坦平台、CT平台、Aone等,從而實現編碼、構建、測試和發布的開發閉環;

在上述目的中,對 Amap CI 平台最重要的是自動化,下面我們重點介紹一下自動化中的整樹聯動編譯。

777.png

整樹聯動編譯

在第二部分中我們提到了一個問題,即如果某個庫被其它庫所依賴且有更新,那麼依賴它的庫也應當隨之構建,以確保依賴的一致性,這是構建自動化的關鍵點之一。Amap CI 採用整樹聯動編譯的方案來解決這個問題。

開發者在CI平台上建立對應的版本構建樹,構建樹中羅列了各個庫之間的構建順序,如下圖所示。CI平台會根據這棵構建樹進行構建,被依賴的庫優先構建,完成後再自動觸發其上級的庫構建,以此類推,最終形成一棵多叉樹。在這棵多叉樹上,從葉子節點開始按層級順序逐級並發構建對應的庫,這就是整樹聯動編譯。

根據上述思路,我們保證了持續集成時的依賴一致性。開發者只需關心自己負責的庫,打個 tag ,即可觸發生成所有依賴該庫的庫,從而避免了依賴不一致的問題。

工程實踐

在介紹完 Amap CI 的一些基本原理後,我們將介紹日常開發中應該如何使用Amap CI。

一個新的工程項目在集成到 Amap CI 平台時,首先需要將CI平台的 web hook 網址增加到 Gitlab 的配置中,然後編寫配置文件 CI_CONFIG.json ,至此一個新的項目已集成完成,非常簡單。下面我們重點介紹一下 CI_CONFIG.json 。

CI_CONFIG.json 是核心配置文件,一次編寫,無需再修改。它的結構如下:

CI_CONFIG.json DEMO:(json)
{    "mail":"[email protected]",                    # 郵件通知    "arch":"Android,iOS,Mac,Ubuntu64,Windows",        # 構建的平台    "build_vars":"-v -V",                             # 構建參數    "modules":{                                       # 構建的模組列表        "amap":{                                      # 模組名為amap            "features":[                              # 功能列表
               {                    "name":"feature1",                # 設置功能名為feature1                    "macro":"-DFEATURE_DEMO1=True"    # 宏控:FEATURE_DEMO1
               },
               {                    "name":"feature2",               # 設置功能名為feature2                    "macro":"-DFEATURE_DEMO2=True"   # 宏控:FEATURE_DEMO2
               }
           ]
       },        "auto":{                                    # 模組名為auto            "features":[                            # 功能列表
               {                    "name":"feature1",              # 設置功能名為feature1                    "macro":"-DFEATURE_DEMO1=True"  # 宏控:FEATURE_DEMO1
               },
               {                    "name":"feature3",             # 設置功能名為feature3                    "macro":"-DFEATURE_DEMO3=True" # 宏控:FEATURE_DEMO3
               }
           ]
       }
   }
}


從上圖可以看出,配置文件描述了郵件通知、構建的平台、構建參數等資訊,同時還為多業務線訂製提供了良好的支援。
Amap CI 構建時讀取上述文件,解析不同項目中配置的宏,並通過參數傳遞給 Abtor ,另一方面開發者在程式碼中利用這些宏進行程式碼隔離,構建時會根據這些宏選擇對應的源碼進行編譯,從而支援多條業務線不同的需求,達到程式碼層面的最大復用。

 

目前 Amap CI 接入的項目數有幾百個,編譯的次數達到幾十萬次級別,同時在構建性能和構建成功率方面相比之前都有了大幅度的提高,現在仍舊不斷有新的項目接入到構建平台上。可以說 Amap CI 平台是高德地圖客戶端 C++ 工程快速迭代開發的堅實保障。

 

04 未來展望

 

從2016年年中調研現有構建工具算起,到現在三年有餘。三年很長,足以讓我們將構想變成現實,足以讓我們不斷完善 Abtor ,足以讓我們發展出 Amap CI 。三年又很短,對於一個系統開發生命周期而言,這僅僅是萌芽階段,我們的征途才剛剛開始。

 

關於未來,我們的規劃是向開發閉環方向發展,即打通編碼、構建、集成、測試、交付和部署等各個環節中的鏈路,解決業務開發閉環的問題,實現整個開發流程自動化,進一步把開發者從繁瑣的流程中解放出來,使得這些人員有精力去做更有價值的事情。