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

01 背景


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

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

支持多團隊協做:多團隊意味着多操做系統多 IDE ,下降不一樣操做系統和不一樣 IDE 下的工程配置的難度是重點要解決的難題之一;
支持多業務線定製:引擎庫爲手機、車機、開放平臺等業務線提供支持,而各個業務線的訴求不一樣,因此須要具有按功能構建的能力;
支持車機環境:在諸多業務線中,高德地圖有一個很是特殊的業務線,即車機(AMAP AUTO)。車機直接面對各大車廠和衆多設備商,環境多爲定製化,構建工具鏈各式各樣。若是針對每一個車機環境都定製一套構建配置文件,那麼其維護成本將很是高,因此如何用一套構建配置知足車機的多樣化構建需求成爲亟需解決的問題;
此外,因爲歷史緣由,引擎庫中源碼和依賴庫混雜,都存放於 Git 倉庫中,這樣會帶來兩個問題:web

隨着構建次數不斷增長,Git 倉庫愈來愈大,代碼與依賴庫檢出愈來愈慢,極大影響本地開發以及打包效率;
缺少統一管理,依賴關係混亂,常常出現由於依賴問題而致使的構建失敗,或者雖然構建成功但運行時發生錯誤的狀況;
上述的挑戰和歷史遺留問題嚴重阻礙了研發效能的提高。爲此,咱們對現有的構建及持續集成工具進行了深刻的研究和分析,並結合自身的業務特性,最終發展出高德地圖 C++ 本地構建工具 Abtor 和持續集成工具 Amap CI 。數據庫

02 本地構建


現有工具分析編程

C++ 是一門靠近底層的語言。不一樣的硬件、操做系統、編譯器,再加上交叉編譯,致使 C++ 構建的難度很是高。針對這些問題,C++ 社區涌現出許多優秀的構建工具,好比大名鼎鼎的 Make 和 CMake 。json

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

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_demo
├── 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核心配置文件:

#!/usr/bin/python
# -*- 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":"name@alibaba-inc.com",                    # 郵件通知
    "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 。三年又很短,對於一個系統開發生命週期而言,這僅僅是萌芽階段,咱們的征途纔剛剛開始。

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

高德二維碼.jpg

相關文章
相關標籤/搜索