这几年,持续集成随着敏捷在国内的推广而持续走热,与之相伴的持续部署也一直备受关注。自前两年,持续交付这个延续性概念又闯进了国内IT圈,慢慢开始在社区和会议中展露头角。许多不明真相的群众跟风哭着喊着要“上”,而许多前CI的半吊子玩家换件衣服就接着干,有的甚至衣服都来不及换……国内的这些土财主如果不巧请了某些所谓的战略家,除了建了一堆持续集成环境,以及每天嚷嚷着要这个要那个,混乱的状况在根本上没有得到改善。本文无意费力探讨持续集成和持续交付的概念,而是打算谈谈对于大型软件企业,以持续集成为基础实现持续部署(交付)时,所要面对的问题以及可行的解决方案。地主老财们,夜黑风正猛,山高路又远,注意脚下……
And God Said, Let there be light: and there wa— GENSIS, Charpter
1, King James
一、起步
先来讲个故事……
几年前,一对留美的夫妇通过朋友找到我,让我帮忙在国内组建一个开发团队,该团队负责为其开发一款基于社交网络的客户关系管理软件,(暂且称之为项目A)。这个项目除了尚不清晰的需求范围和很紧的期限外,作为业内人士的老公Richard根据眼下流行的软件开发过程还提了诸多额外的要求:
● 功能要及早交付(以便拿去和潜在的投资人洽谈)
**● 功能在部署到生产环境前要先部署的一个测试环境**(Richard要试用后给予反馈)
**● 功能必须经过测试**(长期作为软件外包的甲方,对质量要求严格)
**● 要减少后期维护的工作**(美国人精贵,少雇一个是一个)
**● 支持协同开发**(以便维护人员及早介入)
● ……
这正是持续集成所要解决的典型场景。针对Richard的要求,我们只要建立一个基于Hudson(现在叫Jenkins)+Maven
+SVN
的持续集成环境(再加上持续集成所要求的测试和过程)就可以很好地满足上述要要求,此方案的结构如下:
对于上述方案,让我们近距离看看各个服务器的内部情况,以及人员在这种方案下的分工协作:
我们先谈谈上面的图中涉及的一些概念性问题:
1.1)编译时依赖和运行时依赖
从字面上不难理解这两种依赖的类型。但要注意虽然编译时依赖常常也是运行时依赖,但并不能推断出一方必然是另一方。比如,在开发的过程中需要某些提供API的Jar包,而运行时可能是具体API实现的Jar包。再者,被依赖的包会有其自身的依赖,因此,项目对这些包产生间接依赖(运行时依赖),依此类推,最终形成一个依赖树。当项目运行时,这些依赖树上的包必须全部就位。
Maven在POM中通scope来界定依赖的类型,从而帮助开发和运维人员摆脱手动处理依赖树的工作,然而运行时所依赖包最终是要安装到生产环境的,这部分工作Maven并不能自动完成。因此,一个常用方式是将运行时所依赖的包拷贝到项目文件中,比如Java
Web应用的WEB-INF/lib,然后将项目总的打一个包。在安装项目包后,修改环境变量,将这些包所在的路径加入相应的环境变量中,如ClassPath。
再看个例子,现代的操作系统和其它系统框架都考虑到了运行时依赖树的处理问题,比如Ubuntu的apt-get,CentOS的yum,Ruby的RubyGem,Node的npm等等。
1.2)依赖时的复杂度
项目除了对程序包的依赖,对于运行环境也有些具体的要求,比如,Web应用需要安装和配置Web服务器,应用服务器,数据服务器等,企业应用中可能需要消息队列,缓存,定时作业,或是对其它系统以Web
Service方式暴露的服务。这些可以看做项目在系统层面对外部的依赖。这些依赖有些可以由项目自行处理,而有些则是项目无法处理的,比如运行容器,操作系统等,这些是项目的运行环境。
总之,依赖的复杂度主要有两个:
1. 依赖包间的版本兼容性问题。兼容性问题是软件开发的恶梦
2.间接依赖,或多重依赖问题。这个问题可以类比想像一下C++中的多重继续种出现的很多问题。
比如:A依赖于python 2.7,A还依赖于B,但是B却依赖于python 3,而Python
2.7和Python 3不兼容。这是依赖中最恶心的事。
1.3)任务分工
由于项目简单,因此并不需要专门的运维人员。以一个100人左右以交付为主业(恩,就是做外包)的公司为例,由于没有任何历史项目和代码的拖累,且各个项目间也没有任何关联,故而只需要配备一个IT支持人员进行资源方面的管理:分配机器,报修,初始化系统,分配IP地址等。各个项目的运行环境、数据库、开发环境等都由具体项目的开发人员手动完成。
环境出问题怎么办?很简单,凉拌——重装系统。实际的运行效果不错。
1.4)自动化部署
由于Hudson这样的持续集成环境提供了自动编译(定时或触发式)的功能,而且可以在编译过程中提供了一些扩展点,因此通过提供一个部署用的脚本,就可以非常容易实现简单的自动化部署。
毫无疑问,持续集成就是敏捷的魔法药,它见效快、副作用小、业界的争论少。每每运用在混乱的项目中时,几周内项目就开始持续的产出经过测试的功能。对于独立项目,以持续集成为中心的持续部署绝对是不二选择。
但是,我们有没有想过,这会是一个自动化部署的通用解决方案吗?持续集成应该位于持续交付的中心吗?
二、困境
回到我们的故事:项目A上线两年后,运营业绩不错,投资人第一轮注资后,Richard的公司进行了扩张,他们对项目进行了重构,而且随着用户数量的增长,公司分别在美国、英国和日本等地建立了运营中心,并且对亚洲市场进行的定制功能开发(项目A+),接下来,公司又投入开发了团购系统(项目B)。在获得了新一轮投资后,各条本来比较简单的业务和功能线上越来越复杂,需要不断地细分,于是公司再度扩张(开发人员达到了300人,国内200多人,而运维团队主要在美国),随后又为项目A/A+的高级用户开发了问答系统(项目C)。目前,他们正准备开发手机系统。
看看下面的图,公司增长的过程中,整个项目环境也变得复杂。(注意,这里是一种逻辑结构,而在物理层面项目B和项目A的生产环境可能部署在相同的机器上)。
同时,原本单一的项目软件结构随着业务系统的增加也不再简单: {width=”503”
height=”274”}
而软件间的版本依赖使这个问题变得更为复杂:
现在,Richard的公司已经不再是一条快乐的小鱼,而是渐渐成为一直庞大的巨兽。虽然只有四个产品,但公司却要支持几百台开发机,几十台生产服务器,还有对应的测试环境,数据库服务器,以及几十个开发小组,和一大堆的内部项目。我们尽可以使用持续集成来为我们完成自动化部署。但,当我们为各个项目建立起持续集成环境后,它能满足我们对于持续部署的要求吗?我们前期的工作可以简化我们今后项目的持续交付的工作的难度吗?它需要我们为之建立一个庞大的运维团队,还是可以让我们能节省下每一毛钱来投入到真正的业务价值中去?
让我们先来看看复杂的项目环境中的几个场景:
场景1:环境升级
项目A和项目B都依赖于Web容器,公司决定升级Web容器版本,而公司要升级的机器有上百台,依赖人肉升级已不现实,维护团队因此针对各种软件开发了相应的自动化脚本,但当新的软件出现时,必须要开发新的脚本。而且当同时升级若干环境软件时,则难度随之增大,手工调度的方式极易出错,当升级失败时仍需要大量人工处理。由于存在大量升级脚本,有一定的维护成本。
场景2:依赖于环境的软件升级与回滚
针对环境升级,公司为项目A和项目B开发了新的版本。但环境的升级和软件的升级不是同步进行,出错的可能性非常大(想一想间接依赖和多重依赖的情况)。当新版本部署到生产系统时,发现问题,需要回滚到之前的版本——所有运行时版本都需要回滚,而且环境也需要同步回滚。几百台机器……
场景3:运行时依赖
在第一节的方案中,我们将所有的运行时依赖都打包到一起。当项目依赖关系复杂时,这样产生的包将非常臃肿,潜在地延长了部署的时间(想一想全世有几百台服务器,一个部署计划需要部署几百兆文件的情况),而且产生冲突的可能性非常大,而且对于不同类型的项目(Java和Ruby项目)缺乏通用性。06年左右,Nortel可是拿Excel统计过运行时依赖的,牵涉若干项目组,反复多次,没有个把月真搞不定。
场景4:泛滥的部署
每个项目相关的持续集成环境都需要开发自己的部署脚本,重复投入大,而且各个项目的部署过程不一致,并且对于同一个项目无法同时满足不同目的部署要求,例如,环境或系统配置参数改变后,无需安装包,只需做清理和激活的工作。最后,持续集成只是支持了和代码修改有关的部署。
场景5:不一致的环境
简单项目中,开发环境和运行环境都由开发人员搭建,当公司变大时,系统的运行环境将由运维人员搭建,而开发环境如果由运维人员搭建则工作量太大,由开发人员自己搭建则操作复杂又容易产生不一致的情况。
场景6:热切换
对于某些部署,需要尽量减少服务的停止时间,需要在服务的同时进行部署。
这些场景只是以持续集成为中心的持续部署在面对大型企业时所遇到的部分问题。大型企业,人多,项目多,机器多,项目环境复杂,部署维护工作繁多。以持续集成为基础的部署可以解决各个项目的集成问题,却无法帮助企业应对复杂的项目环境和各种不同的部署要求。究其更本,大型企业中的部署不再是一个简单的问题,而是一个交付生态圈,基础设施和环境管理必须要纳入考虑之中。要实现真正意义上的持续部署,我们就必须把环境和项目同等对待,通通纳入管理之中。同时,部署本身要得到统一。一个好的部署机制,应该是易于建立,易于使用,易于维护。
三、任脉——环境管理
什么是环境?
系统运行所依赖和包含的一切就是其环境:硬件、操作系统,网络资源(IP地址、域名),服务容器,服务器软件配置,环境亦是,运行时依赖的命令和包,项目本身的包和配置都是环境的一部分。对于部署而言,广义上,这些通通应该纳入环境管理的范畴,但狭义上,从软件系统的角度看,一个环境就是其运行需要的软件及其配置(我们先把操作系统和网络资源当做基础设施,其在部署时已处于就位的情况)。因此:
项目A的生产环境 = 项目A本身的软件包 + 项目A运行时依赖的软件包 +
项目A运行时依赖的其它软件 + 项目A的配置信息
由于,项目本身的软件包、项目运行时依赖的软件包,以及项目运行时依赖的其它软件在本质上没有区别——都是软件,上面的定义可以进一步抽象为:
环境 = 软件包 + 配置信息
在这个定义下,我们就必须将运行环境的软件解构,并以包的形式导入到公司的整个项目资源库中,比如Apache将作为一个包被导入,而Apache依赖的其它包也将依次被导入,并建立起正确的依赖关系。而且,在导入的过程中还必须做些相应的调整,如,环境变量的读取和设置,必须来自于环境配置模块,而不要修改系统的环境变量,防止不同环境在系统环境配置上相互影响和依赖。
再回头审视我们的示例,项目A的生产环境可以部署在不同的区域,对于各个区域可能有定制化的设定。这就像面向对象中的类,可以通过继承使子类重用父类的公有属性和行为并添加自己特有的信息。因此,环境的概念模型如图:
通过这样的关系,我们很容易为示例的复杂环境建立一种简单的结构,对于项目A:
这里,环境依然是处于知识层面(Knowledge
Level),它并未与具体的基础设施相关联。当我们将一个环境“具现化”成一个运行系统时,我们就产生了一个真正的环境实例。在这两者之间,我们还必须要考虑环境实例的使用目的(开发?测试?……)以及安装所依赖的其它信息(如机器),因此,我们需要增加一个环境目标来集中这些信息,而且由于不同目标的环境可能会有所差别,因此,环境目标也需要配置的能力。概念模型如图:
图中的环境实例是如何产生的呢?部署,一次部署可能会产生一个环境实例。一系列部署将产生对应于环境目标的多个环境实例,除去当前起作用的环境实例外(最新的),其它的是历史环境实例。通过在历史环境实例中切换,我们自然而然的就可以使整个环境回滚,因为项目所依赖的一切都已经成为的环境中的软件包,而且环境依赖的包的版本会随着部署具体确定下来。如此一来,我们便可以给每个环境实例分配一个版本号,再通过环境实例的版本号与软件包的版本对应起来,从而得知一次部署时应用的具体软件包,如图:
目前的环境管理结构,已经可以解决场景1、2和5的问题。那么对于场景2,运行时依赖,环境管理应该如何解决呢?
细心的朋友,可能已经发现,在环境层面上我们确定了环境依赖的软件包,这里有两个隐藏的含义:
● 环境定义的是对软件包的运行时依赖
● 由于环境是一个逻辑上的概念,因此其所用的软件包也是一个逻辑上的概念(相对于版本控制系统中的软件包)
我们也已经知道,在部署时,一个环境实例将具体的确定其依赖的软件包的版本。某个版本的软件包最终与代码库中的物理的软件包相关联。但软件包是运行时的安装包,因此,它应该是代码库中包编译的结果。在对代码库的包编译时,既要将结果打上版本保存起来,也好在两者的版本间建立关系,最后,编译结果应该是某种既定的安装包目录文件结构。
另外,当环境包含的包比较多时,运行时版本树会非常大,手动的指定全部的包的版本将是一个非常大的体力劳动,这部分工作也要得到简化。由此,我们必须
**● 建立逻辑软件包版本和版本库中软件包版本间的关系**
**● 为相互依赖的包编译并打上统一的标签**
**● 简化运行时包依赖关系的生产**
**● 简化运行时包依赖的指定(可参考apt-get和RubyGem,环境只需指定直接依赖的包,间接依赖的包从运行时依赖树中自动导入)**
一个可能的简单结构如下:
上述讨论还没有涉及操作系统,如果我们的运行机器要支持多个系统,我们又该怎么办???
配置信息也是个大问题,大家可以思考
**● 环境配置和应用配置如何区分?**
**● 如何简化环境配置工作?**
**● 如何使环境配置的效果只对具体环境有效,而不会泄露到环境外部?**
再者,
**● 如何使应用支持多运行目标?**
**● 环境管理如何能方便开发环境的调试?**
**● 要如何简化版本的选择?**
**● 在多个包有编译和运行时依赖时,编译时如何检查以减少引入兼容性问题的风险?**
这些都留待大家思考。
四、督脉——部署系统
《持续集成》和《持续交付》中都对部署有详细的讨论,不在赘述。在我看来,部署其就是按照其目的执行一系列步骤将环境置于其目的所指向的状态中。我们一会再回国头来看这段文绉绉的话,先看看第一部分持续集成的环境下,我们部署的步骤可能会是下面这个样子:
1.登陆目标机(ssh)
2.停止服务
3.清理环境
4.准备安装环境(创建文件夹等)
5.安装项目包(rsync,解压,权限设置等)
6.配置环境变量
7.启动服务
8…….
而在第二部分的情景4中,我们看到如果对不同的持续集成环境建立不同的部署脚本和环境维护脚本,这部署过程的维护会非常繁琐。基于第三部分的环境管理,我们可以将部署过程抽象为:
现在回到开头那个文绉绉的描述:部署其就是按照其目的执行一系列步骤将环境置于其目的所指向的状态中。
由于我们已经将部署作为环境管理的一部分,而环境又是对外提供服务的最小实体,因此,对环境的部署就是要根据部署的类型,在环境上按一定的步骤执行一系列操作,从而使环境置于部署类型所要的状态,这个过程中可能会生成对应的环境实例。举例来说,我们可能会修改环境相关的一些配置,然后重启环境,显然,这种情况下不需要下载安装软件包(没有改变),因此也就不需要生成环境实例。
对于标准的部署——安装软件包并启动环境,可能的步骤将会是:
1. 选择将要部署的软件包的版本
2.
生成新的环境实例(确定环境实例的版本和其依赖包的版本,确定环境配置等)
3.清理和准备目标机环境
4.下载包
5.设置环境配置
6.环境实例切换
7.生成部署报告
8…….
好,部署系统和环境管理各就各位,我们可以将各个项目环境纳入我们的环境管理之中,甚至是持续集成环境本身。再补充一句,要让部署系统和环境管理能很好的发挥作用,我们即需要一个简单一致的UI界面(为开发人员),也需要提供一个清晰明了的服务接口(供外部系统调用,如持续部署系统)。对于与环境管理相关的机器状态管理,网络资源的配置等等,本文不再涉及,大家可以自己思考。环境管理的实现、编译系统改造以及持续部署的具体实现,另作文章探讨。
就技术而言(不考虑围绕持续部署的过程实践),环境管理、部署系统以及我们没有提及的编译系统改造才是生产线的真正引擎,持续部署不过是水到渠成的传送带而已。
五、没完
打通了任督二脉后,事还还没有完,还有很多细节上的问题。你想,这个工具实在是太好用了,于是公司里成百上千的工程师们都在使用这个自动化部署系统,我们又会面对很多很多问题:
**● 部署系统的性能问题**。几百号人不停地在把他们的软件部署到自己的机器上,部署到测试环境,部署到生产环境,一天之内一个人可能会要部署N次,回滚N次,不但有大量部署请求,还有大量的文件在网络上传输。你得想想这套部署系统如何解决这些性能问题,还得考虑未来更大规模的性能水平扩展问题。
**● 目标机环境的管理。**在目标运行机上需要解决几个问题:1)两个环境间如果有一些的一样的包,那就没有必要再下载了,这样可以节约时间。2)每次部署都需要把老的部署环境给保留下来,这样方便在新旧环境下的切换。这两点对于在生产环境下部署非常关键。(这需要环境内所有软件的绿色安装才能更容易达到这个目标,因些,Unix/Linux会比Windows更容易做到这点)
**● 部署一致性事务问题**。有时候,我们需要同时部署若干台服务器,比如:包A到机器MA,包B到机器MB,包C到机器MC,……(Web
Service的SOA架构),这些包之间有运行依赖性和兼容性问题,要么一次性全部完成,要么就全部失败。回滚也是一样的,这是一个部署事务或部署一致性的问题。如何解决呢?
**● 部署环境的版本控制问题**。前面说过,我们的一个环境就会和若干个包的版本耦合,环境必需管理要部署的包的版本。于是,当你的部署越来越多的时候,各个环境的包的版本开始出现混乱,各种依赖间的版本也会出现不统一的情况,也就是说,就算你有这样的一个工具,在一个高速开发的环境下,我们的部署环境的管理还是会出现很多混乱的情况,需要你不断地统一大家的开发、测试环境。
**● 部署计划**。我们可能会有很多部署计划,比如:设定定时部署,提升或降低部署优先级,部署事务定义,部署策略(如:先部署10%的机器,如果没有问题,再把剩下的系统部署了),热切计划和策略……
等等 ,等等 。
**● 部署的监控和维护**。任何软件和系统都会有这样的问题,当规模上去了以后,我们的自动化部署系统的监控和维护的复杂度并不亚于一个大型的互联网应用。
这样的问题会有很多,基本上来说,这样一个持续集成持续部署的自动化系统并不是那么简单的事,其开发工作量和一个标准的大型互联网业务系统没什么两样。
六、总结
这里只谈一点自己的看法,从传统的持续集成到面向大型软件的持续部署,我们将系统所依赖的软件环境和软件包抽象为一致的实体纳入到管理之中,并将运维人员的工作真正的分摊到开发人员身上。而云计算的出现,使得计算机本身也可以自动化的创建和回收,这样环境管理的范畴将进一步扩充。相应的,部署的能力和灵活性也是一次质的飞跃,将再一次减轻运维人员的工作压力。
说了这么多废话,总结一下自己的观点,对于向大型软件企业推销基于持续集成的持续部署(交付)的哥们:
**● 你就是在耍流氓**,如果你不解决环境管理!!!
**● 你就是在耍流氓**,如果你不建立部署系统!!!
**● 你就是在耍流氓**,如果你不扩展编译系统!!!
**● 你就是在耍流氓**,如果你只是推销小团队的实践而不考虑改造大环境!!!
**● 你就是个流氓**,如果你只是不断地告诉别人怎么做,自己却从来不动手写一个测试或建立一个持续集成环境!!!
最后,用Linus最经典的话来结束本文——“ Talk is Cheap, Show me the
Code!”