阿里技术专家:持续交付与微服务背后的实践逻辑
崔力强
阿里巴巴技术专家
前言
大家好,我是崔力强。目前在阿里巴巴任职。负责一款持续交付领域的SaaS产品的开发。非常高兴能够和大家分享持续交付和微服务的话题。
本次分享的重点是持续交付。也会提到一些微服务的概念,以及持续交付和微服务之间的关系。今天会涉及的一些实践可能大家或多或少有所耳闻。我会着重讲述这些实践背后的逻辑,及它们之间的关系。
先看一看提纲:
持续交付的概念
关于持续交付的概念。从《持续交付》这本书的副标题可见一斑:“发布可靠软件的系统方法”。可以看到这本书中讲的“持续交付”主要是技术相关的实践,虽然近年有些朋友把持续交付的概念进行了延伸,把精益需求管理和精益创业也包含了进来,不过今天我还是会按照它最初的内涵,只讲技术相关的实践。
这里其实有两个概念,第一个是,怎么样才算持续交付,也就是目标;第二个是,如何做到持续交付,也就是一些技术实践。
为了了解“怎么样才算持续交付”,让我们先解剖一下软件开发的过程。
针对单个需求来说,我们会先进行需求分析、细化。然后开发和测试。部署时,需要拷贝一些文件到一些机器,运行一些脚本,有时候还需要改一些配置文件或者做一些数据迁移等。
然而在实际的项目中,肯定不会只有一个需求。需求会源源不断地输入到开发团队。下面是一个开发团队随着时间进行需求开发的示意图。蓝色的条是开发某个功能的开始时间和结束时间点。有的需求占用时间长,有的需求占用时间短。
在每个功能的完成时刻,对于业务方来说,都可能是一个可以发布的状态(也有可能不是,比如业务方认为相关的几个需求必须都做完了才能一起上,或者需要等到某个特定的时间点才能上)。那么如果立马就能够进行一次发布,并且能够快速并且安全的完成这次发布,则能够对业务的发展具有非常积极的作用。也就是在下图的这些时刻。
这些时刻,不一定每个都需要发布。但作为开发团队,要给业务方足够的灵活度。所以持续交付的目标,并不是每次提交都进行发布,而是每次提交都是可发布的状态。这就回答了“怎么样才算持续交付”这个问题。再换一种方式来说,持续交付是对业务方友好的一种,开发团队的开发节奏。
接下来就要讨论使用什么样的技术实践来达到这种开发节奏。为了讨论具体的技术实践,首先来看看在软件开发中有什么因素会阻碍我们达到这种节奏。
持续交付会遇到的问题及解决方案
第一个问题是:上线前总还是要做测试的吧,至少做一遍重要功能的回归测试(手工回归本身其实也是有章可循的,推荐我同事的一篇文章:https://yq.aliyun.com/articles/6898)。随着一个系统上的功能越来越多,每次上线前需要测试的东西就越来越多。慢慢的,发布就慢下来了。我曾经工作过的一个系统,上面承载了三块业务,有将近40多个人在上面工作。每次发布前都要先进行几天的回归测试,顺利的话,从确定要发布的版本到验证完该版本,确定可以发布,也要差不多一周的时间。
另一个我工作过的项目规模要小一些,做一次发布也需要一天的时间来回归,通常都会发现一些问题。顺利的话,可以在当天把所有的问题修复掉,进行发布。不过因为bug没修完而不能当天发布,拖到第二天的这种情况也时常发生。
甚至有一次,到了晚上九点多的时候还没有修完bug,但当时大家都憋着一口气,硬是要发,结果发到了晚上一点多。
因为成本高,所以不敢做的太频繁;做的不频繁的话,就会在发布中积累更多的功能,从而进一步增加出问题的可能性,从而形成一个恶性循环。
作为一个妥协,人们不得不使用瀑布或者迭代内小瀑布的开发模式。
把开发的周期分解为一个一个的迭代,比如两周到四周的时间。在迭代开始前,保证该迭代内计划的需求分析完毕。然后在迭代内部开发。顺利的话,会在接近迭代末尾时完成迭代内计划的所有任务,然后拉一个发布分支出来,开始测试,然后发布。
做出上述妥协的直接原因就是“测试和部署”花费的时间过长。如果只花费一个人一个小时就能够完成回归和发布,那显然团队就更愿意去频繁的发布。
一个行之有效的方法就是进行自动化测试。
自动化测试大致可以分为几种:单元测试、API测试、验收测试/功能测试/端到端测试。在不同的技术栈下,分类可能会略有不同的,但本质上来讲是类似的。不同层次的测试有自己的侧重点,组要组合使用来达到一个比较好的效果。如上图所示:
这里以Java Spring项目为例来列举不同层次的测试工具:单元测试使用Junit;集成测试使用Spring Test + Junit;功能测试使用cucumber+ capybara+selinium或者robotframework+ selinium。
如果使用了前端框架,比如Angular、ReactJS等,它们本身也提供了相应的测试框架。
底层的单元测试,测试的范围较小,一般只涉及一个或者几个类,不会调用网络或者数据库。所以编写和运行起来都比较快。在这个级别应该覆盖尽量多的分支和逻辑。这个级别的测试能够达到比较高的覆盖率,所以在它的保护下,可以放心大胆的做重构或者添加新代码,只需要花上几秒钟的时间运行一遍单元测试,就能够知道这次修改是否引入了问题。前阵子做了很长时间的nodejs开发,单元测试对这种弱类型的语言尤其重要。因为像变量未定义,传参数的个数错误等很低级的问题IDE都无法给出有效的提示。
因为单元测试需要隔离被测类和系统的其它代码,所以需要有一些测试替身来代替真实的类。有很多工具可以做这样的事情,比如Java中的mockito。
如上图所示,它会创造一个假的ClassB的实例出来,并传给ClassA的实例。然后对ClassB的行为做出一些假设,在此假设的基础上对ClassA的行为进行测试。
但是一个类或者几个类的正确,并不能让你对系统的正确性有足够的信心。因为单元测试中充满了对其它类行为的假设。所以一旦这个假设错误,就会出现测试依然能通过,但整个系统的行为已经错了的尴尬情况。所以我们还需要覆盖面更大的集成测试。这种测试在服务内部不使用任何的测试替身。但对外部的服务进行打桩。对基于HTTP的服务进行打桩的工具包括moco(https://github.com/dreamhead/moco)和pact(https://github.com/realestate-com-au/pact,其实pact能做的事情不止打桩,更多的是做“契约测试”)。这种测试更真实,但运行起来会慢,所以这个层面的测试主要保证的是连通性。不需要100%的覆盖率。
集成测试覆盖面很大,但它仍然是白盒测试,因为它直接调用了函数(比如上页的controller)。如果这个服务只提供API,那么这种测试就够了。但如果这个服务是提供页面的,也就是一个web应用,那么就还需要一层直接操作网页来进行基于用户行为的测试,我们一般称之为验收测试,或者功能测试。上面列举的cucumber和robotframework是非常流行的两款功能测试工具。他们是通用的测试框架,与具体的被测系统是无关的。
如果要测试web系统的话,就需要能够驱动网页的驱动程序。现在非常主流的驱动是selinium(https://github.com/SeleniumHQ/selenium
),当然我更喜欢的是在其上包了一层的capybara(https://github.com/jnicklas/capybara),它是用ruby编写的,封装的API更好用。
建议至少对核心的流程编写功能测试,以保证上线不要出现严重的故障。前段时间我们的功能测试发现了一个bug。这个bug对于老用户都不会有问题,但是用户首次登录就会500。而用户首次登录的场景恰恰是平时自测的时候很容易忽略的,因为准备数据还是有点麻烦的。当时发现这个问题的时候大家并没有什么感觉,因为已经习惯有测试保护的软件了。但如果跳出来想想,这个问题要是在一周后的“迭代末尾”才发现,会多么的打击气势。如果上线才发现,那么产品的新用户增长量一定会直线下降。
端到端测试其实也就是使用功能测试的工具在更大的范围进行测试,也就是包含所有的服务。
下面总结一下各个层次测试的特点。
回归测试是迭代开发中必不可少的一个步骤。我们能做的就是通过自动化测试去尽量减小这个时间。
很多人对于测试有一些顾虑,觉得会花费很多时间。而且当代码结构调整时,测试也要跟着改。
我的看法是,测试代码也是代码,维护测试代码的代价跟测试代码本身的质量是直接相关的。所以对测试代码也需要及时重构,提高可维护和可重用性。具体一点对于测试来说,提高可维护性和看重用性,无非就是要在数据准备、断言工具方面去抽取一些库。比如Ruby的factory girl就是一个极好的基于ActiveRecord的数据准备库。有了它,写测试的代价大大降低。那如果你不用Ruby怎么办,那只好自己实现一个其它语言版本的factory girl喽。我上一个项目用的是nodejs,就写了一个nodejs版本的factory girl。
而单元测试能够给你带来的好处不仅仅是回归这么简单。有了完备的单元测试,你才有信心,有动力去做一些重构。只要测试通过,我就知道我的重构是正确的,你才敢不断的去重构,优化代码,才能使得代码更易维护。所以可以说写测试是保证你代码可维护性的必由之路。不要考虑写不写测试,而是考虑,如何低成本的写测试。
当我开发新功能时候,编写好测试运行一下,就知道功能正确与否,这样就不用把服务器启起来,减小反馈的周期。在这个场景下,它会直接节省你的时间,虽然你写了更多的代码。
关于代码改动,测试也要跟着改的问题,我想说两点:
第一,项目的初期,一般代码的架构会不太稳定,那么在不影响运行和编写速度的前提下,尽量测试的范围大一些,也就是包含的类多一些。这样改动一个类,引起测试变化的可能性就会降低,并且可以在测试的保护下放心大胆的做架构调整。
第二,要善用IDE帮你做改动。测试代码也是代码,当你修改一个函数签名时,IDE会帮你把所有的调用处都改掉,包括测试代码。所以IDE用得好,修改代码也不是那么痛苦的事情。
那么有了这些测试之后,我应该什么时候运行它们呢。是迭代结束时吗?不!我们应该在每次提交时都完整的运行一遍这些测试。这样一旦出了问题我就可以第一时间知道。这就是持续集成的基本概念。
每次提交代码触发编译、测试、静态检查、打包归档、然后再运行验收测试(AT),然后再部署到类生产环境进行性能测试,再部署到端到端测试环境运行端到端测试。并且把每一步的结果反馈给开发团队。
我们把上图称为持续集成流水线。可以使用很多工具来实现,比如最常见的开源工具Jenkins。或者我目前所负责产品:crp.aliyun.com。
关于更多的持续集成的实践和流水线设计,因为内容很多,这里只讨论几个要点。
-
代码仓库: 每个开发要及时的提交代码,不要把代码长期留在本地,一天至少提交一次。不要开长时间的分支。越频繁的提交代码,就能得到越及时的反馈。
-
构建脚本: 构建脚本必须要放在代码库,切忌把它们放在只要少数人能够访问的神秘的地方。
-
软件包服务器: 每次构建的产出物,必须要按照一定的版本规则存放起来,以供后续的步骤使用,比如做测试和部署。软件包的形式是多种多样的,比如Java的jar、war,Ruby的gem,操作系统的rpm等等。甚至是最通用的tar,也可以成为你的软件包形态。
-
分stage: 大家可以看到上面这个流水线是分stage的,每个stage是顺序执行的。越往前的 stage检测越快,并越简单。越往后的stage检测越耗时。任何一个stage运行失败,后续的stage都不会再继续进行,本次流水线的进行就失败了。所以流水线运行到越往后的阶段,我们对于本次构建是可以上线的信心就越强。
我们使用自动化测试加持续集成解决了第一个发布前回归测试耗时的问题。
第二个问题就是:“别人的功能还没做完”。假设现在团队正在进行单分支开发(也就是说所有的功能都提交在一个分支上,不会为了一个功能单独 开出一个长期存在分支)。就拿图中红色线这个时间点来讲,第三个需求完成了,业务人员也认为可以做一次发布。但是同时还有另外三个需求正在开发。如果做发布的话,就会把做了一半的东西也给发上去。这是不可以接受的。
解决这个问题有两个思路:那就是功能分支和功能开关。
先看看功能分支:
每开一个新功能,就开一个分支。这个分支存活的时间通常是“周”这个数量级的。哪个功能开发完成了就合入到主干,进行一次发布。这样,其它未完成的功能还没有合入到主干,就不会造成影响。但功能分支有很多的问题,最严重的一个问题是:它和持续集成的理念是冲突的。持续集成是希望你每次提交都能够放在一起进行验证。但使用了分支的话,就只能在合并的时刻,才能真正把所有的东西放在一起进行验证。而这时发现的问题可能一周前已经发生了。
另一个方法,功能开关,会给任何一个新开发的功能在代码级别加上一个开关,使得可以简单的修改一个配置就把一个功能完全隐藏掉。默认所有的开关都是关闭的,如果一个功能做完了,想上,则修改配置,打开开关,进行一次发布即可。听起来很理想,但事实上也需要花费不少的代码来把这件事情真正做好。
关于功能分支和功能开关今天不展开细讲了。
接下来我们聊一聊发布。因为我们希望发布也是快速并安全可靠的。
发布是一件麻烦事。一次发布可能会需要部署多个应用,每个应用都要部署多台机器,有时候除了改代码之外,还需要修改配置,比如nginx配置等。大多运维团队都会有一些脚本来做这些变更。但这些脚本通常都藏在某些只有运维团队才知道(并有权限)的机器上,开发和业务团队都已经就绪之后,还需要等待运维团队抽出时间来做些变更,这就无形中增加的时间成本。还是在前面提到的那个项目中,作部署就是有专门的运维团队, 排期来对该应用进行部署。通常又会再多等一两天。
DevOps是一种团队合作的模式,即开发人员自己可以按需进行部署,不需要等待一个专门的发布团队的时间。DevOps其实现在还是没有一个标准的翻译,我的一个前同事将它翻译为“开发自运维”,我觉得还是挺贴切的。
在这种模式下,原先的运维团队应该转换自己的职责,从负责具体业务的变更,变成基础资源的提供者。比如当开发团队需要一台虚拟机,或者一个Docker集群时,能够通过简单的调用API,在很快的时间得到它,而不需要繁杂冗长的审批流程。运维团队还可以提供有效的监控、告警工具等,同样把他们以基础服务的形式提供给开发团队。就像现在AWS和阿里云做的事情。
其实很多小团队,包括我自己所在的团队,都采用了DevOps的合作模式。但是做归做,如何能做好呢?如何能够保证每个开发(甚至是入职不久的开发)能够安全快速的完成一次发布呢?
答案是自动化加可视化。自动化就是部署一个应用时,应该有脚本能够一键从构建物仓库拉取出正确版本的构建物,然后部署到相应的机器(或者多台机器)上。更重要的是这个自动化脚本不应该藏在一个秘密机器的角落,因为这样的话, 就很难告诉团队成员如何去使用它们。所以应该把它可视化,而可视化的最好的平台就是上面提到的那个持续集成流水线,在流水线的后面再加上一个部署线上的环节。
这样,开发人员就能够在一个统一的入口去了解从验证、打包,再到生产环境部署的的全流程。当然我们的持续集成流水线也就顺理成章的变成了持续交付流水线。
在发布方面,还有一个重要的课题就是环境管理。
现在大多的线上部署模式是:申请几台虚拟机,标明每一台的用途,然后开始在各个虚拟机上安装各自需要的基础软件,比如nginx、tomcat等。然后写一个脚本进行各个应用程序的部署(这个脚本最终会集成到持续交付流水线中),注意这里的脚本仅仅负责应用程序的部署,而不包含前面提到的基础软件。如果基础软件需要升级,或者安装新的基础软件,或者需要调整系统参数,这些过程都需要手动进行。这种模式在大多数情况下是没有问题的,但一旦机器出了问题,或者需要扩容时,就需要花费大量时间来重新安装一台和之前那台一模一样的机器,再修改部署脚本把软件部署上去。这个过程不但耗时,而且非常不可靠,因为你没法保证你装出来的这台机器就和之前的那台一模一样,很有可能就给未来埋下了一颗定时炸弹。
解决这个问题的思路就是所谓的“基础设施即代码”。也就是把环境的创建过程使用代码的形式描述出来,并且提交到代码库中。任何的环境变更都必须通过修改代码、提交,然后总是使用代码库中的最新版本重新构建环境。禁止直接在机器上进行任何环境变更,比如装软件,升级软件,该软件配置等。这样所有机器的状态就是可预测的,并且是一致的。
基础设施及代码的相关工具有很多,比如最早流行的chef和puppet,到后来的Ansible。这里我们就拿Ansible为例子讲一讲环境管理和部署自动化。
Ansible是一个Agent Less的通用部署及环境管理工具。也就是说不需要在目标机器上预先安装任何客户端软件,这点与chef有所差别。它依赖的就是简单的ssh命令。你说这跟我直接写shell或者ruby脚本ssh到目标机器,然后执行一些脚本有什么差别呢?
差别当然是有的,它会在以下几个方面给你提供便利:
第一:Inventory管理。Inventory,即你要管理的那些机器。使用Ansible,你可以在一个集中的文件中,以结构化的形式列出所有你需要管理的机器,及如何登陆它们,也就是ssh的用户名和密码 等信息。不同机器的用途不同,比如这三台是web服务器,那两台是搜索引擎等。那么Ansible也提供了对机器进行分组的能力。有了这些分组后,就可以很容易的在命令行中指明我这次要对那些机器做变更。并且Ansible会自动地对所有这些机器做变更,省去了自己做循环的工作。
下面看几个Ansible官方的Inventory例子:
第二个重要的点叫做变更操作的幂等性。举个例子,某一次对机器的变更是在~/.bash_profile中添加一行对JAVA_HOME的配置:“export JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_71.jdk/Contents/Home/”。那么我可以写一个shell脚本完成这件事情:“echo export JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_71.jdk/Contents/Home/ >> ~/.bash_profile”。但是如果下次我的shell脚本里面多了安装apache web server的代码。我就需要把这整个脚本再对目标机器运行一次。那么就会出现~/.bash_profile中出现两行JAVA_HOME的配置的问题。虽然不至于引入错误,但也是很没有必要的操作。
所谓幂等性,就是同一个脚本对同一台机器运行多次后,机器的状态应该都是一致的。Ansible中模块(module)的概念就覆盖了“幂等性”这个概念。所谓模块是预先写好的一些库,然后可以在Ansible的脚本中进行调用。上面的在一个文件中添加一行的操作就可以使用“lineinfile”这个module来做。在Ansible脚本中的写法是:“lineinfile: dest=~/.bash_profile line=/Library/Java/JavaVirtualMachines/jdk1.8.0_71.jdk/Contents/Home/”。再比如还有一个module,叫做service,Ansible脚本中对于service的一个调用示例是这样的: “service: name=httpd state=started”。这个描述的含义就是:“保证名为httpd的service是started状态”。所以你可以想象到它的具体实现就是先检查下httpd这个service的状态,如果已经是started的就什么都不做,否则就启动它。
这种幂等性在配置管理方面是非常有用的,这样我就可以放心的运行这些脚本,知道最终一定可以得到某个一致的状态。而且可以节省运行这些脚本的时间,比如发现JDK已经装好了,就不需要再装一遍。
上面提到的Ansible编写的脚本被称作Playbook,下面是几个playbook的例子:
这个playbook是一个完整的例子,其中包括了我要部署那些机器(hosts)。是用什么账户登录(root),运行哪些任务(tasks)等等。task中的name只是描述信息。
但是遗憾的是这种幂等性是不能完全保证的,有的module可以保证,比如上面提到的service和lineinfile。但有些是不行的,比如command module,它做的事情就是运行一条命令。Ansible无法判断这条命令是否执行过。
所以在使用Ansible的过程中需要尽量使用能够保证幂等性的module。这样才能保证所有的机器在运行一段时间之后配置是相同的,避免“配置漂移”。当然还有一个避免配置漂移的方法就是每次都重新申请一台新的机器,然后对着它运行一遍这些脚本。这也是可行的,我们后面对此进行讨论。
Ansible作为一个完备的工具,在错误处理,回滚,调试等方面也都提供了便利的支持。详情大家可以参看Ansible的官网。上面有关于Ansible本身的介绍,和一系列的扩展module。
最后再看一看Ansible整体的结构:
前面我们提到了一种模式,即每次都新做出来一台机器,然后把这些Ansible(或者其它什么工具)脚本对着这些干净的机器运行一遍。最后再把特定版本的软件部署上去。也就是说每次部署都会把原来的虚拟机实例干掉,再重新生成一台。在实际场景中,同一个应用会存在多个实例,我们没有必要对每台机器都这么做,只需要把一台机器使用Ansible装好之后,再打个镜像,然后通过这个镜像启动多台实例。
这种模式能够带来的好处是显而易见的,不但保证了环境的一致性,且扩容非常容易,只需要把同一个镜像再多启动几个实例,然后挂接到相应的负载均衡中即可。而且永远不需要害怕线上机器crash掉。按照镜像再启动一个就可以了。但这种模式带来的问题也是显而易见的,首先打虚拟机镜像的时间是很长的。其次这种做法就要求服务器是没有状态的,也就是不能在硬盘上存文件,写log。还好现在的云服务提供商(AWS,阿里云等)都有相应的产品来解决这些问题。
对于上传的附件和图片等文件来说,有两种方式:
-
用户上传的文件直接转存第三方云存储。
-
把NAS等第三方存储挂载到实例的本地目录。
这两种方法能够在一定程度上解决问题,但终究不是本地磁盘,在读写速度和并发写的处理等方面都会多多少少存在一定问题。所以只能适用于对这些指标要求不高的场景。
对于log来说,也有两种方式:
-
用户产生的log不要写到本地,而是直接推送到一个中央log处理服务。
-
log还是写在本地,但是本地会有一个收集log的agent,不断的读取log内容,并发送到集中的log服务。比如阿里云的SLS就是这么工作的。
使用上述的方式时,所有的代码,软件和配置的变更都需要走这么一个流程:各种各样的自动化测试、镜像构建,实例化镜像启动服务。所有的变更,不管再小,都会走这样的流程,而不会直接更改在实例机器上。所以实例机器就是不可变的了。这就是所谓“不可变服务器”的概念。
但这个过程是比较漫长的。所以对于紧急发布之类的场景,是很让人捉急的。而Docker技术的出现就很好的解决了这个问题。Docker基于Linux Container(LXC)技术,能够做到轻量级的虚拟化。
Docker采用了分层的文件系统,所以如果我在包含Java的镜像的基础上打一个包含Tomcat的镜像,只需要创建出一层只包含Tomcat的镜像,然后和原先的包含Java的镜像叠加在一起,就可以形成一个完整的可运行的镜像。
在这种分层的镜像机制下,如果每次只修改最上面一层镜像,则构建的速度是很快的。而最上面一层通常就是添加应用程序。以Java Web程序为例,包含Java和Tomcat的镜像可以作为一个 基础镜像。然后每次生成的WAR包通过Dockerfile(用于构建Docker镜像的描述文件)中的ADD指令添加到新的镜像层中即可。
举个例子:这里有一个Java的web应用,通过运行“./gradlew war”的命令会在本地目录下生成’build/libs/bookstore.war’。然后编写如上图的Dockerfile,它会把本地生成的war包ADD到Docker镜像中。运行’docker build . -t bookstore: <版本号>’就可以生成一个镜像。
通过Docker的history命令可以看到1.5和1.3两个版本的最上面一层是不同的,但它们的基础镜像层都是“25e98610c7d0”。最上面一层的大小是6.106M,也就是比一个war包稍微大了一点点。
综上所述,可以看到相比使用虚拟机镜像作为不可变服务器,使用Docker镜像有如下优势:
-
构建时间短。
-
使用空间小。
而前面提到的那些使用虚拟机作为不可变服务器时,需要解决的问题(本地文件,log等),使用Docker同样会面对。而解决方法也是类似的。
既然Docker这么方便,那么使用虚拟机作为不可变服务器是否还有价值呢?这个其实主要还是看相关工具,及其成熟度。比如AWS和阿里云都提供了使用配置文件来编排虚拟机资源的能力,而且可以设置一些触发器来自动以虚拟机为单位对应用程序进行扩展(scale)。这种模式已经非常成熟了。
而对于容器而言,这些云提供商也开始逐渐推出容器服务,把上述的那些对虚拟机的操作也引入到了容器的领域。今年五月份阿里云的容器服务就已经商用化了。它提供了集群管理的能力,也可以设置触发器对某一个应用进行扩容和缩容。关于阿里云容器服务提供的更多能力,因为时间关系,就不再赘述,有兴趣的朋友可以在这里做详细了解:https://yq.aliyun.com/teams/11。
Ansbile、虚拟机不可变服务器、Docker Image都是很有用的技术,但针对每个具体的技术,还是需要仔细评估你的应用是否能够克服或者容忍前文提出的相应的限制和问题。并且需要看看这些技术能给你的业务带来多大的好处。
最重要的一点就是无论你在部署阶段使用的是何种技术,使用一条完整的从代码提交到最终部署上线的持续交付流水线都是必须的。在流水线上看到的都只是一个一个的stage,并且某些stage(比如部署)应该需要手动批准触发。至于点击之后到底是调用了Ansible脚本,还是运行了docker pull都是实现细节了。下面是一个使用 http://crp.aliyun.com 配置出来的示例持续交付流水线,及其不同的状态。
上图是一个持续集成流水线的不同状态的样子。可以看到刚开始的两个stage,代码检出和集成测试,是由代码提交自动触发的。到了第三个stage,也就是部署测试环境,就需要手工批准了,所以出现了一个按钮给你按。后续的预发和生产环境也都类似。
持续交付部分就讲到这里,下面是个小结:
接下来我们再聊聊微服务。
微服务与持续交付的相互作用
关于微服务的概念,《微服务设计》一书给出的定义是:一些协同工作的小而自治的服务。微服务能够带来很多的好处,帮助我们更好的进行持续交付。当然微服务本身也需要很多实践的支撑,比如Martin Fowler就在他的bliki(http://martinfowler.com/bliki/MicroservicePrerequisites.html)中提到了“You must be this tall to use microservices”。而这个’tall’中的很多内容都已经涵盖前面讨论的那些持续交付的技术实践中。所以可以说微服务和持续交付也是相辅相成的关系。
使用微服务之后,显然你需要部署的服务就会增多。如果一个服务的自动化部署和相应的流水线都没有做好,那么服务多了之后部署的复杂性就可想而知了。所以只有把持续交付的实践先做好,才有可能顺利地使用微服务。
反过来看,微服务架构下,每个服务都很小。因此如果我的某次修改只涉及了一个微服务的代码,我只需要发布这一个服务即可。那么相应的测试工作也就简单的多。
其实按理说,虽然服务拆开了,但是还是需要这些服务在一起才能完成整个系统的功能。所以只修改一个服务,还是有可能影响整个系统的功能的。但是因为它们是不同的服务,所以一定会有非常清晰的API接口。这种API接口其实跟一个单块系统中的模块化的概念很类似。只不过API容易做的清晰,而单块系统中的模块化的边界很难维持。所以从这个角度看,微服务带来的其实是“强制的模块化”,从而带来更好的设计。
好的,话说回来。既然每次发布只涉及到需要修改的那些微服务,那么影响的面就相应的较小,所以就可以更加放心大胆的去做发布,也就进一步促进了持续交付。
微服务所涉及的话题非常多,大家可以移步《微服务设计》这本书查看所有的话题。这里只分享一点,那就是使用渐进式的方式进行微服务化。当然其实“渐进式”是我做大部分变动时的一个通用原则,比如重构,架构变化等。
渐进式微服务化的一个场景就是当你要新做一块相对来说比较大,而且比较独立的功能时,就可以考虑,是否可以单独写在一个服务中。举个例子,若干年前我在一个比较大的Java Spring项目上工作。然后客户有了一块新的业务,最终希望以主站上的一个tab页的形式存在。但我们都不想在这个陈旧的系统上继续开发。最终的方式就是新启一个应用。使用当时开发效率最高的技术。
那么怎么做到存在主站上的一个tab呢?答案是使用nginx集成。为了不暴漏客户信息,下面我们会用一些加的信息。这个应用的所有url都在/new_app/下。在主站的nginx配置中加上一条转发的规则,把/new_app/*这样的url,都转发到新部署的应用上。
这是一个行之有效的粘合新服务的方法。后来我们使用类似的方法把其它的“tab页”(也就是其它不同的业务)也都一一用新的技术重写了一遍,挂载到了主站上。
好的,稍微总结一下今天的内容:
今天主要讲了什么是持续交付的目标,为了达到这个目标需要使用哪些技术。然后还聊了聊微服务的方法论会给持续交付这件事情带来怎样的机遇和挑战。最后举了一个例子来说明如何逐步进行微服务化。
感谢大家的聆听。
Q&A
Q1:使用Docker部署微服务持续交付时,应该注意什么?你们的Docker使用情况是怎样的?
A1:我现在做的是一款持续交付产品,本身会有一个构架集群,执行任务使用的是Docker,但集群软件本身并没有使用Docker来不熟。不然就是在Docker中运行Docker了,性能会有些影响。
前端的portal正在做Docker化,还没有应用到生产环境中。一个可以分享的就是,要把自己的应用的相关配置都环境变量化,这样对于Docker化比较友好。
我们还有一个产品是code.aliyun.com。这个产品也没有Docker化,或者说『不可变服务器化』,原因就是因为要在本地磁盘写代码库。
所以上面提到对本地读写要求比较高的应用做『不可变服务器』还是有些困难的。
Q2:你们目前关注的持续集成的量化指标有哪些?比如项目单元测试/接口测试/静态检查等方面的内容。整个持续集成有效运转的效率如何?失败了怎么办?怎么保证交付系统的稳定性?
A2:指标主要是测试覆盖率、代码复杂度,及checkstyle检查出来的一些问题。持续集成会有一个大屏幕把信息辐射出来,所以如果出错了,所有人都能看到会要求把CI break的同学立即修复,并且在修复之前不允许新功能的提交。
Q3:问个小问题,崔力强老师描述的package能包括什么内容?
A3:可以是纯粹的应用,比如war包。也可以是一个压缩包,里面包含了war包和安装脚本,这样这个软件包就是可以自安装的。
Q4:Windows的架构会支持吗?
A4:刚才提到的Ansible是支持Windows的。Docker的话,现在出了原生的Docker,可以做开发测试之用。但生产环境的性能如何,还需要测试一下。
Windows上有原生的Docker,我是mac用户,win版的Docker其实也没有用过,只是看到Docker官网的消息:https://www.docker.com/products/docker :)原生的mac版Docker的volume挂载的性能也很差,猜测可能win版本的也不会太好。
本文来自云栖社区合作伙伴"DBAplus",原文发布时间:2016-07-25
标签:
相关文章
-
无相关信息