高德引擎構建及持續整合技術演進之路

amap_tech發表於2019-11-06

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":"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 。三年又很短,對於一個系統開發生命週期而言,這僅僅是萌芽階段,我們的征途才剛剛開始。


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


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69941357/viewspace-2662925/,如需轉載,請註明出處,否則將追究法律責任。

相關文章