持续部署,并不简单!

来源:酷壳网 作者:@常新居士
  

原文链接

这几年,持续集成随着敏捷在国内的推广而持续走热,与之相伴的持续部署也一直备受关注。自前两年,持续交付这个延续性概念又闯进了国内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的生产环境可能部署在相同的机器上)。

同时,原本单一的项目软件结构随着业务系统的增加也不再简单: 

而软件间的版本依赖使这个问题变得更为复杂:

现在,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,运行时依赖,环境管理应该如何解决呢?

细心的朋友,可能已经发现,在环境层面上我们确定了环境依赖的软件包,这里有两个隐藏的含义:

  • 环境定义的是对软件包的运行时依赖
  • 由于环境是一个逻辑上的概念,因此其所用的软件包也是一个逻辑上的概念(相对于版本控制系统中的软件包)

我们也已经知道,在部署时,一个环境实例将具体的确定其依赖的软件包的版本。某个版本的软件包最终与代码库中的物理的软件包相关联。但软件包是运行时的安装包,因此,它应该是代码库中包编译的结果。在对代码库的包编译时,既要将结果打上版本保存起来,也好在两者的版本间建立关系,最后,编译结果应该是某种既定的安装包目录文件结构。

另外,当环境包含的包比较多时,运行时版本树会非常大,手动的指定全部的包的版本将是一个非常大的体力劳动,这部分工作也要得到简化。由此,我们必须

好文,顶一下
(0)
0%
文章真差,踩一下
(0)
0%
------分隔线----------------------------


把开源带在你的身边-精美linux小纪念品
无觅相关文章插件,快速提升流量