返回 登录
0

使用Docker实现丝般顺滑的持续集成

  • 作者简介:蒋运龙,有容云高级咨询顾问。十年来混迹于存储、三网融合、多屏互动、智能穿戴、第三方支付、Docker等行业;经历过测试、运维、实施各岗位全方位的摧残,依然活跃在技术的风头浪尖。
  • 本文转载自《程序员》,谢绝转载,更多精彩,请订阅《程序员》

持续集成(Continuous Integration,简称CI)作为先进的项目实践之一,近年来逐渐受到国内软件公司的重视;但对于许多朋友来说,可能从未听说过持续集成这个词,抑或只是了解概念但并没有实践过。
什么是持续集成?它对软件开发有哪些好处呢?

持续集成的概念

随着软件开发复杂度的不断提高,团队开发成员间如何更好地协同工作以确保软件开发的质量已经慢慢成为开发过程中不可回避的问题。尤其是近些年来,敏捷(Agile)在软件工程领域越来越红火,如何能在不断变化的需求中快速适应和保证软件质量也显得尤其重要。

持续集成正是针对这类问题的一种软件开发实践:它倡导团队开发成员必须经常集成他们的工作,甚至每天都可能发生多次。而每次集成都是通过自动化的构建来验证,包括自动编译、发布和测试,从而尽快地发现集成错误,让团队能够更快的开发产品。

让我们以A项目为例描述一个普通团队是如何使用CI的:

首先,解释下集成:所有的项目代码都托管在SVN或者Git服务器上(以下简称代码服务器)。每个项目都有若干单元测试和集成测试。集成测试是单元测试的逻辑扩展:在单元测试的基础上,将所有模块按照设计要求组装成为子系统或系统进行集成测试。实践表明,一些模块虽然能够单独地工作,但并不能保证连接起来也能正常工作。一些局部反映不出来的问题,在全局上很可能暴露出来(关于单元测试及集成测试的详述,读者可以查阅相关文档)。

简单来说,集成测试就是把所有的单元测试跑一遍,以及其他能自动完成的测试。只有通过了集成测试的代码才能上传到代码服务器,确保上传的代码没有问题。集成一般指集成测试。

持续,显而易见就是长期对代码进行的集成测试。既然是长期进行,那么最好是自动执行,否则人工执行既没保证,而且耗人力。

基于此种目的,我们需要有一台服务器,它将定期从代码服务器中拉取,并进行编译,然后自动运行集成测试;并且每次集成测试的结果都会记录在案。
在A项目中,设定执行这个工作的周期是1天。也就是说服务器每天都会准时地对代码服务器上的最新代码自动进行一次集成测试。

持续集成的特点

  1. 它是一个自动化的周期性的集成测试过程,从拉取代码、编译构建、运行测试、结果记录、测试统计等都是自动完成的,无需人工干预;
  2. 需要有专门的集成服务器来执行集成构建;
  3. 需要有代码托管工具支持。

持续集成的作用

  1. 保证团队开发人员提交代码的质量,减轻了软件发布时的压力;
  2. 任何一个环节都是自动完成的,无需太多的人工干预,有利于减少重复过程以节省时间、费用和工作量;
  3. 笔者在实践的过程中总结出了以下心得:
  4. 代码越早push出去,用户能越早用到,快就是商业价值;
  5. 用户越早用到就越早反馈,团队越早得到反馈,好坏都是有价值的输入;
  6. 用户不反馈,说明我们做了用户不想要的东西(通过用例跟踪)或者市场没做好,能帮助产品市场人员调整策略;
  7. 代码库存越是积压,就越得不到生产检验,积压越多,代码间交叉感染的概率越大,下个release的复杂度和风险越高;
  8. 代码库存越多,workflow的包袱越重,管理成本越大。

了解了持续集成的优点,读者是不是有立马部署一套的冲动呢?然而,部署持续集成环境,并不简单。

从一个Java项目持续集成的典型场景为例。我们只要建立一个基于Jenkins + Maven + Git(SVN) 的持续集成环境,再加上持续集成所要求的测试和流程就可以大致运行。

但在搭建好一整套持续集成的流水线之后,却发现它并不像想象中丝般顺滑,甚至比在本地开发测试的效率还低。为什么会出现这样的情况?
目前常见的在持续集成过程中遇到的问题有如下几种。

【编译时依赖和运行时依赖】

从字面上不难理解这两种依赖。虽然编译时依赖通常也是运行时依赖,但并不能由此判定编译时依赖一定是运行时依赖。例如,在开发过程中需要某些提供API的Jar包,但在运行时可能需要的是具体API实现的Jar包。其次,被依赖的包会有它自身的依赖,这样的情况下,项目会对这些包产生间接依赖(运行时依赖),依此类推,最终形成一个依赖树。当项目运行时,这些依赖树上的包必须全部就位,否则项目无法运行。

Maven在POM中通过scope来判定依赖的类型,从而帮助开发和运维人员摆脱手动处理依赖树的工作,然而运行时所有依赖包最终是要安装到生产环境的,这部分工作Maven并不能自动完成。因此,一个常用方式是将运行时所依赖的包拷贝到项目文件中,比如Java Web应用的WEB-INF/lib,然后将项目全部打成一个包。在安装项目包后,修改环境变量,将这些依赖包所在的路径加入相应的环境变量中,如ClassPath。

再举个典型的例子,目前的操作系统和其他系统框架都考虑到了运行时依赖树的处理问题,比如Ubuntu的apt-get,CentOS的yum,Ruby的RubyGem,Node的npm等等,在安装某个软件包时,系统会自动将所需的依赖包按顺序下载并安装。

【依赖时的复杂度】

项目除了对程序包的依赖,对于运行环境也有些具体的要求。比如,Web应用需要安装和配置Web服务器、应用服务器、数据服务器等,企业应用中可能需要消息队列、缓存、定时作业,或是对其他系统以Web Service或API的方式暴露服务等。这些可以看成项目在系统层面对外部的依赖。这些依赖有些可以由项目自行处理,而有些则是项目无法处理的,比如运行容器,操作系统等,这些是项目的运行环境。

总之,依赖的复杂度主要有两个:

  1. 依赖包间的版本兼容性问题。兼容性问题是软件开发者的噩梦。
  2. 间接依赖,或多重依赖问题。例如:A依赖于Python 2.7,A还依赖于B,但B却依赖于Python 3,苦逼的是Python 2.7和Python 3不兼容。依赖中最痛苦的事莫过于此。

【不一致的环境】

简单项目中,开发和运行环境都由开发人员搭建,当公司变大时,系统的运行环境将由运维人员搭建,而开发测试环境如果由运维人员搭建则工作量太大,由开发人员自己搭建则操作复杂又容易产生不一致的情况。假设公司为项目A和项目B开发了新版本。但环境和软件升级不是同步进行,出错的可能性非常大(想一想间接依赖和多重依赖的情况)。大家对这样的场景有没有印象:当新版本部署时,发现问题,测试或部署人员说:“版本有问题,无法运行!”开发人员却说:“我这里没问题啊,运行正常!”

【泛滥的部署】

如果项目简单,没有任何历史项目和代码的拖累,且各项目之间也没有任何的关联,只需进行资源方面的管理:分配机器,初始化系统,分配IP地址等。各个项目的运行环境、数据库、开发环境等都由具体项目的开发人员手动完成,这样的环境出问题怎么办?很简单,凉拌——重装系统。

理想很丰满,现实很骨感,历史遗留问题往往对现在的项目有很大影响:多语言(Java,PHP、C ),多系统(各种Windows、Linux),多构建工具版本(Java7、8),各种配置文件和各种黑科技补丁脚本散落在系统的各个角落,没人能找得到,也没人搞得懂。配置被谁改掉了,服务宕掉了,根本无从管理。

问题分析完了,我们又该如何克服这些问题,使用什么工具和方法,从而达到丝般顺滑的持续集成呢?这就是接下来要介绍的主角:Docker、AppoSoar及AppHouse。

Docker如今在国内已经如火如荼,Docker可以做到一次构建,处处运行;各种实践和生产部署也纷纷上马,Docker的基本知识和好处笔者也就不在这里科普了。

AppSoar,以Docker为基础,以企业应用为导向,以安全稳定为目标,打造专业、简单易用的企业级容器云解决方案。它提供了企业应用商店、友好图形化界面管理、多环境管理、混合云支持、容器持久存储、容器网络模式、容器可扩展性、容器负载均衡与调度、API接口支持、系统高可用等一系列强大功能。

AppHouse为容器运行平台提供了一种集镜像管理、镜像安全、镜像高可用及镜像高速访问为一体的企业级镜像仓库管理方案。AppHouse支持HTTPS访问;基于角色的权限控制;系统高可用;部署灵活;一键安装,一键升级;友好的图形化界面,LDAP/AD用户集成;支持swift/Glusterfs/公有云存储接入;提供丰富的API接口;支持连接Git源码仓库,代码自动构建;提供webhook接口,支持持续部署。解决了docker build、push、pull的一揽子问题。

言归正传,既然为了达到顺滑的持续集成要求,那么为什么选择用Docker、AppSoar、AppHouse来部署?

上面我们提到了传统方式搭建持续集成平台经常遇到4个问题:编译和运行时依赖、依赖时的复杂度、不一致的环境和泛滥的部署,接下来我们来分析下Docker+AppSoar+AppHouse组合如何解决这些问题。

首先,Docker可以让你非常容易和方便地以“容器化”的方式去部署应用。 它就像集装箱一样,打包了所有依赖,再在其他服务器上部署很容易,不至于换服务器后发现各种配置文件散落一地,这样就解决了编译时依赖和运行时依赖的问题。

其次,Docker的隔离性使得应用在运行时就像处于沙箱中,每个应用都认为自己是在系统中唯一运行的程序,就像刚才例子中,A依赖于python 2.7,同时A还依赖于B,但B却依赖于Python 3,这样我们可以在系统中部署一个基于Python 2.7的容器和一个基于Python 3的容器,这样就可以很方便地在系统中部署多种不同环境来解决依赖复杂度的问题。这里有些朋友可能会说,虚拟机也可以解决这样的问题。诚然,虚拟化确实可以做到这一点,但是这需要硬件支持虚拟化及开启BIOS中虚拟化相关的功能,同时还需要在系统中安装两套操作系统,虚拟机的出现是解决了操作系统和物理机的强耦合问题。但Docker就轻量化很多,只需内核支持,无需硬件和BIOS的强制要求,可以轻松迅速地在系统上部署多套不同容器环境,容器的出现解决了应用和操作系统的强耦合问题。

正因为Docker是以应用为中心,镜像中打包了应用及应用所需的环境,一次构建,处处运行。这种特性完美解决了传统模式下应用迁移后面临的环境不一致问题。

同时,Docker压根不管内部应用怎么启动,你自己爱咋来咋来,我们用docker start或run作为统一标准。这样应用启动就标准化了,不需要再根据不同应用而记忆一大串不同启动命令。

基于Docker的特征,现在常见的利用Docker进行持续集成的流程如下:

  1. 开发者提交代码;
  2. 触发镜像构建;
  3. 构建镜像上传至私有仓库;
  4. 镜像下载至执行机器;
  5. 镜像运行。

其基本拓扑结构如图1所示。

图1   利用 Docker 进行持续集成基本拓扑结构

图1 利用 Docker 进行持续集成基本拓扑结构

熟悉Docker的朋友都知道,Docker启动非常快,可以说是秒启。在上述的五步中,1和5的耗时较短,整个持续集成主要耗时集中在中间的3个步骤,也就是docker build、docker push、docekr pull这样还是无法达到顺滑的极致要求,下来我们来分析下build、push、pull的耗时和解决方法:

docker build

  1. 网络优化
    dockerhub的官方镜像在国外,由于众所周知的原因,在国内进行构建时网络会是很大的瓶颈,甚至某些公司的环境是无Internet连接的。
    在这种情况下,建议使用国内的镜像源,或者自己搭建私有仓库,保存项目需要的基础镜像,把构建过程中的网络传输都控制在国内或者内网,这样就不用再考虑网络方面的问题。
  2. 使用 .dockerignore文件
    dockerignore文件的设计是为了在docker build的过程中排除不需要用到的文件以及目录,目的是为了docker build这个过程可以尽可能地快速高效以及构建出来的image没有多余的“垃圾”。
  3. 最小化镜像层数(layers)
    把镜像层数减到最少,能加快容器的启动速度,但是这里也要权衡另一个问题:dockerfile的可读性。你可以把一个dockerfile写得很复杂以达到构建出最小层数的镜像,但同时你的dockerfile可读性也降低了。所以我们要在镜像层数和dockerfile可读性之间做出妥协。

docker push

docker registry升级到v2后加入了很多安全相关检查,在v2中的镜像的存储格式变成了gzip ,镜像在压缩过程中占用的时间也比较多。我们简单分解一下docker push的流程。

  1. buffer to disk,将该层文件系统压缩成本地的一个临时文件;
  2. 上传文件至registry;
  3. 本地计算压缩包digest,删除临时文件,digest传给registry;
  4. registry计算上传压缩包digest并进行校验;
  5. registry将压缩包传输至后端存储文件系统;
  6. 重复1-5直至所有层传输完毕;
  7. 计算镜像的manifest并上传至registry重复 3-5。

这样的设计导致push会很慢,如果采用官方的dockerhub,需要考虑docker build一节中提及的网络方面影响,dockerhub公有镜像库还需考虑安全方面的因素。

同时docker和registry设置了过多的安全防范措施(如双向证书认证等),主要是为了防止在公有云的环境下镜像的伪造和越权获取。但是在一个可信的环境内,如果build和push过程都是自己掌控,很多措施都是多余的。

docker pull

docker pull 镜像的速度对服务启动速度至关重要,好在registry v2后可以并行pull了,速度有了很大改善。但是依然有一些小的问题影响了启动的速度:

  1. 下载镜像和解压镜像是串行的;
  2. 串行解压,由于v2都是gzip要解压,尽管并行下载了还是串行解压,内网的话解压时间比网络传输都要长;
  3. 和registry通信, registry在pull的过程中并不提供下载内容只是提供下载url和鉴权,这一部分加长了网络传输,而且一些metadata还是要去后端存储获取,延时还是有一些的。

通过刚才的分析,大家可以看到,其实docker build、push、pull其实主要耗时是在网络传输(主要)及安全防范措施(轻微)上,整个传输过程甚至大大超过了其他所有步骤的时间;这样可以借助我们的AppHouse方便的搭建本地企业级镜像仓库,将网络传输转移至内网,同时完全掌控了 build、push和pull的过程,这样提高效率的同时也解决了安全问题,可谓一举两得。

经过Docker、AppHouse的帮助,我们距极致追求的如丝般顺滑的持续集成目标只有一步之遥,Docker解决了依赖和环境问题,AppHouse解决了镜像安全快速传输的问题,接下来就是容器的部署和管理问题。

Docker实现了底层技术的创新,它的出现将开发者从与系统的纠缠中释放了出来,但是阻碍企业使用Docker的问题是容器的大规模部署、管理问题和缺少企业级容器工具及系统。

镜像创建完成后,需要把它发布到测试和生产环境。因为Docker占用资源小,在单个服务器上部署成百上千个容器也不足为奇。这个阶段中如何更合理地使用Docker也是一个难点,开发团队需要考虑如何打造一个可伸缩扩展的分发环境。

AppSoar提供人性化的Web管理界面,丰富的Compose文件格式和功能完备的API接口,通过Compose实现以十分简单的文件描述复杂的应用结构,让部署变得更简单。并且,AppSoar还提供丰富的企业应用商店,让在一键创建服务成为可能。这样可以快速搭建应用场景,开发者只需要关注开发本身即可。
打通最后一个环节后,整个持续集成平台架构演进到如图2所示。

图2   整体持续集成平台架构演进

图2 整体持续集成平台架构演进

总结

通过Docker+AppSoar+AppHouse的组合,开发团队面对复杂的环境时,可以结合自己团队的实际情况,定制出适合自己的方案,从而打造出一套如丝般顺滑的持续集成系统。

评论