- 《架构师》2016年1月
- InfoQ中文站
- 475字
- 2020-06-26 06:06:34
专题 | Topic
微服务与持续交付
编者按:InfoQ开设新栏目“品味书香”, 精选技术书籍的精彩章节,以及分享看完书留下的思考和收获,欢迎大家关注。本文节选自王磊著《微服务架构与实践》中的章节“微服务与持续交付”,介绍了持续交付是什么,以及微服务如何做到持续交付。
十年以前,软件在一年之内的交付次数屈指可数。
过去的十年间,交付的过程一直被不断地优化和改进。从早期的RUP模型、敏捷、XP、Scrum,再到近几年的精益创业、DevOps,都力求能更有效地降低交付过程所耗费的成本并提高效率,从而尽早实现软件的价值。
持续交付是一种软件开发策略,用于优化软件交付的流程,以尽快得到高质量、有价值的软件。这种方法能帮助组织更快地验证业务想法,并通过快速迭代的方式持续为用户提供价值。对于任何一个可交付的软件来说,必然要经历分析、设计、开发、测试、构建、部署、运维的过程。而从持续交付的角度来分析,对于任何一个可部署的独立单元,它都应该有一套独立的交付机制,来有效支撑其开发、测试、构建、部署与运维的整个过程。
如第2章所述,微服务将一个应用拆分成多个独立的服务,每个服务都具有业务属性,并且能独立地被开发、测试、构建、部署。换句话说,每个服务都是一个可交付的“系统”。那么在这种细粒度的情况下,如何有效保障每个服务的交付效率,快速实现其业务价值呢?
本文,我们就来探讨微服务与持续交付。本文的内容主要包括:
· 什么是持续交付
· 持续交付的核心
· 微服务与持续交付
从技术上讲,持续交付是软件系统的构建、部署、测试、审核、发布过程的一种自动化实现,而其中的核心则是部署流水线。因为部署流水线能够将这几个环节有效地连接起来。当然,像探索性测试、易用性测试,以及管理人员的审批流程等还是需要一定的手工操作,如图1所示。
图1 持续交付
1 持续交付的核心
在持续交付过程中,需求以小批量形式在团队的各个角色间顺畅流动,并以较短的周期完成小粒度的频繁发布。实际上,频繁的交付不仅能持续为用户提供价值,而且能产生快速的反馈,帮助业务人员制定更好的发布策略。
因此,持续交付的核心在于三个字:小、频、快。
● 小批量价值流动
通过建立自动化的构建及部署机制,将业务功能以小批量的方式,从需求产生端移动到用户端。
● 频繁可发布
通过建立自动化的构建及部署机制,将小批量的业务功能频繁地从需求产生端移动到用户端,持续地交付价值。
● 快速反馈
通过建立高效的反馈机制,快速验证需求是否有效。同时根据反馈,及时指导业务团队并调整策略,优先为用户交付高价值的功能。
持续交付让业务功能在整个软件交付过程中以小批量方式在各角色间顺畅流动,通过更频繁的、低风险的发布快速获得用户反馈,以此来持续达成业务目标。
2 微服务架构与持续交付
从交付的角度来分析,对于任何一个可部署的独立单元,它都应该有一套独立的部署流水线,来有效支撑其开发、测试、构建、部署与运维的整个过程。
在微服务架构中,由于每个服务都是一个独立的、可部署的业务单元,因此,每个服务也应该对应着一套独立的持续交付流水线,可谓是“麻雀虽小,五脏俱全”。
接下来,让我们看看在微服务的架构中,如果构建这样一套持续交付的流水线,各个环节需要做什么样的准备。
2.1 开发
对于微服务架构而言,如果希望构建独立的持续交付流水线,我们在开发阶段应该尽量做到如下几点。
● 独立代码库
对于每一个服务而言,其代码库和其他服务的代码库在物理上应该是隔离的。所谓物理隔离,是指代码库本身互不干扰,不同的服务有不同的代码库访问地址。譬如,对于我们平时使用的SVN、GIT等工具,每个服务都对应且只对应一个独立的代码库URL。如下所示,分别表示产品信息服务和客户信息服务的代码库。
http://github.com/xxxxx/products-service
http://github.com/xxxxx/customers-service
除此之外,对不同服务隔离代码库的另一个好处在于,对某服务的代码进行修改,完全不用担心影响其他服务代码库中的代码,在很大程度上避免了修改一处,导致多处发生缺陷的情况。
● 服务说明文件
对于每一个服务而言,都应有一个清晰的服务说明,描述当前服务的信息,同时帮助团队更快地理解并快速上手。譬如,在笔者的微服务实践过程中,对于每一个代码库,其服务说明都包括如下几个部分。
1. 服务介绍
· 服务提供什么功能,譬如产品服务主要提供产品数据的获取或者存储。
· 谁是服务的消费者。譬如产品服务的消费者为电商的前端网站系统或者CRM系统。
2. 服务维护者
· 挑选1~2个团队的成员,作为服务的负责人,登记其姓名、电子邮件、电话等联系方式,以便其他团队遇到问题能及时找到服务的负责人。
3. 服务可用期
· 服务可用周期,如7×24小时,或周一~周五(7:00~19:00)等。
· 可用率,可用率是指服务可以正常访问的时间占总时间的百分比,如99.9%或者99%。如果服务一天内都可以访问,则服务当天的可用率为100%。如果服务有3分钟访问中断,而一天共有1440分钟,那么服务的可用率为: ((1440 - 3) /1440) * 100%,也就是99.79%。
· 响应时间,指服务返回数据的可接受响应时间。譬如为0.5~1秒。
4. 定义环境,描述服务运行的具体环境,通常包括:
· 生产环境
· 类生产环境
· 测试环境
5. 开发,描述开发相关的信息,通常包括:
· 如何搭建开发环境
· 如何运行服务
· 如何定位问题
6. 测试,描述测试相关的信息,通常包括:
· 测试策略
· 如何运行测试
· 如何查看测试的统计结果,譬如测试覆盖率、运行时间、性能等。
7. 构建,描述持续集成以及构建相关的信息,通常包括:
· 持续集成访问的URL
· 持续集成的流程描述
· 构建后的部署包
8. 部署,描述部署相关的信息,通常包括:
· 如何部署到不同环境
· 部署后的功能验证
9. 运维,描述运维相关的信息,通常包括:
· 日志聚合的访问
· 告警信息的访问
· 监控信息的访问
● 代码所有权归团队
团队的任何成员都能向代码库提交代码,做到任何服务代码的所有权归团队。
代码所有权归团队,它表现的更多的是团队协同工作的观念,即集体工作的价值大于每个个体生产价值的总和。当所有权属于集体的时候,那么每个开发者就不应当出于个人原因来降低代码质量。代码质量上出现的问题应该在整个团队的努力下共同处理。
相反,如果某段代码背后的业务知识没有适当地分享给其他人,那么代码的演变逐渐变为依赖于具体的某个人,瓶颈也就由此而产生。
● 有效的代码版本管理工具
代码版本管理工具的使用早已经成为开发者必备的核心技能之一,譬如Git、Mercurial,以及CVCS(Centralized Version Control System)等。不过,团队最好能使用分布式版本控制工具(DVCS, Distributed Version Control System),它可以避免由于客户端不能连接服务器所带来的无法提交代码的问题。
● 代码静态检查工具
另外,团队也需要有代码静态检查工具,帮助完成代码的静态检查。譬如Java语言的CheckStyle、Ruby语言的Rubocop等。
另外,代码度量(Code Metrics)工具,譬如常用的SonarQube、Ruby的Cane等,能够保障团队内部代码的一致性和可维护性。
● 易于本地运行
作为团队的开发人员,当我们从代码库检出(Check out)某服务的代码后,应该花很短的时间、很低的成本就能在本地环境中将服务运行起来。如果依赖于外部资源,并且构建、使用成本较高,就应该考虑采取其他打桩的机制来模拟这些外部资源。这类外部资源通常指数据库、云存储、缓存或者第三方系统等。
譬如,笔者最近参与的一个企业内部系统改造项目,使用了OKTA集成单点登录的功能。开发环境下当然也可以使用OKTA,但由于网络、安全、审批等多种原因,极大影响了开发人员在本地环境访问OKTA的效率。最后,团队采用打桩的机制,构建了一套符合OKTA协议的模拟OKTA,在本地使用。在开发环境下,通过加载这个模拟的OKTA,有效地解决了本地访问OKTA时间较长的问题。
另外一个例子是,笔者在系统中使用了AWS的S3服务。由于权限、网络等多种因素的存在,本地开发时使用S3的成本非常高,因此就构建了一套模拟的S3环境。当服务运行在开发环境时,加载开发模式的环境变量,访问本地的Mock S3环境;而在生产环境,则使用生产模式的S3地址。在不改变任何代码的前提下,帮助团队快速在本地搭建运行环境并演示,极大地提高了开发效率,如图2所示。
图2 模拟S3存储
2.2 测试
对于微服务架构,如果希望构建独立的持续交付流水线,我们在测试方面应该注意以下几点。
● 集成测试的二义性
对于任何一个服务而言,单元测试必不可少。但是否需要集成测试,团队可以根据喜好自行决定。笔者个人建议明确定义集成测试的范围,因为“集成”这个词,很难有一个准确的度量机制。到底什么样的组合才叫集成?其可以是对外部不同系统之间的测试组合,也可以是系统内部实现逻辑、类与类之间调用的组合,因此“集成测试”这个术语,在团队和组织内部,容易在沟通过程中产生误解。
● Mock与Stub
对于单元测试而言,我们可以使用Mock框架帮助我们完成对依赖的模拟(Mock)或者打桩(Stub),譬如Java的Mockito、Ruby的RSpec等。当然,如果对象之间的依赖构建成本不高,也可以使用真实的调用关系而非Mock或者Stub机制。关于Mock和Stub的区别,有兴趣的读者可以参考ThoughtWorks首席科学家马丁·福勒的Mocks Aren't Stubs这篇文章。
● 接口测试
除了单元测试覆盖代码逻辑外,至少还应该有接口测试来覆盖服务的接口部分。注意,对于服务的接口测试而言,更关注的是接口部分。譬如,作为数据的生产者,接口测试需要确保其提供的数据能够符合消费者的要求。作为数据的消费者,接口测试需要确保,从生产者获取数据后,能够有效地被处理。另外,对于服务与服务之间的交互过程,最好能设计成无状态的。
● 测试的有效性
如果单元测试的覆盖率够高,接口测试能有效覆盖服务的接口,那么基本上测试机制就保障了服务所负责的业务逻辑以及和外部交互的正确性。
有些朋友可能会存在疑问,是否需要使用行为测试的框架,譬如像Cucumber、JBehave等的工具,基于不同的场景,做一些类似用户行为的测试呢?
实际上,这里并没有确定的答案。在笔者参与的项目中,通常都是在最外层做一部分行为测试,原因有以下几点。
1. 通常我们所说的服务,大多是不涉及用户体验部分的。也就是说,作为服务,更关注的是数据的改变,而不是同用户的交互过程。譬如,当我们从电商网站挑选某件商品、下单,一个新的订单就生成了。这时候,订单的状态可能是“新建”。随后,当完成付款时,那么订单的状态可能会被更新成“已支付”。如果忽略状态更新的实现流程,譬如同步更新、异步更新等,那么从服务的角度而言,其并不在意用户到底是从PC端的浏览器,还是手机上的APP来完成订单的支付,服务自身只完成了一件事:就是完成订单状态从“新建”到“已支付”的更新。
2. 服务作为整个应用的一部分,能够独立存在,那必然有其对应的边界条件。前面提到,单元测试保障内部逻辑,接口测试保障接口,在这样的前提下,服务的正确性和有效性在大部分情况下已经得到了验证。
3. 从经典的测试金字塔来看,越是偏向于用户场景、行为的测试,其成本越高,反馈的周期也越长;相反,越是接近代码级别的测试,成本越低,反馈周期也相对较短,如图3所示。
图3 测试金字塔
2.3 持续集成
持续集成经过多年的发展,已成为系统构建过程中众所周知的最佳实践之一。对于每个独立的、可部署的服务而言,应为其建立一套持续集成的环境(Continuous Integration Project)。
当团队成员向服务的代码库提交代码后,配置好的持续集成工程会通过定期刷新或者WebHook的方式检测到代码变化,触发并执行之前开发阶段定义的静态检查、代码度量、测试以及完成构建的步骤,如图4所示。
图4 持续集成
常用的企业级持续集成服务器有Jenkins、Bamboo以及GO等,在线的持续集成平台有Travis-CI、Snap-CI等。
更多关于持续集成的细节,请参考ThoughtWorks首席科学家马丁·福勒的这篇文章http://www.martinfowler.com/articles/continuousIntegration.html。
2.4 构建
每个服务都是一个可独立部署的业务单元,经过静态检查、代码度量、单元测试、接口测试等阶段后,构建符合需求的部署包。
部署包存在的形式是多种多样的,可以是deb包、rpm包,能在不同UNIX操作系统平台直接安装;也可以是zip包、war包等,只需将其复制到指定的目录下,执行某些命令,就可以工作。当然,也有可能是基于某特定的IAAS平台,譬如亚马逊的AMI,我们称之为映像包(Image)。
另外,作为容器化虚拟技术的代表,Docker(一个开源的Linux容器)的出现,允许开发者将应用以及依赖包打包到一个可移植的Docker容器中,然后发布到任何装有Docker的Linux机器上。
通过使用Docker,我们可以方便地构建基于Docker的部署镜像包。
2.5 部署
对于每个独立的服务而言,如果希望构建独立的持续交付流水线,需要选择部署环境并制定合适的部署方式来完成部署。通常,我们可以从如下两个维度考虑如何进行部署。
1. 部署环境
● 基于云平台
我们知道,云平台是一个很广的概念,其中主要包括IAAS、PAAS和SAAS三层。当基于云平台部署的时候,要先分清楚部署的环境,即部署将发生在哪一层。由于SAAS层是相对于应用的使用者而言,软件即服务。即对使用者而言,不需要再去考虑本地安装、数据维护等因素,直接通过在线的方式享受服务,这和我们讨论的部署环境没有关系。因此,这里我们主要讨论IAAS层和PAAS层的部署。另外,笔者这里没有区分是公有云还是私有云。公有云指运行在Internet上的云服务,私有云则通常指运行在企业内部Intranet的云服务。
● 基于IAAS层
云平台的IAAS层,通常包括运行服务的基础资源,譬如计算节点、网络、负载均衡器、防火墙等。因此,对于该层的部署包而言,实际上应该是一个操作系统映像,映像里包含运行服务所需要的基本环境,譬如JVM环境、Tomcat服务器、Ruby环境或者Passenger配置等。当在IAAS层部署服务时,不仅可以使用映像创建新的节点,也可以创建其他系统相关的资源,譬如负载均衡器、自动伸缩监控器、防火墙、分布式缓存等。
● 基于PAAS层
PAAS层并不关心基础资源的管理,它更关注的是服务或者应用本身。因此,对于该层的部署包而言,通常是能直接在UNIX操作系统安装的二进制包(譬如deb包或者rpm包等),或者是压缩包(譬如zip包、tar包、jar包或者war包等),将其复制到指定目录下解压缩,启动相关容器,就可以工作。
除此之外,也可以使用PAAS平台提供的工具或者SDK,直接对当前的代码进行部署。譬如Heroku提供的命令行,就能很方便地将Java、Ruby、NodeJS等代码部署到指定的环境中。
● 基于数据中心
云平台已经成为大家公认的未来趋势之一,但是对于很多传统的企业,由于组织或者企业内部多年业务、数据的积累,以及组织架构、团队、流程固化等原因,无法从现有数据中心一步迁移到云端。而且,针对传统的数据中心,其对应的环境通常比较复杂,既没有IAAS那种按需创建资源的灵活性,也没有PAAS这种资源能够被自动化调配的可伸缩性。这时候,对于数据中心而言,部署就相对较麻烦,需要投入更多的成本构建环境以及调配资源。
很多企业也开始尝试在数据中心的节点上创建虚拟机(譬如VMware、Xen等),以帮助简化资源的创建以及调配。
● 基于容器技术
容器技术,是一种利用容器(Container)实现虚拟化的方式。同传统的虚拟化方式不同的是,容器技术并不是一套完全的硬件虚拟化方法,它无法归属到全虚拟化、部分虚拟化和半虚拟化中的任意一个,它是一个操作系统级的虚拟化方法,能为用户提供更多的资源。
过去两年里,Docker的快速发展使其成为容器技术的典型代表。Docker可以运行在任意平台上,包括物理机、虚拟机、公有云、私有云、服务器等,这种兼容性使我们不用担心生产环境的操作系统或者平台的差异性,能够很方便地将Docker的映像部署到任何运行Docker的环境中。
2. 部署方式
部署方式,是指通过什么样的方法将服务有效地部署到相应的环境。对于服务而言,由于部署环境的不同,采用的部署方式自然也不同,如图5所示。
图5 部署方式的演变
● 手动部署
对于传统的数据中心环境,考虑到资源有限以及安全性等因素,通常的部署方式都是使用SSH工具,登录到目标机上,下载需要的部署包,然后复制到指定的位置,最后重启服务。
● 脚本部署
由于部署团队每次都要手动下载、复制,不仅效率低,而且人为出错的概率也大。因此,很多企业和组织使用Shell脚本将这些下载、复制、重启等过程逐渐实现自动化,大幅提升了效率。Shell脚本的优势是兼容性好,但其弊端在于实现功能所需要的代码量大,可读性也较差,时间长了不易维护。
● 基础设施部署自动化
随着业务的发展,很多组织以及团队逐渐发现,环境的安装和配置、应用或者服务的部署所耗费的成本越来越高。“基础设施自动化”这个概念的提出,正好有效地解决了这一类问题。于是,越来越多的组织开始尝试使用Chef、Puppet、Ansible等工具,完成软件的安装和配置,以及应用或者服务的部署。
● 应用部署自动化
无须过多的人工干预、一键触发即可完成部署的自动化方式可以说是任何组织,从业务、开发到运维都希望达成的目标。但说起来容易,实现起来却很难,而且这也不是一蹴而就的过程。需要随着组织或者企业在业务的演进过程中、技术的积累过程中,逐渐实现自动化。通常,应用部署的自动化主要包括以下两部分。
● 映像部署
私有云、公有云的出现,使得部署方式发生了显著的变化。面对的环境不一样,因此部署包也不一样。以前的war包、rpm包,在基于IAAS的云平台上,都可以变成映像。譬如,对于亚马逊的AWS云环境,可以方便地使用其提供的系统映像(AMI)来完成部署。
基于映像的最显著优势在于,能够在应用需要扩容的时候,更有效、迅速地扩容。原因在于,映像本身已经包括了操作系统和应用运行所需要的所有依赖,启动即可提供服务。而基于war、rpm包等的部署,扩容时通常需要先启动同构的节点,安装依赖,之后才能部署具体的war包或者rpm包。
● 容器部署
利用容器技术,譬如Docker,构建出的部署包也可以是一个映像。该映像能运行在任何装有Docker的环境中,有效地解决了开发与部署环境不一致的问题。同时,由于Docker是基于Linux容器的虚拟化技术,能够在一台机器上构建多个容器,因此也大大提高了节点的利用率。
所以,就微服务架构本身而言,如何有效地基于部署环境选择合适的部署方式,并最终完成自动化部署,是一个值得团队或者组织不断探讨和实践的过程。
2.6 运维
由于每个服务都是一个可以独立运行的业务单元,同时每个服务都运行在不同的独立节点上。因此,需要为服务建立独立的监控、告警、快速分析和定位问题的机制,我们将它们统一归纳为服务的运维。
● 监控
监控是整个运维环节中非常重要的一环。监控通常分为两类:系统监控与应用监控。系统监控关注服务运行所在节点的健康状况,譬如CPU、内存、磁盘、网络等。应用监控则关注应用本身及其相关依赖的健康状况,譬如服务本身是否可用、其依赖的服务是否能正常访问等。
关于监控,目前业界已经有很多成熟的产品,譬如Zabbix、NewRelic、Nagios以及国内的OneAPM等。对于笔者参与的项目,服务节点的运行环境大都基于AWS(使用EC2、ELB以及ASG等),因此使用AWS的CloudWatch作为系统监控工具的情况比较多。关于应用监控,通常使用NewRelic和Nagios作为监控工具。
● 告警
告警是运维环节另外一个非常重要的部分。我们知道,当系统出现异常时,通过监控能发现异常。这时候,通过合适的告警机制,则能及时、有效地通知相关责任人,做到早发现问题,早分析问题,早修复问题。由于每个服务都是独立的个体,因此针对不同的服务,都应该能提供有效的告警机制,确保当该服务出现异常时,能够准确有效地通知到相关责任人,并及时解决问题。
对于告警工具,业界较有名的是PagerDuty,它支持多种提醒方式,譬如屏幕显示、电话呼叫、短信通知、电邮通知等,而且在无人应答时还会自动将提醒级别提高。除此之外,之前提到的常用的监控产品也能提供告警机制。
● 日志聚合
除此之外,日志聚合也是运维部分必不可少的一环。由于微服务架构本质上是基于分布式系统之上的软件应用架构方式,随着服务的增多、节点的增多,登录节点查看日志、分析日志的工作将会耗费更高的成本。通过日志聚合的方式,能有效将不同节点的日志聚合到集中的地方,便于分析和可视化。
目前,业界最著名的日志聚合工具是Splunk和LogStash,不仅提供了有效的日志转发机制,还提供了很方便的报表和定制化视图。更多关于Splunk与LogStash的信息,请参考其官方网站。
3 小结
本文首先讲述了持续交付的概念及其核心,接着讨论了在微服务架构的实施过程中,如果建立基于服务的细粒度的持续交付流水线,应该考虑的因素。虽然微服务中的服务只是整个应用程序的一个业务单元,但作为一个可以独立发布、独立部署的个体,它必然也要遵循持续交付的机制和流程,包括开发、测试、集成、部署以及运维等,可谓是“麻雀虽小,五脏俱全”。通过搭建稳定的持续交付流水线,能够帮助团队频繁、稳定地交付服务。
书籍介绍
本书首先从理论出发,介绍了微服务架构的概念、诞生背景、本质特征以及优缺点;然后基于实践,探讨了如何从零开始构建**个微服务,包括Hello World API、Docker映像构建与部署、日志聚合、监控告警、持续交付流水线等;最后,在进阶部分讨论了微服务的轻量级通信、消费者驱动的契约测试,并通过一个真实的案例描述了如何使用微服务架构改造遗留系统。全书内容丰富,条理清晰,通俗易懂,是一本理论结合实践的微服务架构的实用书籍。
本书不仅适合架构师、开发人员、测试人员以及运维人员阅读,也适合正在尝试使用微服务架构解耦历史遗留系统的团队或者个人参考,希望本书能在实际工作中对读者有所帮助。