- 微服务实战
- (英)摩根·布鲁斯 保罗·A.佩雷拉
- 6809字
- 2020-08-27 09:06:09
1.1 什么是微服务应用
微服务应用是一系列自治服务的集合,每个服务只负责完成一块功能,这些服务共同合作来就可以完成某些更加复杂的操作。与单体的复杂系统不同,开发者需要开发和管理一系列相对简单的服务,而这些服务可能以一些复杂的方式交互。这些服务之间的相互协作是通过一系列与具体技术无关的消息协议来完成的,这些协议可能是点到点形式的,也可能是异步形式的。
这种想法听起来很简单,但是它确实能够显著降低复杂系统开发过程中的摩擦和冲突。传统的软件工程实践倡导设计良好的系统都应该具备高内聚、低耦合的特点。具备这些特性的系统更加易于维护,并且在面对变更时,也更加容易适应和扩展。
内聚度是用来衡量某个模块中的各个元素属于一个整体的紧密程度的指标,耦合度则是衡量一个元素对另一个元素的内部运行逻辑的了解程度的指标。在讨论内聚度时,罗伯特·C.马丁(Robert C. Martin)的单一职责原则是一种非常有用的方式:
将那些因相同原因而修改的内容聚合到一起,将那些因不同原因而修改的内容进行拆分。
在单体应用中,开发者会在类、模块、类库的层面来设计功能属性;而在微服务应用中,开发者的目标则变成了可独立部署的功能单元——要为这些功能单元设计功能属性。单个微服务应该是高内聚的:它应该只负责应用的某一个功能。同样,每个服务对其他服务的内部运行逻辑知道得越少,就越容易对自己的服务或者功能进行修改,而不需要强迫其他服务一起进行修改。
为了更全面地了解如何搭建微服务应用,我们会通过一个在线投资工具展开介绍。接下来,我们考虑这一工具的一些功能:开户、存取款、下单购买或出售金融产品(如股票)以及风险建模和金融预测。
我们再研究一下出售股票的过程:
(1)用户创建一个订单,用来出售其账户里某只股票的股份;
(2)账户中的这部分持仓就会被预留下来,这样它就不可以被多次出售了;
(3)提交订单到市场上是要花钱的——账户要缴纳一些费用;
(4)系统需要将这个订单发送给对应的股票交易市场。
图1.1展示了提交出售订单的流程,这可以看作整个微服务应用的一部分。可以看到,微服务有三大关键特性。
(1)每个微服务只负责一个功能。这个功能可能是业务相关的功能,也可能是共用的技术功能,比如与第三方系统(如证券交易所)的集成。
(2)每个微服务都拥有自己的数据存储,如果有的话。这能够降低服务之间的耦合度,因为其他服务只能通过这个服务提供的接口来访问它们自己所不拥有的数据。
(3)微服务自己负责编排和协作(控制消息和操作的执行顺序来完成某些有用的功能),既不是由连接微服务的消息机制来完成的,也不是通过另外的软件功能来完成的。
图1.1 应用中各微服务间的通信流程图:用户出售金融股票持仓
除了这三大特性,微服务还有两个基本特性。
(1)每个微服务都是可以独立部署的。如果做不到这一点,那么到了部署阶段,微服务应用还是一个庞大的单体应用。
(2)每个微服务都是可代替的。每个微服务只具备一项功能,所以这很自然地限制了服务的大小。同样,这也使得每个服务的职责或者角色更加易于理解。
微服务与传统的面向服务架构(SOA)在思想上的一个关键区别就是微服务负责协调系统中的各个操作,而SOA类型的服务通常使用企业服务总线(ESB)或者更复杂的编排标准来将应用本身与消息和流程编排拆分开。在SOA模型下,服务通常缺乏内聚性,因为业务逻辑会不断地被添加到服务总线上,而非服务本身。
思考一下,这个“在线投资系统”功能解耦的方式是很有意思的。它能够帮助开发者在未来面对需求变更时更加灵活。想象一下,当需要修改收费的计算方式时,开发者可以在不修改上下游服务的情况下,直接修改和发布fee服务。再考虑一个全新的需求:用户下单以后,如果订单不符合正常的交易方式,系统需要向风控团队发送告警。这也是容易实现的,只要基于order服务发出的事件通知开发一个新的微服务,让这个新的服务来执行这个操作即可,同样不需要修改系统其他模块。
1.1.1 通过分解来实现扩展
同样,可以思考一下如何通过微服务来对应用进行扩展。在《可扩展性的艺术》(The Art of Scalability)一书中,阿尔伯特(Abbott)和费舍尔(Fisher)定义了一个被称为“扩展立方体”的三维扩展方案,如图1.2所示。
图1.2 应用扩展的三个维度
单体应用一般都是通过水平复制进行扩展的:部署多个完全相同的应用实例。这种方式也称作饼干模具(cookie-cutter)扩展或X轴扩展。相反,微服务应用是一个Y轴扩展的例子,我们将整个系统分解为不同的功能模块,然后针对每个模块自己特有的需求来进行扩展。
注意
Z轴是指对数据进行水平分区:sharding。不管是微服务应用还是单体应用,开发者都可以采用数据分区分片的方法。但是本书不再针对这一主题进行展开。
我们回过头来再看一下这个“在线投资系统”案例的几个特点:金融预测是计算量特别繁重的功能,但是极少会被使用;复杂的监管和业务规则控制着投资账户;市场交易的规模是海量的,而且还要求极低的延迟。
如果采用微服务的方式开发,为了满足这些功能要求,我们可以针对每个问题选择最合适的技术工具,而不是像将方钉楔进圆洞那样死板地采用固有的技术和工具。同样,自治和可独立部署意味着工程师可以分别管理这些微服务所对应的资源需求。有意思的是,这也包含了一种与生俱来的减少故障的方式:如果金融预测服务出现了故障,它不会导致市场交易服务或者投资账户服务也产生连锁故障。
微服务应用具备一些很有意思的技术特性:按照单一功能来开发服务可以让架构师能够在规模和职责上很自然地划定界限;自治性使得开发者可以独立地对这些服务进行开发、部署和扩容。
1.1.2 核心原则
支撑微服务开发的五大文化和架构原则为:自治性、可恢复性、透明性、自动化和一致性。
工程师在开发和运行微服务应用时,应该运用这些原则来推动自己做出技术和组织决策。下面我们逐一研究。
1.自治性
我们已经明确了微服务是自治的服务,每个服务的操作和修改都是独立于其他服务的。为了保证自治性,开发者需要将服务设计得松耦合、可独立部署。
(1)松耦合——每个服务通过明确定义的接口或者发布的事件消息来与其他服务进行交互,这些交互独立于协作方的内部实现。比如我们在前面介绍过的order服务并不需要知道account transaction服务的具体实现方式,如图1.3所示。
(2)可独立部署——不同的服务通常是由多个不同的团队并行开发的。强迫所有团队按照同样的步调或者按照专门设计的步骤进行部署,都会导致部署阶段有风险更大、更加使人焦虑。理想情况下,大家想要这些服务都能够快速、频繁地发布小的改动。
图1.3 服务按照定义好的契约来通信以实现松耦合,契约隐藏了实现的细节
自治性也是一种团队文化。将各个服务的责任和所有权委派给有责任交付商业价值的团队,这是至关重要的。正如我们所确定的,组织设计会对系统设计产生影响。清晰的服务所有权有助于团队基于他们本身所处的环境和目标来迭代开发和做出决策。同样,当团队同时负责一个服务的开发和生产时,这种模式也能够促进提升团队端到端的主人翁意识,是非常合适的。
注意
在第13章中,我们将讨论有责任感和自治性的工程团队的培养及其在微服务中的重要性。
2.可恢复性
微服务与生俱来地具备故障隔离的机制:如果开发者独立地部署这些微服务,那么当应用或者基础设施出现故障后,故障将只会影响到整个系统的一部分功能。同样,部署的功能粒度越小,开发者越能更平缓地对系统进行变更,这样才不会在发布新功能的时候发布的是一个有风险隐患的大炸弹。
再次考虑一下那个“投资工具”,如果market服务不可用,系统将不能把订单发布到市场上。但是用户仍然可以创建订单,当下游的功能恢复以后,market服务能够把这些订单筛选出来继续发到市场上。
尽管将应用拆分成多个服务能够隔离故障,但它还是会存在多点故障的问题。同样,当故障发生的时候,开发者需要能够解释到底发生了什么问题以避免连锁反应。这包括设计层面的(在可能的情况下支持异步交互以及适当地使用熔断器和超时),还包括运维层面的(比如,使用可验证的持续交付技术和对系统活动进行稳定可靠地监控)。
3.透明性
最重要的一点,当故障发生时,开发者需要记得微服务应用是依赖于多个服务(而非单个系统)之间的交互及其表现的,而这些服务可能是由不同的团队开发的。不管在什么时候,系统都应该是透明的、可观测的,这样既可以发现问题,也可以对问题进行诊断。
应用中的每个服务都会产生来自于业务、运营和基础设施的数据、应用日志以及请求记录。因此,开发者需要搞清楚这些大量数据的含义。
4.自动化
通过开发大批的服务来缓解应用不断变大所带来的痛苦,这看似是有悖常理的。事实上,相对于开发一个单体应用,微服务确实是一种更加复杂的架构。通过采用自动化和在基础设施内保持服务之间的一致性,开发者可以极大地降低因这些额外的复杂性引入的管理代价。开发者需要使用自动化来保证部署和系统运维过程中的正确性。
微服务架构的流行与两种趋势是同时发生的—— 一种趋势是DevOps技术得到主流接纳,其中的典型就是基础设施即代码(infrastructure-as-code)技术;另一种趋势是完全通过API进行编程的基础设施环境(如AWS和Azure)的兴起。这三者的同时发生并不是巧合。后两种趋势做了大量的基础工作,这才使得微服务在小型团队里具有可行性。
5.一致性
最后,以恰当的方式调整开发工作是至关重要的。开发者的目标应该是围绕业务概念来组织服务和团队,只有这样安排,服务和团队的内聚性才能更高。
为了理解一致性的重要性,我们考虑一下另外一种方式。许多传统的SOA系统都是分别部署它们的技术层的——UI层、业务逻辑层、集成层和数据层。
SOA和微服务架构的对比如图1.4所示。
图1.4 SOA和微服务架构的对比
一方面,在SOA中使用横向拆分是有问题的,因为这样会导致内聚的功能被分散到多个系统中。新的功能可能需要协调发布到多个服务中,并且可能与其他在同一技术抽象层次的其他功能产生耦合,而这种耦合是不可接受的。另一方面,微服务架构应该偏向于纵向拆分。每个服务应该与一个独立的业务功能相匹配,并且将所有相关的技术层的内容封装在一起。
注意
极少数情况下,构建一个实现了某个特定技术功能的服务也是合理的,比如多个服务都需要与某个第三方服务进行集成,那就可以将这一集成工作封装成一个服务。
开发者应该时刻牢记是谁在消费这些服务。为了保证系统的稳定性,开发者需要在开发过程中有足够的耐心来保持所开发服务的向后兼容性(不论是显式地兼容,还是同时运行多个版本的服务),这样就可以确保不需要强迫其他团队升级或者破坏服务之间已有的复杂交互。
牢记这五大原则有助于开发者更好地开发微服务,进而使系统更易于修改、扩展性和稳定性也更强。
1.1.3 谁在使用微服务
许多组织已经成功地构建和部署了微服务,业务领域也跨越很大,涉及媒体(The Guardian)、内容分发(SoundCloud、Netflix)、交通物流(Hailo、Uber)、电子商务(Amazon、Gilt、Zalando)、银行(Monzo)和社交媒体(Twitter)。
上述领域的大部分公司都是采用了单体先行的方案。他们从开发单个大型应用开始,然后逐渐迁移到微服务中,以解决他们所面临的发展压力,见表1.1。
表1.1 软件系统的增长压力
比如,Hailo公司想要拓展国际市场,这对他们最初的架构而言已经成为巨大的挑战,此外,他们还想加快功能交付的速度。SoundCloud公司想要提升生产力,而当初的单体应用的复杂度阻碍了公司的发展。有时候,这种转变和业务优先级的改变是一致的:Netflix从实体DVD分发转移到流媒体内容领域。有些公司已经完全将它们最初的单体应用下线了。但对于很多其他公司而言,这还是一个进行中的过程,一个单体应用的周围有一系列更小的服务。
现在,微服务架构已经十分普及。很多早期采用者通过开源、写博客和做演讲的方式介绍了他们所采用的实践方案,因此越来越多的团队开始直接用微服务来开发他们的新项目,而非先开发一个单体应用,比如,Monzo已经开始将微服务作为其构建更良好的、更具可扩展性的银行系统的使命之一。
1.1.4 为什么微服务是一个明智的选择
有大量成功的业务是基于单体应用软件来开发的,我们能立刻想到的有Basecamp、StackOverflow和Etsy。在单体应用的世界里,我们有很多可以借鉴的东西,这其中包括传统传承下来的思想和看法、长期建立起来的软件开发实践和知识。那么,我们为什么还要选择微服务呢?
1.技术差异性为微服务开路
有一些公司同时采用了多种不同的技术,这使得微服务成为很自然的选择。在Onfido公司,我们引入一个由机器学习驱动的产品时,发现它和我们当时使用的Ruby技术栈并不完全匹配,于是开始开发微服务。即便开发者还没有完全决定使用微服务方案,运用微服务的一些原则也会让开发者在解决业务问题的时候有更大的技术选择范围。不过,技术的差异性并不总是这么明显。
2.开发冲突随着系统发展而增加
归根到底,这就是复杂系统的特点。在本章开头,我们提到,为了解决复杂的问题,软件开发者努力在设计提供高效、及时的解决方案。但是我们开发的软件系统天生就具有复杂性,没有哪种方法论或者架构能够消除系统核心的这种本质复杂性(essential complexity)。
但是,这并不是沮丧的理由。开发者还是可以有所作为的,可以通过采用恰当的开发方案来确保开发出的是一套良好的复杂系统,而从那种偶然的复杂性(accidental complexity)中解脱出来。
花时间考虑一下,作为一名企业软件开发者,开发者想要获得的是什么。丹·诺斯(Dan North)讲得很好:
软件开发的目标是持续地缩短交付周期来产生积极的商业价值。
在复杂的软件系统中,我们的困难是:面对变化却还要持续地交付价值,即便系统越来越庞大越来越复杂,也要在保持敏捷、节奏和安全的情况下持续交付。因此,我们相信一个良好的复杂系统应该能在整个生命周期内将冲突和风险这两个因素的影响最小化。
冲突和风险会限制开发速度和敏捷性,进而会影响交付商业价值的能力。随着单体应用越来越大,下面这些几个因素会导致冲突。
(1)变更周期耦合在一起,导致协作障碍并增大回滚的风险。
(2)在没有严格规范的团队中,软件模块和上下文边界含混不清,导致组件之间产生意料之外的紧耦合。
(3)应用的大小成为痛点:持续集成作业、系统发布(甚至是本地应用启动)都会变得越来越慢。
并不是所有单体应用都存在这几个问题,但是很遗憾,我们见过的大部分系统都有这三个问题。同样,在前面提到的那些公司中,这些问题也是他们共同的故事主线。
3.微服务降低冲突和风险
微服务通过三种方式来降低冲突和风险:在开发阶段将依赖进行隔离和最小化;开发者可以对单个的内聚组件进行思考,而非整个系统;能够持续交付轻量的、独立的变更。
在开发阶段将依赖进行隔离和最小化——不论是多个团队之间的依赖,还是已有代码层面的依赖,这种方法都可以让开发者的行动更加迅速。开发可以并行执行,还减少了单体应用中存在的对历史决策的长期依赖。技术债务很自然地被限制在服务的边界内。
和单体应用相比,微服务更易于独立构建和理解。这非常有益于提升成长中的组织的开发生产力。同时,微服务还提供了一种灵活且令人信服的模式,让大家可以积极应对不断增长的规模或顺利地引入新技术。
小的服务也是持续交付的重要推动者。在大型应用中,部署的风险是很高的,而且涉及漫长的回归和验收周期。通过部署更小的功能元素,开发者可以降低每次独立部署的潜在风险,更好地隔离对线上系统的改动。
现在,我们达成两个结论:其一,开发小的、自治的服务能够降低在开发长期运行的复杂系统时出现的冲突;其二,通过交付内聚的独立功能,开发者可以开发出一个面对变化时灵活、易扩展且具备可恢复性的系统,这有助于开发者在降低风险的同时交付商业价值。
这并不意味着每个人都应该构建微服务。当有人问“我需要微服务吗”时,如果真的有一个客观的标准答案,那是最好不过的,但是实际上开发者只能说“要视情况而定”。这个“情况”包括开发者的团队、开发者的公司以及开发者要开发的系统的特点。如果系统的领域范围并不重要,那么开发和运行这种细粒度的系统所增加的复杂度会超过开发者所获得的好处。但是如果开发者已经面临着本章前面提到的那些挑战,那么有充分的理由来采用微服务的解决方案。
一个发人警醒的故事
我们听过一个因实施微服务而出现问题的故事。这家出现问题的初创公司当时已经开始扩大规模,他们的CTO认为唯一的解决方案就是将应用按照微服务架构重新开发。如果开发者听到这句话还无动于衷,那么现在可以开始祈祷了,因为这将是噩梦的开始。
技术团队开始改造他们的应用。这花费了他们5个月的时间,在这段时间里,他们既没有发布任何新功能,也没有将任何微服务的功能发布到生产环境。在业务最繁忙的那段时间里,这个团队上线了他们这套新的微服务应用,上线后完全是一团乱麻,最终他们被迫将系统回滚到当初的单体应用上。
这种迁移给微服务带来了很坏的名声。很少有业务允许有好几个月的时间完全停滞新功能开发,也很少有业务会纵容一个新的架构方案突然地直接发布上线。好在这样的例子很少,大部分我们关注过的成功的微服务迁移都是一点一点推进的,会在架构愿景、业务需求、优先级和资源约束之间进行平衡。尽管这需要耗费更长的时间并需要做更多的技术工作,但是开发者永远不会希望成为上述故事里的主人公。