【编者的话】今天的话题是,持续集成和“云”,主要部分是我之前两年的工作和我的一些个人思考。
这个话题我之前在中国的ruby大会上讲过,slides在这里 ,供参考,不过但是现在讲的内容根据最近多半年的工作进展又有所变化。
先自我介绍一下,我是软件工程师,从业13年,主要从事的领域都是应用系统开发,涉及OA、电信网管/增值业务、互联网等领域。
2009年底加入阿里,2015年4月离职,主要做的事情:广告应用系统 -> 运维自动化平台 -> 持续集成服务平台。
最后的持续集成服务平台是来自于实践需要,我最先在做广告业务系统的研发工作,广告系统虽然复杂,但是其中的应用系统从软件架构上看并没有什么特别的地方,所以希望将精力投在可以改进团队工作水平的地方。
一开始是一个运维自动化平台,由于团队人手有限,我基本是一个人做的,发现开发效率很好,软件质量也不错,所以在工作中总结了一些质量改进的实践,在团队中推广,这是我从研发进入QA的起点。
经过一段时间摸索,我们发现测试自动化搞不起来的原因之一是成本太高。
我之前习惯用ruby或者rails,所有的测试都可以单机完成,用cucumber这样的工具可以做BDD,用vagrant可以避免环境污染,所以自动化没问题。而java就没有这些条件了,数据库掌握在DBA手里,测试的linux是大家公用的,很容易引起冲突。
于是我和主管商量,决定搞一个平台,通过它降低研发成本,在本团队开展一段时间以后,又带着系统转到技术质量部,把CISE做大,测试部门成立了专门的团队,离职前已经开始在各bu和研发团队广泛运用
对CI的理解
持续集成平台究竟解决什么问题呢?简单说,就是两个自动化:
- 构建自动化
- 测试自动化
测试自动化好理解,构建自动化比想象中要复杂一些,我一般用下面这张图来解释——
我们手里的软件,总是可以进行不断地分解,从系统到模块,最后到类和方法。由于整体和部分的功能不能完全划等号,所以测试需要在各个层面上进行,但是这些测试的成本和收益有所不同。
如图,我之前感到java开发的痛苦,就源于工程师手中没有更上层的“武器”,所以只能在单测上用力。
所以,如果我们能做好更大尺度系统的自动化的构建,那么研发人员也就有机会使用“高层”的自动化测试,而避免在细节上写太多的用例。
但这个并不容易,我们的努力也只是起到了一部分作用。
平台介绍
下面说一下这个平台本身,由于涉及到阿里巴巴内部系统,有些是不能说的,不过还好,核心部分并没啥技术含量 :-)很多人对CI的了解是基于jenkins,当然也有些人接触过travis CI或者circle CI,我们的系统更像后者,当然功能上要更强些。
平台的起点是公司的gitlab和svn服务,通过自动监控或者hook触发,每一次代码提交都会触发一个自动化过程,在这个过程中,平台负责分配虚拟机、数据库等必要资源,然后将代码编译打包构建运行。
编译打包构建运行看起来是一个自动化过程,但每个环节实际上都可以有验证——这其实就是各种测试。
- 编译前可以做代码扫描(有的语言是编译后做代码扫描)
- 编译之后可以做单测
- 打包运行后可以做集成测试
- ......
和travis不同的是,如果目标涉及多个应用,之间存在服务调用,那么我们还会自动的将相关应用也部署好,然后在一个机器群里面做更接近真实场景的功能测试。
做过类似工作的同学一定知道这一点的代价有多大,但这样会有很大好处——我们可以在一次commit后自动进行所有层面的测试——从单测到系统交付测试。
当然这是理论上的,实际中可以根据研发团队需要进行选择。
总结一下,我们认为,CI平台应该能进行所有粒度的测试,最小针对函数,最大可以针对分布式系统。作为前提,CI平台需要支持整个系统的自动化构建。
这个大概是和travis CI之间最大的不同,下面再列几个不是很重要的区别。
使用云平台解决虚机问题,身在阿里巴巴,所以我们使用阿里云的ECS,需要资源是随时申请,用过以后立即释放重置。
这样可以解决环境污染的问题(即使是java应用,有些团队也会留下本地文件操作,这些东西会导致测试不可重复)。
数据库自动分配,我们构建了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多人参与,成本极大。
插件
最后是插件的问题,这个说来简单——插件能解决一些问题,不过插件设计之初实际上就限制了其使用,所以我总结的教训就是:先别急着做插件,想好了设计再动手。问答
Q1:没有更上层的工具表示什么?A1:指的是开发只能写单测,集成测试和系统测试需要条件很多,所以开发没法做。
Q2:主要进行的是程序的功能测试还是什么?
A2:后面提到了,包括从单测到系统验收测试的所有层面
Q3:能详细介绍一下mysql的集群吗,能够提供原生的那种mysql体验吗,比如说外键关联,多表查询?
A3:这个地方被你想复杂了,可以认为就是准备了一堆mysql server,用的时候把ip、user、password、port设定到应用里面,放开用就是了,完全就是普通的mysql。
Q4:gitlab 和svn 启动监控是什么原理,不是很清除?
A4:这个问题和主题关系不大,基本上就是利用svn和gitlab本身的hook或者web hook机制。
Q5:你的slide-27提到的数据库克隆,和保留故障现场,具体是指?如何做到保留现场呢?
A5:这块有点特别,有些测试是在测试代码中写好测试数据,比如rails的fixtures机制,但是不适合针对大数据量,某些场合的大数据量测试需要预先准备好一个基准数据库,然后我们调用脚本,在mysql的那台服务器上直接copy数据文件准备一个新的mysql数据库,数据和基准库一样,这样可以应对10G以内的功能测试场景。
Q6:服务之前存在依赖,定义服务状态已运行,才能进行测试?
A6:我们会要求开发人员编写运行的脚本,最简单是 mvn run ,复杂的就没边了,服务间依赖让开发自己解决,这也是我之前说过”业务团队需要适应“这一原则的体现。
Q7:系统测试包括前端的集成测试?由于现在的应用前后端都分离了,UI考虑了各种平台,特别是移动应用又是如何测试的呢?
A7:我们之前的工作对移动测试不涉及UI部分,当然web有UI部分,这时需要驱动windows,这块有些现成的做法,当然毕竟不如linux的控制那么流畅
Q8:基于maven构建的系统,特别是有服务依赖的情况,又是如何集成测试的?
A8:和Q6问题类似。
Q10:请问你们的集成测试也是自动化的吗?
A10:是的,但是不同团队的技术成熟度不同,执行情况也不同,最简单的是仅作单测,就是和travis ci一样。
Q11:数据库初始化和累计的补丁是如何管理和部署的?
A11:起初要求团队提供,后来发现数据库的建表语句都掌握在公司DBA团队手里,我们就和他们合作,通过DBA的系统获取当前线上的数据库建表脚本,然后允许团队提交变更sql和初始化sql,这样做带来一个额外的好处——可以顺手测试数据升级脚本了,很棒。
Q12:有的本次提交不能立即上线,有的本次提交却希望立即上线,这种差异如何满足呢?
A12:这是个需要研发团队改进做法的地方,可以参考各种敏捷实践的做法。
Q13:自动化测试除了测试API么,还需要测试什么,结合容器我们又该测试什么?
A13:全都要测试,简单说CI就是尽量避免人的介入,提升管理闭环的运行效率。至于和容器的结合,我下次会提到一部分,但是这块其实是一个待开垦的领域,值得大家一起想。
Q14:容器怎么做应用性能测试? 性能指标采集值往往是宿主机的,另外还要防止应用滥用过多资源。
A14:我们还没涉及性能测试,这块有些特殊。
Q15:我想问一下测试用例和开发的接口是对应的吗,中间涉及到远程方法调用吗?
Q16:测试代码的力度如何把握呢?测试代码写起来也是很费时间的,有时比写业务代码还费劲。
A15-16:这两个问题都是涉及测试如何做的了,恐怕不是一句话可以说完的。
Q17:2. 数据库自动分配,我们构建了mysql集群,不过不是一般意义上的那种协作集群,而是一个数据库池,让数据库和虚机一样随用随取,用后重置。
这样做是为了让java程序员也可以像rails的db migration一样可以用到干净的数据库“,这段不好理解。
A17:应用系统的测试常常需要涉及数据访问,rails的做法是用command模式提供undo功能,或者借助fixture技术,但是java工程师普遍没有意识到数据需要清理,所以我们”连根拔“,直接给一个空的数据库,用完就回收,所以不会有残留测试数据。
Q18:持续集成考虑到系统升级问题,比如数据库升级(某表增加字段,某表删除字段,新增表等),这种情况做持续集成,你们是怎么应对的?
A18:参考A11。
Q19:持续集成过程中,应用的升级的补丁包你们如何管理的,比如svn diff,git diff之类,产生的文件,如何适应持续集成呢。
A19:这是版本控制系统的责任,我们拿到的都是revision对应的完整代码树。
Q20:继续集成这套代码也需要维护起来 阿里怎么维护这套代码的?
A20:我们的系统本身也是用自己的系统进行CI的 :-)
~~~~~~~~~~~~~~~~~~~~~~~分割线,以下是下半场周四分享内容~~~~~~~~~~~~~~~~~~~~~
周二进行的分享,重点对我们做的持续集成平台进行介绍,这次重点会在对CI本身。先进一步谈谈对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等环节阶段,这是后续报表很重要的信息,不能小看。
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,根据这个想法,我自己做了一点简单的尝试,不过是用青云做的,录了两段很短的视频,在这里:
这样还符合我之前说的原则——开发人员自己管理环境
我要讲的基本上就这些,今天应该没超时,最后关于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 ,其内容由分支维护者负责。
问答
Q1:使用compose slave作为局部开发?A1:说的是最后一段吧?"在不同分支上编写不同的compose.yml",这是针对之前和daocloud的同学交流的考虑结果,允许在代码中保留多份 compose 配置文件会严重消弱系统的简化程度,应该统一成一份compose.yml ,开发的需求其实是在分支开发中出现的特殊场景,所以在他的分支中改变compose.yml的内容即可。
Q2:假如开发依赖平台,而平台又在不断变动,另外模块间还存在接口调用。请问这种情形如何CI?
A2:这里的“平台”在CI系统看来没有任何特殊地位,其实就是有两个系统在同时演化,而他们之间有依赖而已。理论上应该各自演化,同时盯住对方的master分支(即svn得trunk),然后每次commit时把两个系统都构造出来进行测试。
Q3:请问slave分支要合回master分支的话,怎么解决slave分支的compose.yml也合到master分支的问题?
A3:当然是保留master上的那份了,如果全过程是CI驱动,那么可以由CI平台“自动的“的保留master的compose.yml文件,有的团队在代码合入时需要人工背书,这时就人工保留了。
以上内容根据2015年6月23日和25日晚微信群分享内容整理。分享人李建业,前阿里巴巴员工(花名:李福),2002年本科毕业,之后一直从事软件开发,涉及办公自动化、电信网管/增值业务系统以及互联网;2009年12月加入淘宝的广告应用开发团队;从2011年底开始,关注软件研发本身,主要工作包括运维自动化系统和持续集成服务平台。 DockOne每周都会组织定向的技术分享,欢迎感兴趣的同学参与。
有疑问加站长微信联系(非本文作者)