微服务实战
上QQ阅读APP看书,第一时间看更新

1.3 微服务开发生命周期

在个体层面,开发者应该熟悉每一个微服务——即便它比较小。为了开发一个微服务,开发者会使用很多相同的框架和技术:Web应用框架、SQL数据库、单元测试、类库等,这些都是开发者在开发应用程序时经常会用到的。

在系统层面,选择微服务架构会对开发者设计和运行应用的方式产生重要影响。纵览本书,我们会聚焦于微服务应用开发生命周期的三大阶段(图1.7):服务设计、将服务部署到生产环境中和功能监控。

图1.7 微服务开发周期的三大迭代阶段:设计、部署和监控

在这三大阶段中,每个阶段所做出的合理决定都有助于构建出具备可恢复性的应用,即便面对不断变化的需求和不断增加的复杂度,应用仍然具备可恢复性。下面我们逐一介绍这三大阶段,并思考用微服务交付应用所要采取的措施和步骤。

在开发微服务应用时,开发者需要做出一些设计决策。这些设计决策在开发单体应用时并不会遇到。开发单体应用时,我们通常都会遵循一些已知的模式或者框架,比如三层架构或者模型-视图-控制器(MVC)。但是微服务的设计技术还处于相对起步的阶段。鉴于此,开发者需要考虑以下问题。

(1)是从一个单体应用起步,还是一开始就使用微服务?

(2)应用的整体架构以及开放给外部消费者的接口。

(3)如何识别和划定服务的边界?

(4)服务之间是如何通信的?同步还是异步?

(5)如何实现服务的可恢复性?

有太多内容要讲。现在,我们会针对这些问题一一进行解答,以便开发者能够理解关注这些内容对于设计出良好的微服务应用的重要性。

1.单体应用是否先行

在开始采用微服务这件事情上,开发者会看到两种截然不同的趋势:其一,单体先行;其二,只使用微服务方案。赞成前者的开发者给出的理由是:始终应该以单体应用开始,因为在前期,开发者还没了解系统中各个组件的边界,而在微服务应用中,如果这一步出错的话,代价会大得多。换句话说,在单体应用中选择的边界与精心设计过的微服务应用中的边界并不需要保持一致。

虽然在开始的时候,微服务方案的开发速度会慢一些,但是能够降低未来开发的冲突和风险。同样,随着工具和框架越来越成熟,微服务最佳实践不再那么令人生畏,会变得越来越容易应用。不管开发者是考虑从单体应用迁移到微服务,还是直接开发一个新的微服务应用,这两条路都可以选,本书的建议都是有帮助的。

2.服务的范围划定

为每个服务选择恰当水平的职责——功能范围——是设计微服务应用中最困难的挑战之一。开发者需要基于服务提供给组织的业务功能对其进行建模。

我们来对本章开头的例子做一下扩展。如果开发者想引入一个新的特殊类型的订单,如何对服务进行修改呢?开发者可以通过三种方式来解决这个问题(图1.8):①对现有的服务接口进行扩展;②添加一个新的服务接口;③添加一个新的服务。

每种方案都各有优缺点,也会影响到应用中各个服务之间的内聚和耦合性。

注意

在第2章和第4章中,我们会研究服务的功能范围划分,并且会讨论如何在服务职责划分上做出最优的决策。

图1.8 为了划定功能范围,开发者需要决定是新设计一个服务,还是将这个功能划到现有的服务中去

3.通信

服务之间的通信可以是异步的,也可以是同步的。虽然同步系统更易于进行问题排查,但是异步系统的解耦性更高,能够降低变更的风险,还能让系统更易于恢复。但是这种系统的复杂度比较高。在微服务应用中,开发者需要在同步和异步消息之间进行平衡,以有效地对多个服务的行为进行协调。

4.可恢复性

在分布式系统中,一个服务不能完全信任它的协作方服务,这不一定是因为他们的代码很糟糕或者人为失误,还因为开发者不能想当然地认为服务之间的网络以及这些服务的行为是可靠的、可预测的。服务在遇到故障的时候需要能够进行恢复。为了做到这一点,开发者需要通过在出现错误的时候进行回退、对于一些不友好的调用方要限制其请求速率、动态寻找健康服务等方式来使服务具有防御性。

在构建微服务时,开发和运维是相互交织在一起的。开发者将服务开发完成以后就当甩手掌柜,让其他人来部署和运维的方式是行不通的。在由大量的自治服务组成的系统中,如果这个服务是开发者开发的,就应该由开发者来运行它。对服务的运行方式了解清楚,反过来有助于开发者在系统发展壮大以后做出更好的设计决策。

记住,应用的特别之处是它所交付的商业价值。这来源于多个服务之间的协作。实际上,开发者可以将每个服务所提供的特有功能标准化和抽象化,以保证团队聚焦于商业价值。最后,开发者应该达到一个阶段,也就是在部署新的服务时,不涉及任何客套的东西。如果做不到这一点的话,开发者将要投入大量的精力来“通下水道”[在英文中“通下水道”(plumbing)用来比喻一些价值不大的脏活累活],而不能为客户创造任何价值。

在本书中,我们会教开发者如何将已有的服务和新开发的服务可靠地部署到生产环境。为了能够快速地进行创新,部署新服务的成本必须是可以忽略不计的。同样,开发者应该将部署步骤标准化以简化系统操作,并在这些服务上保持一致。为了做到这一点,开发者需要做到两点:其一,将微服务部署的人为操作标准化;其二,实现持续交付的流水线。

我们已经听说过可靠的部署是很“单调”的。“单调”的意思不是说乏味无聊,而是说没有事故发生。遗憾的是,我们看到太多团队的情况恰恰相反:软件部署是一个压力很大的操作,而且病态地需要全员出动来完成。一个服务这样就已经很糟糕了,如果开发者要部署非常多的服务,随之而来的焦虑会让他们疯掉的。下面我们看一下如何通过这些步骤实现稳定可靠的微服务部署。

1.微服务部署的人为操作标准化

通常,每一门语言和框架都有自己的部署工具。Python有Fabric,Ruby有Capistrano,Elixir有exrm,等等。此外,它们自己的部署环境也很复杂,具体表现如下。

(1)应用部署在什么服务器上?

(2)应用有哪些其他工具的依赖?

(3)如何启动这个应用?

在运行环境层面,应用依赖(图1.9)是很广的,这其中可能包括类库、二进制和操作系统包(如ImageMagick和libc)以及操作系统进程(如cron和fluentd)。

从技术角度来说,服务自治一个非常大的好处就是差异化。但是差异化并不会让服务部署变得更加容易。没有一致性,开发者就不能把生产环境的服务部署方法标准化,进而会增加部署管理和引入新技术的成本。最差的情况就是,每个团队重复“造轮子”,每个团队都有自己与众不同的依赖管理、打包、部署和应用运维的方法。

经验表明,完成这项工作最好的工具就是容器。容器是一种操作系统层面的虚拟化方法,它支持在同一台主机上运行多个独立的系统——每个系统共享同一个内核,但是都有自己的网络和进程空间。与虚拟机数分钟的构建和启动时间相比,容器的构建和启动速度都要快很多,能够在秒级完成。开发者可以在同一台机器上运行多个容器,这样不但可以简化本地开发的复杂度,而且能够有助于在云环境中优化资源利用率。

容器将应用的打包过程、运行接口进行了标准化,并且为操作环境和代码提供了不可变(immutability)的特性。这使得它们成了在更高层次进行组合的强有力的构件。通过使用容器,开发者可以定义任何服务的完整执行环境并将它们相互隔离。

图1.9 应用对外暴露了一个运维API,这个应用有多种类型的依赖,包括类库、二进制依赖和辅助进程

虽然可以使用容器技术的许多实现方案和概念(除了Linux,还有FreeBSD的jails和Solaris的zone),但是到目前为止,我们所使用的最成熟和友好的工具是Docker。我们会在本书后面部分介绍这个工具。

2.实现持续交付流水线

持续交付是一种开发实践方式。通过这种实践方式,开发者可以在任何时间将软件可靠地发布到生产环境中。想象一下工厂的生产线:为了持续交付软件,开发者建立了类似的流水线,将开发者的代码从提交状态变成活生生的操作。图1.10所示的是一个简单的流水线。可以看到,每个阶段都能够向开发团队反馈代码的正确性。

图1.10 微服务的部署流水线概览

前面我们提到,微服务是持续交付的理想推动者,因为它们体积更小,这意味着开发者可以快速开发这些服务并独立发布。采用微服务的开发方式并不意味着就自动做到了持续交付。为了能够持续地交付软件,开发者需要关注以下两个目标。

(1)制订一组软件必须通过的验证条件。在部署流程的每个环节,开发者都应该能够证明代码的正确性。

(2)代码从提交状态发布到生产环境上的流水线实现自动化。

搭建一套可验证的、正确的部署流水线能够让开发者工作得更加安全,并和他们在服务开发阶段的迭代步调保持一致。在交付新功能时,这种流水线是一种可靠、可重复的流程。理想情况下,开发者应该有能力将流水线中的验证条件和步骤标准化,并在多个服务间进行使用,这样能够进一步降低部署新服务的成本。

持续交付还能降低风险,因为软件的质量和团队的交付变更的敏捷性都能够得到提升。从产品的角度来讲,这可能意味着开发者可以按照一种更精益的方式进行工作——快速验证开发者的假设并进行迭代。

注意

在第三部分,我们会使用免费的持续集成工具Jenkins的Pipeline功能来搭建一个持续交付的流水线。我们还会研究一些不同的部署模式,比如金丝雀(canaries)部署和蓝绿(blue-green)部署。

在本章中,我们已经讨论了透明性和可观测性。在生产环境中,开发者需要了解系统的运行情况。它的重要性有两点:其一,开发者想要主动发现系统中的薄弱环节并进行重构;其二,开发者需要了解系统的运行方式。

和单体应用相比,在微服务应用中,完全的监控是一件更加困难的事情。因为一个事务可能会涉及多个不同的服务;在微服务中,不同技术开发的服务可能会生成格式相反的数据;运维数据的总规模也要比一个单体应用高很多。但是如果开发者能够理解系统的运行方式,并且能够进行深入观测的话,即便微服务很复杂,开发者还是可以对系统进行高效的修改的。

1.发现潜在的薄弱环节并进行重构

不论是引入了程序错误、运行环境出错、网络发生故障,还是硬件出现了问题,都会导致系统出现故障。久而久之,消除这些未知的缺陷和错误的成本要高于快速和高效地响应所需要的成本。监控和报警系统使得开发者可以对问题进行诊断,并判断是什么问题导致了当次故障。开发者可以通过自动化的机制来响应这些告警,比如在另一个机房创建一个新的容器实例,或者增加服务的运行实例的数量来解决负载问题。

为了将故障的影响最小化并避免在系统内产生连锁反应,开发者需要采用一些支持服务局部降级的方案来设计和调整服务间的依赖。即便一个服务不可用,也不应该导致整个应用垮掉。认真思考应用中可能的故障点,承认故障总是会发生并做相应的准备,这是非常重要的。

2.了解数以百计的服务的行为

为了了解这些服务的行为,开发者需要在设计和实现这些服务时提高“透明性”的优先级。收集日志和一些数据指标,并将它们统一起来用于分析和告警。这样开发者在监控和分析系统的行为时,就可以诉诸于所构建的这个唯一的可信来源(single source of truth)。

我们在1.3.2节中提过,开发者可以标准化和抽象化每个服务提供的特有功能。开发者可以把每个服务看作一个“洋葱”。在“洋葱”的最里面,是这个服务所提供的特有业务功能。它的外面分别是各个工具层——业务指标、应用日志、运维指标和基础设施指标。这些工具可以让业务功能更易于观测。开发者可以在这些层之间跟踪每个请求,之后将从每层收集的数据推送到一个运维数据库用来进行分析和监控,如图1.11所示。

图1.11 一个业务功能的微服务由多个工具层所包围。请求会穿过这些工具层发送给微服务,而返回的结果也会穿过它们发送出去,这个过程中所收集的数据也会存储到一个运维数据库中

注意

在本书第四部分,我们会讨论如何为微服务搭建一个监控系统、如何收集合适的数据,以及如何用这些数据为一个复杂的微服务应用创建一个实时现场模型。