天天看點

高德引擎建構及持續內建技術演進之路01 背景02本地建構03 持續內建04 未來展望

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 。這些工具的特點和不足如下:

高德引擎建構及持續內建技術演進之路01 背景02本地建構03 持續內建04 未來展望

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

Abtor

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

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

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

高德引擎建構及持續內建技術演進之路01 背景02本地建構03 持續內建04 未來展望

如上圖所示,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的車機建構關系圖如下:

高德引擎建構及持續內建技術演進之路01 背景02本地建構03 持續內建04 未來展望

依賴管理

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

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

高德引擎建構及持續內建技術演進之路01 背景02本地建構03 持續內建04 未來展望

當下,市面上 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解決本地建構遇到的一系列挑戰與問題後,我們開始将目光轉移到了整個持續內建階段。

高德引擎建構及持續內建技術演進之路01 背景02本地建構03 持續內建04 未來展望

持續內建是指軟體個人研發的部分向軟體整體部分傳遞,頻繁進行內建以便更快地發現其中的錯誤。它源自極限程式設計(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管理、建構管理、通知管理、網頁前端展示等幾部分組成,整體架構圖如下:

高德引擎建構及持續內建技術演進之路01 背景02本地建構03 持續內建04 未來展望

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

  • 可擴容:所有建構機器通過注冊的方式接入,建構機器擴容變得非常容易,減輕建構峰值帶來的壓力;
  • 可視化:Abtor Server 對于開發者是透明的。CI 平台與 Abtor Server 互動,為開發者提供沖突檢查、依賴檢視及庫下載下傳等可視化功能;
  • 智能化: CI 平台内置标準的 Jenkins Job 構模組化闆。開發者不感覺這些模闆,也無須做任何的修改。他們隻需要通過 Git 送出一個 tag 資訊即可實作全平台的建構,進而實作一鍵打 tag 建構;
  • 自動化:服務分析 Gitlab hook tag 的 push 資訊并拉取代碼,然後解析對應的配置檔案和要建構的所有平台資訊。根據這些資訊CI平台配置設定建構機器,并執行 Abtor 指令進行建構與釋出。所有這些皆自動完成;
  • 即時性:建構啟動後會發送釘釘消息,消息除了概要資訊外還附加了建構的連結等,開發者可以點選連結跟蹤進度情況。建構成功或失敗也都會發送消息,進而使得開發者可以及時進行下一步工作或處理建構錯誤;
  • 可擴充:CI平台提供可擴充的對接方式,友善高德或阿裡的其它平台對接,比如泰坦平台、CT平台、Aone等,進而實作編碼、建構、測試和釋出的開發閉環;

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

高德引擎建構及持續內建技術演進之路01 背景02本地建構03 持續內建04 未來展望

整樹關聯編譯

在第二部分中我們提到了一個問題,即如果某個庫被其它庫所依賴且有更新,那麼依賴它的庫也應當随之建構,以確定依賴的一緻性,這是建構自動化的關鍵點之一。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 。三年又很短,對于一個系統開發生命周期而言,這僅僅是萌芽階段,我們的征途才剛剛開始。

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

繼續閱讀