持续交付2.0

使 C++ 项目编译速度提高 3 倍的 2 个技巧

Image

2011年,我在百度工作指导大搜的某个团队时,发现该项目每次编译构建的时间都要40分钟,根本无法做到「快速且高质量地反馈」。于是,为了提高c++ 项目的编译速度,我带着三个同学做了百度第一代具有分布式编译加速能力的持续集成工具。

在《持续交付2.0》第7 章「部署流水线原则与工具设计」中,介绍了当时所设计的架构,并没有讨论技术细节。本文介绍的是当时 C++ 项目所用的两个工具,distcc(https://distcc.github.io/)和ccache(https://ccache.dev/)。

Image

本文将介绍如何通过使用 distcc 服务器容器分配编译负载来加快编译时间,即:如何设置和使用运行distcc(https://distcc.github.io/)服务器的容器来将编译负载分布到异构节点集群(开发笔记本电脑、旧台式电脑和 Mac)上。为了提高编译的速度,我们也将使用ccache(https://ccache.dev/)这个本地编译缓存工具。

1

编译结果对比

1.1 没有使用编译加速前

大型的 C++ 项目,编译速度一直都是一个阻碍团队做持续集成实践的理由。

下面是编译LLDB(https://lldb.llvm.org/)所有的环境与结果数据,即使用是 Release Mode,编译目标(git 标签:llvmorg-7.1.0)也需要接近两个小时。Release version 已经是在构建速度最快的一种了。

编译环境:

  • Lenovo thinkpad T460s(https://www.lenovo.com/us/en/p/laptops/thinkpad/thinkpadt/thinkpad-t460s/22tp2tt460s?cid=us:sem:egjcna|se|google|kw_1299|T460S|_IIP_NX_Thinkpad_T_Series_SMB&invsrc=sem_google&ef_id=WD0KagAAAF_wIyYA:20170402044135:s)笔记本电脑

  • Fedora29(https://getfedora.org/)操作系统

  • GCC8.2.1 20180801

  • clang版本 7.0.1 (Fedora 7.0.1–6.fc29)

当使用 make -j 4 编译 LLDB 时,time命令会报告以下内容:

real    72m24,439s
user 224m32,731s
sys 12m2,094s

这意味着,必须等待大约两个小时才能完成一次性编译。

1.2 使用编译加速后

编译工具增加了:

  • ccache版本3.4.2

  • distcc 3.2rc1 x86_64-redhat-linux-gnu

使用 ninja ,参数为10 个编译作业(由 确定distcc -j)在集群(笔记本电脑、台式机、iMac)上编译 LLDB 时,time命令报告如下:

real    22m29,130s
user 49m16,726s
sys 4m27,407s

这说明,使用 ninja 进行编译, distcc 集群

  • 实时速度快 3.22 倍。

  • 进程在用户模式下花费的 CPU 秒总数快了 4.56 倍。

  • 进程在内核模式下花费的 CPU 秒数快 2.7 倍。

2

技巧1:分配编译负载
Image

distcc(https://distcc.github.io/)可以让您将编译作业分发到其他机器上。但是,它要求在工作计算机(服务器)上安装与开发人员计算机(客户端)上完全相同的编译器。这对后期的环境维护可能是一个麻烦的问题。现在解决这个问题就简单多了,使用容器技术。

3

技巧2:使用distcc服务器容器

Image

下面是一个使用异构集群做编译器集群的架构示意图。

Image

在一个Docker容器中,只能有一个顶层进程。在我们的例子中,那就是distcc(https://github.com/kwk/distcc-docker-images/blob/7195a75cda2772bab400a89d24f6daae41eece44/Dockerfile.fedora29#L42)。我们使用所谓的entrypoint 来配置它,并传递我们想要保持独立于运行容器的机器的所有参数:

ENTRYPOINT [\
"distccd", \
"--daemon", \
"--no-detach", \
"--user", "distcc", \
"--port", "3632", \
"--stats", \
"--stats-port", "3633", \
"--log-stderr", \
"--listen", "0.0.0.0"\
]

如果你不了解上面的参数,使用「man distccd」去了解一下。

还有一些默认的参数,你如果需要,也可以自行更改。

# 默认情况下,distcc 服务器将接受来自任意一个客户端。
# 请根据你的需要自行修改
CMD [\
"--allow", "0.0.0.0/0", \
"--nice", "5", \
"--jobs", "5" \
]

可以试一下,无需自己构建映像,只需发出以下命令即可获取最新版本:

docker pull konradkleine/distcc:fedora29

下面是这个Dockerfile 的全文。

如何运行 Docker 镜像

$ docker run \
-p 3632:3632 \
-p 3633:3633 \
-d \
konradkleine/distcc:fedora29

如何测试 Docker 镜像

$ docker run \
-p 3632:3632 \
-p 3633:3633 \
-d \
--name localdistcc \
konradkleine/distcc:fedora29

可以用 htop在创建的容器内运行,查看发生了什么。

Image

编译一些代码并将其分发到 localdistcc 节点,执行的命令如下:[这里我将编译LLDB(https://lldb.llvm.org/resources/build.html#building-lldb-on-linux-freebsd-and-netbsd)并进行调整]:

git clone 
https://github.com/llvm/llvm-project.git ~/dev/llvm-project
export LOCAL_DISTCC_IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' localdistcc)
mkdir -p ~/dev/llvm-builds/release-gcc-distcc
cd ~/dev/llvm-builds/release-gcc-distcc
cmake ~/dev/llvm-project/llvm \
-G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DLLVM_USE_LINKER=gold \
-DLLVM_ENABLE_PROJECTS="lldb;clang;lld" \
-DCMAKE_C_COMPILER=/usr/bin/gcc \
-DCMAKE_CXX_COMPILER=/usr/bin/g++ \
-DCMAKE_EXPORT_COMPILE_COMMANDS=1 \
-DCMAKE_C_COMPILER_LAUNCHER="ccache;distcc" \
-DCMAKE_CXX_COMPILER_LAUNCHER="ccache;distcc"
ninja lldb -j $(distcc -j)

当exporting DISTCC_HOSTS时,我建议注意一下 /7 ,它告诉 distcc ,在这台机器上分配 7 个job。默认是 4 个 job。

另外,通过 Discc 指定并行运行的构建作业数量:-j $(distcc -j)。

使用 ninja 构建时,查看输出distccmon-gnome。它应该显示如下内容:

Image

注意:distcc -j假设列出的所有主机DISTCC_HOSTS都是不同的机器。但容器在你的本地主机上运行, 从而消耗其中的资源。如果速度变慢,就中止编译;无论如何,这只是为了测试一下。

如何可视化编译的分布?

在 Fedora 29 上,可以使用distccmon-gnome或distccmon-text(参见下面的屏幕截图)。要安装它们,请运行:

sudo dnf distcc-gnome distcc

Image

Image

4

FAQ

如何检查所有主机是否都在为 distcc 服务?

检查一下,是否所有节点都通过 netcat ( nc) 为 distcc 提供服务。在以下命令中,将IP替换为你自己的主机的 IP。

for i in 192.168.178.33 192.168.178.39; do nc -zv $i 3632; done

你也可以使用nmap ,可能会慢一些。

nmap -A 192.168.178.33/32 -p 3632 -Pn

如何更安全一些?

在运行Docker 时,指定自己的IP 可以访问。

$ docker run \
-p 3632:3632 \
-p 3633:3633 \
-d \
konradkleine/distcc:fedora29 \
--allow <YOUR_HOST>

必须使用 ccache 还是可以只使用 distcc?

你可以使用其中任何一个,也可以同时使用它们。只需要修改CMake 的参数。

# 使用两者
cmake -DCMAKE_C_COMPILER_LAUNCHER="ccache" \
-DCMAKE_CXX_COMPILER_LAUNCHER="ccache" \
...
export CCACHE_PREFIX=distcc

或者

# 只使用ccache
-DCMAKE_C_COMPILER_LAUNCHER="ccache" \
-DCMAKE_CXX_COMPILER_LAUNCHER="ccache"

或者

# To use distcc alone
-DCMAKE_C_COMPILER_LAUNCHER="distcc" \
-DCMAKE_CXX_COMPILER_LAUNCHER="distcc"

如何为一台机器发送 4个任务?

在DISTCC_HOSTS环境变量中,需要调整限制,默认为4。它在/<LIMIT>主机名后面指定:

export DISTCC_HOSTS="fasthost/8 slowhost/2 localhost"

如何查看 ccache 是否工作?

查看命令 ccache --show-stats 的输出并检查缓存命中率。例如,下面的命中率为15.61%。

$ ccache --show-stats
cache directory /home/kkleine/.ccache
primary config /home/kkleine/.ccache/ccache.conf
secondary config (readonly) /etc/ccache.conf
stats zero time Wed Apr 17 18:44:31 2019
cache hit (direct) 1787
cache hit (preprocessed) 21
cache miss 9774
cache hit rate 15.61 %
called for link 584
called for preprocessing 35
compile failed 12
preprocessor error 116
unsupported code directive 4
no input file 19
cleanups performed 0
files in cache 25218
cache size 1.2 GB
max cache size 5.0 GB

有替代工具吗?

ICECC(Icecream)最初是基于distcc,但Icecream的好处是:你不需要在构建节点上安装相同的工具链,因为源机器的构建环境会分发到网络中。对于具有多个目标平台的项目非常有用。

Image

乔梁老师开课啦~视频课程《持续部署实战解析(NodeJS版)》, 限时特价!

Image

(扫码订阅)

Image