今天的话题是,持续集成和“云”,主要部分是我之前两年的工作和我的一些个人思考。
这个话题我之前在中国的ruby大会上讲过,slides在这里 ,供参考,不过但是现在讲的内容根据最近多半年的工作进展又有所变化。
先自我介绍一下,我是软件工程师,从业13年,主要从事的领域都是应用系统开发,涉及OA、电信网管/增值业务、互联网等领域。
2009年底加入阿里,2015年4月离职,主要做的事情:广告应用系统 -> 运维自动化平台 -> 持续集成服务平台。
最后的持续集成服务平台是来自于实践需要,我最先在做广告业务系统的研发工作,广告系统虽然复杂,但是其中的应用系统从软件架构上看并没有什么特别的地方,所以希望将精力投在可以改进团队工作水平的地方。
一开始是一个运维自动化平台,由于团队人手有限,我基本是一个人做的,发现开发效率很好,软件质量也不错,所以在工作中总结了一些质量改进的实践,在团队中推广,这是我从研发进入QA的起点。
经过一段时间摸索,我们发现测试自动化搞不起来的原因之一是成本太高。
我之前习惯用ruby或者rails,所有的测试都可以单机完成,用cucumber这样的工具可以做BDD,用vagrant可以避免环境污染,所以自动化没问题。而java就没有这些条件了,数据库掌握在DBA手里,测试的linux是大家公用的,很容易引起冲突。
于是我和主管商量,决定搞一个平台,通过它降低研发成本,在本团队开展一段时间以后,又带着系统转到技术质量部,把CISE做大,测试部门成立了专门的团队,离职前已经开始在各bu和研发团队广泛运用
对CI的理解
持续集成平台究竟解决什么问题呢?
简单说,就是两个自动化:
- 构建自动化
- 测试自动化
测试自动化好理解,构建自动化比想象中要复杂一些,我一般用下面这张图来解释——
如图,我之前感到java开发的痛苦,就源于工程师手中没有更上层的“武器”,所以只能在单测上用力。
所以,如果我们能做好更大尺度系统的自动化的构建,那么研发人员也就有机会使用“高层”的自动化测试,而避免在细节上写太多的用例。
但这个并不容易,我们的努力也只是起到了一部分作用。
#p#
平台介绍
下面说一下这个平台本身,由于涉及到阿里巴巴内部系统,有些是不能说的,不过还好,核心部分并没啥技术含量 :-)
很多人对CI的了解是基于jenkins,当然也有些人接触过travis CI或者circle CI,我们的系统更像后者,当然功能上要更强些。
平台的起点是公司的gitlab和svn服务,通过自动监控或者hook触发,每一次代码提交都会触发一个自动化过程,在这个过程中,平台负责分配虚拟机、数据库等必要资源,然后将代码编译打包构建运行。
编译打包构建运行看起来是一个自动化过程,但每个环节实际上都可以有验证——这其实就是各种测试。
- 编译前可以做代码扫描(有的语言是编译后做代码扫描)
- 编译之后可以做单测
- 打包运行后可以做集成测试
- ......
和travis不同的是,如果目标涉及多个应用,之间存在服务调用,那么我们还会自动的将相关应用也部署好,然后在一个机器群里面做更接近真实场景的功能测试。
做过类似工作的同学一定知道这一点的代价有多大,但这样会有很大好处——我们可以在一次commit后自动进行所有层面的测试——从单测到系统交付测试。
当然这是理论上的,实际中可以根据研发团队需要进行选择。
总结一下,我们认为,CI平台应该能进行所有粒度的测试,最小针对函数,最大可以针对分布式系统。作为前提,CI平台需要支持整个系统的自动化构建。
这个大概是和travis CI之间最大的不同,下面再列几个不是很重要的区别。
1.使用云平台解决虚机问题,身在阿里巴巴,所以我们使用阿里云的ECS,需要资源是随时申请,用过以后立即释放重置。
这样可以解决环境污染的问题(即使是java应用,有些团队也会留下本地文件操作,这些东西会导致测试不可重复)。
2.数据库自动分配,我们构建了mysql集群,不过不是一般意义上的那种协作集群,而是一个数据库池,让数据库和虚机一样随用随取,用后重置。
这样做是为了让java程序员也可以像rails的db migration一样可以用到干净的数据库。
对这个平台的介绍就这些,下面讨论一些经验教训。
经验教训
UI应该尽量轻
这个话题要和jenkins/hudson做对比,这两个系统我其实不是很熟,不过也知道它们都是很强大的自动化系统。但是,jenkins/hudson的最大问题是,它们的UI做的太多了,用户可以在UI上做很多事——很多和CI没关系的事情。
举个例子:
jenkins的任务是可以排队调度的,而对于CI来说,排队是什么意思呢?研发工程师养成持续小步提交代码这个好习惯以后,又硬生生由于资源不足而被迫等待,最终可能会放弃这个好习惯,实在是不划算。
我们的办法是——敞开供应,只要有代码提交就分配机器,当然有人会质疑,因为这会导致需要一个很大的资源后备池,不过这可能正是“云”时代的不同思考方式——在“云”的时代,资源的使用毛刺应该通过大规模后备池来抹平。
当然,滥用资源还是要避免的,只是我们认为需要“后置惩罚”,比如通过审计,找出资源消耗大户,打他的板子 :-P
当我们把所有的额外功能都剥掉以后,发现UI其实就一个作用——展现,因为CI需要的是完全的自动化,人工本来就不需要介入,只要最后被notify一下,或者偶尔过来在web上看看报表趋势什么的就够了。
CI是服务加最佳实践
我们理解的第二个经验就是,搞CI,是服务加最佳实践,所以一定要指导研发团队,而不能完全任由研发团队提要求,很多团队的工程师良莠不齐,对各种编程api比较熟悉,但是可能缺乏做事的好习惯,这时需要指导他们,当然前提是你也要对开发很了解才行,否则会被鄙视的 :-P
对CI来说,最大的常见麻烦是不写测试,这个一般是通过管理教育,比较简单。
另外一个隐藏的比较深——开发人员对系统的运行并不了解,比如开发的系统运行在linux上,但是基本的命令都不会。
这个问题我们遇到了挑战,有人认为这涉及到分工,研发工程师不应该了解线上,但是这个观点是有问题的。
因为很多bug和环境相关,如果开发人员不能在”真“的环境中尝试,一旦系统报错,很难不发生扯皮——这个扯皮可能发生在开发和测试之间,更可怕的是发生在线上,光定位就需要N多人参与,成本极大。
插件
最后是插件的问题,这个说来简单——插件能解决一些问题,不过插件设计之初实际上就限制了其使用,所以我总结的教训就是:先别急着做插件,想好了设计再动手。
#p#
更进一步
先进一步谈谈对CI平台职责的理解,然后结合这些理解最后说说“云”和docker的作用
CI平台的职责
关于CI平台的职责,我上次已经提过,基本上就是这些:
- 构建自动化:提供环境
- 测试自动化:提供平台
先说构建
构建工作主要产出两个东西:最终输出的软件包和待测的应用系统(有时后者会包含前者),这两个产出的核心要点有所不同。
最终输出的软件包大多数公司都会做的,重点是构建过程环境无关而且可重复,因此需要提供配管服务,比如最好有yum、npm、gem等软件包服务。
但是这里有个问题——间接依赖的软件包如何锁定的问题,ruby的bundle机制很不错,通过Gemfile.lock让依赖包都有明确的版本(而不是“最新版本”这种含糊的说法),但是maven就没有这样的支持,目前没有很好地办法,只能让构建号和软件包号建立关联,便于回溯。
而待测的应用系统是比较难的地方,它也需要配管系统支持,同时还需要资源的就绪能力,一个关键要点是——资源必须是隔离的,否则很难避免测试时互相干扰。
这个隔离应该做到什么程度呢?举个例子,我们为了进行系统联调,自动构建了一套涉及N个系统的集群,这个集群是为了进行自动的联调测试。而理论上我们如果需要,CI平台应该可以再构建另一个集群,各种架构细节和前一个集群完全相同,但这两个集群进行工作时应该互不影响。
这要求应用需要符合一些最佳实践的要求,比如12 factor里面说的不要硬编码ip地址等等
为什么要这样?因为持续集成是不断前进的工作节奏,一个人的工作有了阶段性结果后,需要进行验证,这种验证应该是独立的,如果和其他人共享测试环境,要么测试结果不稳定,要么变成我上次说到的排队,那就降低了效率。
这两张图说的就是这个意思,同一团队的两个程序员公用数据库进行测试,测试结果就会不稳定,同样,如果他们的应用依赖了另一个公共服务,那么测试依然不能稳定。
不知道第二种情况会不会难以理解,简单说就是那个公共服务也是有存储的,所以那个数据库里面的数据会造成干扰。
构建自动化比较复杂,而测试自动化相对来说简单一些,不过这里的重点是对各种测试的抽象和区分
根据上次分享的反馈,发现有相当多的人不太理解这张图。
我们把测试分为UT/FT/IT/ST等等,但其实它们可以抽象成一个东西——都是对某个软件单元的验证,区别在于单元的粒度
补充说明一下上面说的一些名词:
- UT:单元测试
- FT:功能测试
- IT:集成测试
- ST:系统测试
所谓的“上层用例变化慢“,是相对的,因为应用系统常常是需求一变,整个推翻,所以上层需求用例的变化常常带来下层大量用例的变化,而反之则未必.
考虑到需求和测试用例直接联系很紧密,我们可以认为需求用例的变动频率和测试用例的变动正相关。
当然,测试自动化还有一个要做的工作是对结果的分析汇总,主要是各种测试手段的输出千差万别,需要进行数据汇总,这个和普通的数据处理没啥区别,我也不是这方面的专家,就不献丑了。
但是,作为CI平台,对结果数据的分析汇总要建立在测试阶段的界定上,简单说就是要明确区分UT、IT等环节阶段,这是后续报表很重要的信息,不能小看。
#p#
CI职责讲完了,我下面想说一下自己对“云”在CI方面价值的理解——简单说就是标准化。
CI的自动化测试和普通测试一样,有天生就要面对的问题:
- bug确认(可重现)
- 代码覆盖
- 测试的真实性
先说bug确认的困难,在测试团队待过的人一般都能理解问题确认有多麻烦,很多的bug是辛辛苦苦发现的,结果被开发同学一句“环境不一样”就打发了。
所以测试非常需要标准的环境,而这正是“云”可以提供的,要是进一步考虑自动化测试,那是比普通测试更需要标准环境的场合,因为它是无人值守的,对意外的适应能力更弱。
代码覆盖也是测试的一个重要指标,几乎所有的开发语言和框架都有不止一个代码测试覆盖率统计工具,而代码覆盖其实是涉及到测试层次的,在上层测试一个系统,往往能够覆盖不少下层用例,如果能从多个层次测试系统,可以让工作事半功倍。
最后再说一下测试的真实性,这里是指对mock技术的使用。我们很多时候使用mock技术只有一个原因——对方系统太难打交道了,所以做个mock先绕开(有时需要模拟对端错误,这种情况还是需要mock的)。
但是真实情况下我们访问的不是白板方法,这么做的有风险,最后还是要联调,所以这种情况是把测试推后了,是转移矛盾而不是解决矛盾。
我之前做的CI平台,正是想通过云技术,可以相对低成本的构建“全部系统”,因为(借助我们的平台)有时候这个做法比mock要简单,更重要的是,这种做法肯定比mock要真实。
以上都是云的价值,也是docker的价值,不过docker有个独特的价值,就是有可能将测试甚至运维工作变成服务。
这里说的服务不是那种在公司里某个部门为其它团队提供的服务平台,那很容易模糊边界(比如开发要求测试帮忙等等)。
这里的服务是指成立公司,把这些工作变成business的东西
当然,这块其实不稀奇,我之前说过的Traivs CI、shippable、coding应该都在做,docker创业团队大多都在做这个,不过我想说的是——为什么这事变得可行了?
这是由于标准化,一个应用应该是个什么样子?在docker的语境中是比较一致的,这为用户和服务平台提供了相对简单的协作边界。
举个例子,这是我们平台自己的一个模块在进行自动化构建时写到描述文件(相当于 .tavis.yml)中的内容:
- prepare:
- exec:
- - yum remove taobao-jdk -q -y
- - yum install -b test ali-jdk -y -q
- - echo 'export PATH=/xxxxxx/bin:$PATH' >> /etc/bashrc
- - echo 'export JAVA_HOME=/xxxxxx/java' >> /etc/bashrc
- - yum install jemalloc -b current -y -q
- - yum remove mysql* ruby19 -y -q
- - yum install mysql-devel -y -q
- - yum install ruby21 -b test -y -q
- - echo /usr/local/lib >> /etc/ld.so.conf.d/ruby21.conf
- - ldconfig -v
- - gem sources -r https://rubygems.org/
这么一大坨,我不相信会有人能受得了,但是如果在docker中呢?
- prepare:
- exec:
- - docker build -t xxxxx .
这样还符合我之前说的原则——开发人员自己管理环境
在这种变化下,我觉得我们做的东西可以大幅度简化,以至于变为一个对外服务的business,根据这个想法,我自己做了一点简单的尝试,不过是用青云做的,录了两段很短的视频,在这里:
http://v.youku.com/v_show/id_XODAzNzgyMjUy.html
http://v.youku.com/v_show/id_XODAzNzgxOTky.html
我要讲的基本上就这些,今天应该没超时,最后关于docker上的CI再多说两句,提个想法——docker应该依靠但不依赖IAAS。
借助SDN,避免通过Docker来划分安全域:我看了一点最近docker大会的介绍,老实说,有一种观点是把vm废掉,我是不以为然的,所谓vm所带来的成本其实正是我们获得安全隔离的原因,这两者是一体的,我们直接拿来用vm做安全隔离最好,让docker解决安全问题应该是一条歧路。
深度依赖compose机制,建立联调/系统测试的构建标准:应该会有很多人和我想的一样——将compose.yml作为重要信息来源,外部的系统通过它理解应用间的联系,即使不使用 docker compose 这个软件,也应该遵循 compose.yml 的规范,这样我们不但能让单兵(容器)的外部边界清晰,还能让战阵(分布式系统)也能被管控系统理解和支撑。
深度依赖compose需要让compose.yml目的变得纯粹些,我知道compose描述的内容很容易变成“不同环境”,其实这个想法也许不正确,我认为一个git分支就对应一种场景——比如某些分支是用来开发局部功能的,它不需要系统测试和联调,而master必然要联调——所以不用在代码中留下多份 compose_xxx.yml ,而应该在不同分支上编写不同的 compose.yml ,其内容由分支维护者负责。
分享人李建业,前阿里巴巴员工(花名:李福),2002年本科毕业,之后一直从事软件开发,涉及办公自动化、电信网管/增值业务系统以及互联网;2009年12月加入淘宝的广告应用开发团队;从2011年底开始,关注软件研发本身,主要工作包括运维自动化系统和持续集成服务平台。
原文链接:http://dockone.io/article/470