Android基于distcc的分布式编译及负载均衡的实现(4.ccache的加入极大的改善了编译时间)

时间:2022-01-10 08:54:36

distcc简介

distcc

distcc源起于著名开源项目samba,是一款有着较长历史的跨平台开源分布式编译解决方案。

对于大多数c语言及其衍生语言来说,编译过程主要分为三个步骤:

  1. 预编译
  2. 编译
  3. 链接

distcc的作用就是将第二步的编译(3.0版本后通过pump支持部分第一步)过程采用网格计算的模式,将编译任务分配至其它主机,并在编译结束后回传,以供第三步链接使用。并由此降低了发起编译的机器的负载,并提升了编译效率。

distcc本身事实上并不参与任何编译过程,而只是一个编译器(gcc等)的前端。为编译器加入分布式特性,并参与部分管理和简单的负载均衡的功能。根据官方说法,其性能提升的极限阈值是3倍速度,即根据编译池的扩大,无限接近但无法达到原始编译速度的三分之一。

截至目前为止,distcc仅支持c语言及其衍生的c++,Objective-c等的编译。无法支持Java的分布式编译。

pump

自distcc版本3.0开始,加入了基于python的新工具pump。其功能是将头文件也随同源码一起发送至编译服务器。将部分预编译工作也进行分布式处理。从而进一步的提升了编译效率。官方给出的数字是对于文件传输、编译过程有10倍的效率提升。

但pump模式的局限之处在于:在整个编译过程中,源代码(尤其是头文件)不能进行改动,否则会造成错误的编译结果。

ccache

ccache同样产生于samba项目,其作用是将编译过程中的中间文件根据预编译结果,通过hash表索引进行缓存。Ccache具有以下特性:

  1. 在命中/缺失上是静态的
  2. 自动的缓存大小管理
  3. 可以缓存产生的编译警告
  4. 容易安装
  5. 很低的开销
  6. 可以使用硬联接来避免复制与distcc不同,ccache是android官方直接支持的编译加速方案。

dmucs

dmucs是为distcc服务的分布式负载均衡解决方案。原始的distcc仅根据服务器指定的顺序来分配编译任务,无法根据编译池中服务器的负载情况进行动态的编译任务分配。而dmucs则会将编译池中主机的负载情况通知到dmucs服务主机。并由其根据负载及编译机承担能力,动态的对编译任务进行分配。

基准测试结果及分析

构建结果分析

图1展示了不同编译条件下的编译时间。其中,单机的编译参数使用了-j8, 而双机、三机的编译参数则分别为: -j12,-j15

Android基于distcc的分布式编译及负载均衡的实现(4.ccache的加入极大的改善了编译时间)

由此图,大致可以总结出以下结论:

  1. distcc对android系统编译起到显著的加速作用,编译时间随着编译池中编译机器数目的增加而基本呈线性下降趋势。因为暂时无法获得更多的主机进行测试。故无法获知边界情况。
  2. 分布式编译的效果在android 4.0的编译过程中体现相对于 2.3更为明显。
  3. pump工具在编译过程中起到的速度提升作用并不明显。估计是因为android的预编译过程牵涉了大量的头文件和依赖关系,分布式得到的速度提升被文件传输过程所抵消。
  4. ccache的加入极大的改善了编译时间。对ccache的使用命中率,针对2.3.7平台,统计结果如下:
    cache hit 878
    cache miss 11494
    called for link 691
    not a C/C++ file 307
    unsupported compiler option 48
    files in cache 22988
    cache size 1.0 Gbytes
    max cache size 20.0 Gbytes

    ccache 初次编译 2.3.7 统计结果

    cache hit 12127
    cache miss 16
    called for link 691
    not a C/C++ file 307
    unsupported compiler option 48
    files in cache 23020
    cache size 1.0 Gbytes
    max cache size 20.0 Gbytes

    ccache 二次编译 2.3.7 统计结果

    由此可见,在第二次编译时(代码未改动),cache的命中率超过了90%。而速度也因此得到大幅度的提升,基本接近初始速度的三倍。对android 4.0版本的速度提升尤为显著。但此数据对平时开发过程中代码有变化的情况仅能起到参考作用。

  5. 在测试过程中,dmucs的加入并未在时间上有效的缩短单编译请求的时间。原因是因为java/c的混合编译,无法根据distcc的推荐方法增加编译job的数量,也就无法充分体现分布式编译均衡的优势。但根据编译时观察,dmucs确实能有效的控制编译池中编译机器的负载,并根据编译能力进行平均分配。
  6. 此外,在图表中未能体现的一个结果为:发起编译的客户机平均负载在采用了分布式编译之后,在c类语言编译阶段,均有较为可观的降低。

图2展示了在三机分布式条件下编译android 4.0代码,job数目与编译时间的大致关系。

Android基于distcc的分布式编译及负载均衡的实现(4.ccache的加入极大的改善了编译时间)


由此可知,单纯增加job数目并不能获取更好的编译时间。具体原因如下述。

存在的问题及后续解决方案的方向

由于android系统的编译过程中同时存在java的编译和c的编译,而distcc又仅支持c类语言的分布式编译。故对于android的分布式编译获得的效果不如普通纯c类语言项目明显。随着jobs数目的增加,仅能在本机处理的java编译过程反而由于机器硬件机能的限制而拖慢了整体的编译速度,甚至抵消了由distcc分布式编译获取的益处。

为了解决这个问题,可从以下几个方面入手,进行进一步的改造:

  1. 分割c类语言的编译过程:将整体android编译过程划分为c语言为主和java为主的两个过程,其中c语言的编译过程采用distcc编译,而java语言的编译则因循原来的编译方式。 此方法涉及android编译脚本的修改,实现较为简单。
  2. 修改make的实现,根据调用java/c编译器的不同,动态的调整作业数目。此改动可能涉及编译脚本修改甚至make源代码修改,实现稍微复杂。
  3. 将java编译过程同样进行分布式处理。可能的方式是在不同的主机上对不同的java项目进行编译。但需要物理的共享存储以及编译脚本修改。实现更为复杂。但仍在可行的范围内。

分布式负载均衡编译环境部署i

为支持分布式编译,单一编译主机需要进行以下部署步骤:

  1. 安装支持包: 编译主机需要安装distcc、distcc-pump、dmucs包
    1
    
    apt-get install distcc distcc-pump dumcs
  2. 部署交叉编译工具链至特定位置:
    将android源码自带的prebuilt目录复制到固定地点,比如:/opt/。
  3. 修改distcc的配置文件:
    修改/etc/default/distcc,将

    STARTDISTCC设置为true;
    ALLOWEDNETS为连接的客户端范围,按需设置;
    LISTENER为侦听地址,设置为本机ip地址;
    JOB为同时可承担的编译任务数目,一般设置为CPU个数+2。
    修改/etc/init.d/distcc,将
    PATH参数加入步骤2的交叉编译工具链编译器位置,以使distcc在接到编译任务时,能找到对应的编译器。
  4. 启动distcc服务: sudo service distcc start

发出编译请求的客户端还需要进行如下部署步骤:

  1. 创建交叉编译工具至distcc的链接:
    将交叉编译工具全部链接至distcc:
    cd /usr/lib/distcc
    ln -s ../../bin/distcc arm-eabi-addr2line
    ln -s ../../bin/distcc arm-eabi-ar
    ln -s ../../bin/distcc arm-eabi-as
    ln -s ../../bin/distcc arm-eabi-c++
    ln -s ../../bin/distcc arm-eabi-c++filt
    ln -s ../../bin/distcc arm-eabi-cpp
    ……
  2. 修改android编译脚本,使其支持distcc编译:
    将build/core/combo/TARGET_linux-arm.mk中的TARGET_TOOLS_PREFIX变量改为/usr/lib/distcc/arm-eabi-
    修改build/core/definition.mk, 修改transform-d-to-p为:
    define transform-d-to-p
    $(hide) if [ -e $(@:%.o=%.d) ];then cp $(@:%.o=%.d) $(@:%.o=%.P); 	sed -e 's/#.*//' -e 's/^[^:]*: *//' -e 's/ *\\$$//' 		-e '/^$$/ d' -e 's/$$/ :/' < $(@:%.o=%.d) >> $(@:%.o=%.P); 	rm -f $(@:%.o=%.d);else cp $(notdir $(@:%.o=%.d)) $(@:%.o=%.P); 	sed -e 's/#.*//' -e 's/^[^:]*: *//' -e 's/ *\\$$//' 		-e '/^$$/ d' -e 's/$$/ :/' < $(notdir $(@:%.o=%.d)) >> $(@:%.o=%.P); 	rm -f $(notdir $(@:%.o=%.d));fi
    endef
  3. 修改环境变量:
    修改PATH,将distcc编译器路径加入PATH的第一个,并加入交叉编译器路径:
    export PATH=/usr/lib/distcc:<上述第二步交叉编译工具链位置>:$PATH
    此命令可写入profile。
    修改DISTCC_HOSTS,指明支持分布式编译的主机地址:
    export DISTCC_HOSTS=’localhost … … ‘
  4. 发起编译过程:
    make -j8 CC=distcc 根据编译池主机数和本机资源情况调整job数目,以获取最佳性能。

与ccache配合使用

因android官方已经在构建环境中加入了ccache支持,故可以很简单的打开ccache功能以提高编译效率。具体方法为:

$ export USE_CCACHE=1
$ export CCACHE_DIR=/path_of_your_choice/.ccache
$ prebuilt/linux-x86/ccache/ccache -M 20G

其中,20G为推荐值,事实上可以根据编译机器的具体情况进行调整。

在编译期或编译结束后,可以用“ccache -s”命令查看缓存命中情况。

用dmucs实现负载均衡

选中某台主机作为dmucs服务器。进行配置:

  1. 修改/etc/default/dmucs,启用dmucs:
    将SERVER改为yes
  2. 修改/etc/dmucs.conf 文件,配置编译池:
    根据文件原有说明格式,加入编译池中的主机。
  3. 启动dmucs服务:
    service start dmucs
  4. 在每个编译主机上执行负载发布客户端:
    loadavg -s 主机地址 &

此时,即可以在发起编译时使用

make -j8 CC=gethost distcc

来启用支持负载均衡的分布式编译过程了。

FAQ

  1. 执行pump时报python错误这是一个已知的bug,解决方法参见https://bugs.launchpad.net/distcc/+bug/511585
  2. 编译时pump报主机不支持cpp
    在创建环境变量时需要指明主机支持的编译类型如:
    export DISTCC_HOSTS=’localhost compilehost,lzo,cpp’

参考文献

  1. distcc官方网站:http://code.google.com/p/distcc/
  2. distcc官方文档:http://distcc.googlecode.com/svn/trunk/doc/web/index.html
  3. 《分布式编译环境中的负载均衡 》:http://www.ibm.com/developerworks/cn/aix/library/0905_yangyi_distcc/index.html
  4. dmucs官方网站:http://dmucs.sourceforge.net