高德引擎构建及持续集成技术演进之路

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

相关文章
相关标签/搜索