571
社区成员
发帖
与我相关
我的任务
分享Yet Another Distributed C++ Compiler. yadcc是一套腾讯广告自研的分布式编译系统,用于支撑腾讯广告的日常开发及流水线。相对于已有的同类解决方案,我们针对实际的工业生产环境做了性能、可靠性、易用性等方面优化。目前在我们1700+核的集群中每天编译300,0000+个目标文件,产出约3~5TB,已经持续稳定运营 8 个月。
2021 年 6 月,正式对外开源。
取决于代码逻辑及本地机器配置,yadcc可以利用几百乃至1000+核同时编译(内部而言我们使用512并发编译),大大加快构建速度。
yadcc需要 GCC 8 及以上版本的编译器,基于yadcc进行分布式编译时可以支持其他更低版本编译器。ln -sf yadcc g++创建的符号链接)PATH头部,这样构建系统就会实际执行yadcc来编译yadcc会按照命令行对源代码进行预处理,得到一个自包含的的预处理结果由于预处理时间通常远小于编译时间,因此这样可以降低单个文件的本地开销。同时,由于等待编译结果时本地无需进行操作,因此可以增大本地的编译并发度(如8核机器通常可以make -j100),以此实现更高的吞吐。
需要注意的是,分布式编译通常只能提高吞吐,但是不能降低单个文件的编译耗时(假设不命中缓存)。因此,对于无法并发编译的工程,除非命中缓存,否则分布式编译通常不能加快编译,反而可能有负面效果。
我们的系统由调度器、缓存服务器、守护进程及客户端组成:
同时,我们做了多层重试,确保不会因为网络抖动、编译机异常离线等工业场景常见的问题导致的不必要的失败。
调度器具备全局视图,负责将各个编译任务关联到一台编译机。
不同于distcc,全局视图可以避免由任务提交机器进行本地决策而导致负载不均衡,如压垮某台编译机的同时还有另外的机器空闲等。
目前我们的调度算法较为简单,其以如下几点为目标来分配编译机:
优先考虑专有编译机。专有编译机负载不超过50%时任务始终分配至专有编译机。
负载超过50%之后需要考虑SMT导致的单核性能下降,因此此时如果有更空闲的机器会优先考虑。
除非没有其他机器,尽量避免将负载分配至提交任务的机器:这允许提交任务的机器有更多的CPU资源进行预处理。
为避免某些机器自身已有负载过高而导致新分配的编译任务执行过慢,调度算法会考虑参与编译任务的机器负载。如果机器空闲CPU少于机器本身所能接受的最大任务数,则以较小值作为“实际能接受的最大任务数”。
目前,Daemon定期在心跳包内上报自己15s内的平均负载,选择15s也是为了让daemon机器负载变化对调度算法更加敏感。
在剩余可选机器中尽量保证各个机器的编译负载(任务数/实际能接受的最大任务数)均衡。
在没有机器有空闲资源(包括提交方自身)时,调度器会阻塞分配请求,避免过多任务压垮编译集群。
调度器会定期扫描我们的缓存并构造相应的缓存布隆过滤器。
除此之外,为了保证布隆过滤器的时效性,在守护进程和调度器的心跳中,我们会:
部分情况下一台守护进程即提交任务又接受任务,此时上述两种行为这个守护进程和调度器之间均会发生。
由于编译缓存通常较大(我们的负载中单个编译结果在zstd压缩之后平均约1M),使用Redis这样的全内存数据库并不经济,因此我们决定自行设计一套缓存服务。考虑到需求可能随时会发生改变,我们也保留充分扩展缓存服务器的可能。
参考缓存多层次结构,我们将缓存设计成了2层(简称L1和L2)。通常L1比L2缓存更快成本也更昂贵,L2缓存相对来说比较大并且更加可靠,可以认为是主缓存。
L1缓存毫无疑问是基于内存,直觉上是缓存热点数据,并采用一定淘汰策略保持大小可控。算法是参照ARC实现,该算法会自适应的在LRU和LFU中进行折中,并不需要人工去调节参数。短期我们认为不会有明显更优的方案,所以采用默认实现足以,并不需要考虑扩展问题。
为了便于系统今后方便扩展,适应更多存储方案,我们抽象了底层存储引擎实现。当我们需要其他存储方案时,可以快速实现一套底层存储方案,并通过修改配置选择对应的存储方案,并不需要修改核心逻辑。
可考虑缓存方案如下:
如何使用该L2缓存选项,表示你并不需要L2缓存,那么所有的缓存将在内存进行。如果程序重启,将丢失所有的缓存记录。
考虑到编译缓存的如下一些特点:
因此我们放弃了使用LevelDB、RocksDB等NoSQL引擎,而选择直接基于磁盘文件保存缓存。
守护进程在Yadcc编译工具中主要负责两大板块的内容:
Yadcc自带了必要的第三方库,因此通常不需要额外安装依赖。
需要注意的是,yadcc通过git-submodule引用flare,因此编译之前需要执行git submodule update拉取flare。另外由于flare代码仓库需要git-lfs支持,因此您还需要安装git-lfs。
Flare 是我们吸收先前服务框架和业界开源项目及最新研究成果开发的现代化的后台服务开发框架,旨在提供针对目前主流软硬件环境下的易用、高性能、平稳的服务开发能力。
Flare 项目开始于 2019 年,目前广泛应用于腾讯广告的众多后台服务,拥有数以万计的运行实例,在实际生产系统上经受了足够的考验。
git clone https://github.com/Tencent/yadcc --recurse-submodules
或
git clone https://github.com/Tencent/yadcc
cd yadcc
git submodule init
git submodule update .
git clone https://github.com/Tencent/yadcc --recurse-submodules
git-lfscd yadcc
git submodule init
git submodule update .
./blade build yadcc/...
yadcc并不区分编译机及用户机。默认情况下,用户为了提交任务至编译集群,即会自动贡献一部分CPU至编译集群供其他用户使用。
./yadcc-scheduler
yadcc/cache/yadcc-cache
./yadcc-daemon --scheduler_uri=flare://ip-port-of-scheduler --cache_server_uri=flare://ip-port-of-cache-server
yadcc可以和类似于distcc / icecream等分布式编译系统相同的方式,既可以通过软链接到g++ / gcc实现整合,也可以通过(对于某些构建系统)覆盖CXX / CC / LD环境变量整合。
~/.yadcc/bin、~/.yadcc/symlinks。build64_release/yadcc/client/yadcc至~/.yadcc/bin。~/.yadcc/symlinks/{c++,g++,gcc}至~/.yadcc/bin/yadcc。~/.yadcc/symlinks加入PATH的头部。**请注意,创建软链并通过软链~/.yadcc/symlinks/g++执行并不等同于直接执行~/.yadcc/bin/yadcc**。
CXX='/path/to/yadcc g++' CC='/path/to/yadcc gcc' LD='/path/to/yadcc g++' ./blade build //path/to:target -j500
我们搭建了一个 1000 多核的测试机群,在一些大型 C++ 项目上实测了效果。
LLVM 项目:
在我们的测试环境中共计 6124 个编译目标,结果如下:
对于我们内部的一组更大的实际产品项目代码上:
此处列出了一些对比数据。所有测试均不命中缓存。
8C虚拟机,256并发度,使用 llvm-project-11.0.0.tar.xz
可能取决于机器环境,不同机器上cmake3生成的目标数不一定一样。此处我们的环境中共计6124个编译目标。
命令行:time ninja
[6124/6124] Linking CXX executable bin/opt
real 47m51.414s
user 356m17.391s
sys 23m25.461s
YADCC_CACHE_CONTROL=2表示不读取缓存,但是执行缓存相关逻辑并写入缓存。主要用于调试目的。
命令行:time YADCC_CACHE_CONTROL=2 ninja -j256
[6124/6124] Linking CXX executable bin/clang-check
real 3m11.292s
user 16m48.304s
sys 4m24.946s
ccache + distcc的对比我们基于内部的蓝盾平台提供的ccache + distcc进行对比,16C虚拟机中以144并发度编译约18k个目标:
本地编译采用16并发时编译过程中OOM导致编译器被kill,故本地编译采用8并发。
需要注意的是,我们内部提供的ccache + distcc会对本地并发度进行限制(同yadcc一样,避免本地过载)。且我们观察到了部分失败重试,这些重试一定程度上阻塞了后续分布式编译任务的生成,因此跑出来的结果可能较于理想条件下更差。但是整体而言,yadcc相对于ccache + distcc依然应当有较为明显的优势。
总体而言,yadcc 有相当明显的性能优势。
作者:NP153