想实施微服务?先了解这些先决条件


2017 年 5 月,Phil Calçado 在布达佩斯 Craft 大会上呈现了有关微服务经济的演讲。他在演讲中简要讨论了一系列实施微服务的先决条件,这些先决条件是每个组织在考虑大规模采用微服务架构之前应该要具备的。在这篇文章里,Phil 详细地解释了这些先决条件。

先决条件的重要性

当你决定采用微服务,你将经历从单一应用到一个复杂系统的转变过程。在这个系统里,你会遇到很多无法预测的行为,因为团队和服务在持续地发生变化,它们被创建、被修改,然后被销毁。系统的快速变更能力为你的组织带来了巨大的好处,不过你需要确保你有一些安全护栏,否则你的交付会因为无穷无尽的变更而驻足不前。

这些护栏就是我们将要讨论的先决条件。如果不具备这些先决条件,或者只具备了其中的一部分,仍然可以成功地应用一项新技术,不过如果能够充分满足这些条件,不仅可以增加成功的几率,而且能够减少迁移过程中的噪音和疑惑。

这里列出的先决条件有点多,可能需要大量的投入,不过这要取决于你的组织的具体文化和基础设施。不过这些前期的成本是有必要的。微服务架构不会比其他类型的架构简单,你需要确保在采用微服务之前已经对投入所能带来的回报进行过评估。

先决条件真多

确实,实施微服务有很多先决条件。我试着让你们能够轻松地理解它们,不过你们要知道,对于小规模的服务来说,它们不是必需的。除非你们有大量的服务,否则我不认为你们真的需要微服务。

对于这些先决条件,你可能还没有成熟的想法或经验。即使是像 DigitalOcean 和 SoundCloud 这样成熟的公司,我们也只是从实现基本的条件开始。在开始的时候,我们经历了大量的探索和拷贝黏贴式的工作。

对于这里列出的每一项,你应该要有自己的想法,但不要被它们纠缠住了。你不一定要一下子给出长期的解决方案,你可以在这个过程中边做边学。在这个过程中,这个领域的技术也会更加成熟,它们当中有一些会变成最佳实践。

另外一种途径是忘记微服务,并专注于粒度更粗的服务架构上。更少的组件意味着更少的先决条件,随着你的工程组织和平台的日益成熟,你可以不断地减小每个服务的规模和范围。

先决条件清单

在之前的一个演讲中,我介绍了我们在 SoundCloud 采用微服务的经历,我非常感谢 Martin Fowler 在总结微服务先决条件方面所做的工作。Martin 和他的 ThoughtWorkers 同事总结的先决条件清单如下。

  • 快速配置:具备在短时间内配置好一台服务器的能力。
  • 基本的监控:生产环境的很多轻度耦合的服务在一起协作容易出现问题,而这些问题在测试环境难以被发现。所以我们需要一个有效的监控机制来快速地检测这些问题。
  • 快速部署:因为需要管理的服务太多,所以需要尽快地部署它们,不管是在测试环境还是在生产环境。

SoundCloud 在 Martin 发表他们的清单之前就已经开始了向微服务架构的迁移工作,不过我们得出的结论是相似的。在 DigitalOcean 实现大规模微服务的过程中,我更加肯定了这些先决条件存在的必要性。同时,我也发现了其他几点,它们对于成功实施微服务来说也是至关重要的。

  • 易于分配的存储
  • 易于访问的外围
  • 认证和授权
  • 标准化的 RPC

所以,我的关于微服务先决条件的完整清单如下(按照优先级从上到下)。

  • 计算资源的快速分配
  • 基本的监控
  • 快速部署
  • 易于分配的存储
  • 易于访问的外围
  • 认证和授权
  • 标准化的 RPC

计算资源的快速分配

Martin 说:

你应该具备在几个小时内搭建一个新服务器的能力。在云计算平台上,这是很基本的行为,不过它也不一定非要借助云服务。你可以通过自动化来实现快速配置——刚开始可以不用完全自动化,但在进入微服务架构之后需要实现完全的自动化。

Martin 在他的话里使用了服务器这个词,但在现今的 IT 环境,既可以使用真实的服务器,也可以使用虚拟机、容器、函数,或这些组件的组合。这也是为什么我使用了“计算资源”这个词,它是指任何可以为你提供 CPU 和内存来运行代码的资源。

十多年前,我们需要将应用程序部署到应用服务器上。这个层多路复用了单独的计算单元,多个应用程序或服务可以同时运行在服务器上面,而且这种部署架构沿用了多年。那个时候,每秒钟处理几百个请求就可以称得上是“互联网规模”了。企业可以最大化对硬件的使用,多个不同的服务可以运行在同一个硬件上,有时候甚至多个公司共享一个应用服务器,在上面运行多租户服务。

随着硬件的发展,计算资源成本在下降,本地数据中心和云端都可以提供这些计算资源。这样,对应用服务器的需求就被弱化了。尽管应用服务器为应用程序提供了拆箱即用的服务(比如安全、服务发现、管理面板等),但这些服务器太过笨重了。另外,随着流量的增长,我们从垂直伸缩转向了水平伸缩,而这些服务器在这方面并未能提供很好的支持。

基于上述的各种原因,我们才有了现今的部署架构,服务实例和计算资源之间是一对一的关系。

这种一对一的关系直接影响到了微服务架构。尽管针对微服务还没有最终的定义,不过当人们提及微服务时,他们一定是指很多小型的服务。按照上面的这种部署架构,我们就会有很多计算单元。这就要求计算单元的配置必须是自动化、快速和弹性的,以便满足微服务的需求。

我在 2015 年加入 DigitalOcean 的时候,我和同事们花了很多时间思考我们的内部系统——云控制面板。那个时候,控制面板由三个单体应用组成,它们运行在 Chef Data Bag 的虚拟机上。但我们很快发现这种方式不仅复杂,而且容易出错,无法伸缩。我们需要在转向微服务之前改进我们的配置能力。

我们决定使用容器和 Kubernetes 作为新的计算平台,我们在 2016 年的头六个月把所有的新服务部署到了新平台上,同时把遗留的单体也迁移到上面。我们因此可以在对架构做出变更的同时发布新产品。事实上,我们的监控和告警组件就是在新平台上开发出来的。

基本监控

Martin 说:

生产环境的很多轻度耦合的服务在一起协作容易出现问题,而这些问题在测试环境难以被发现。所以我们需要一个有效的监控机制来快速地检测这些问题。最起码要能够检测技术问题(计数器错误、服务可用性等),不过如果能够同时检测出业务问题(比如订单数的下降)就更好了。如果突然出现了问题,你要确保能够快速回滚……

微服务是一个复杂的系统,我们可以控制和预测的东西很有限。造成这种混乱主要是因为持续的变更,微服务几乎每天要部署好几次。

不过这些问题不是微服务独有的。事实上,十多年前,John Allspaw 等人在 Flickr 和 Etsy 就构建了一些工具用于解决这类问题,他们当时的架构还是单体。当时,Allspaw 在文档中记录了一些有关如何应付快速变更的方法:

换句话说,MTTR 比 MTBF 更重要。我并不是说故障是可接受的。我只是假定故障是一定会发生的,所以把时间和精力花在处理故障上要比花在如何避免故障上更有意义。我很同意 Hammond 的看法,他说:“如果你认为你可以避免故障,那么你就不会想着怎么提升处理故障的能力。”故障平均时间(MTBF)是指系统故障之间的时间间隔。修复平均时间(MTTR)是指修复一个故障所花掉的平均时间。简单地说,MTBF 会告诉你系统有多经常发生故障,而 MTTR 则告诉你故障在被检测到之后修复得有多快。在一个持续变化的系统里,你无法控制 MTBF,所以最好把时间和精力投入到如何改进 MTTR 上。

在你想办法改进 MTTR 的时候,你会发现,加快从故障中恢复的速度所能带来的好处越来越少。用于从故障中恢复的时间并非事故管理的唯一步骤,有时候,它占用的时间并不是最多的。我发现,事故管理当中最令人感到痛苦的是用于检测故障的时间(MTTD)。这个指标反映了故障从发生到被检测出来的时间。

你会因此认为需要把时间和精力投入在问题检测上。这个问题在各种架构中都存在,只不过微服务架构所面临的挑战更为严峻。在单体架构里,你会很容易地找出问题所在:你只要找出是哪个类或者哪个函数出现了问题。使用 NewRelic 这个工具就可以从代码级别找出问题的根源。

这些工具也可以用在微服务架构里,但前提是你要事先找到是哪个服务出现了问题。因为在微服务架构里,很多服务一起合作处理一个请求,你要确保你能够比较每一个服务,找出发生异常的那个,而不会被环境问题干扰。

所以,你要对整个微服务生态系统的所有服务进行基本的监控,而不仅仅是监控部分核心的服务。

在 SoundCloud,我们使用标准化的仪表盘和告警系统来监控微服务。我们保证每个服务都会暴露出一些常用的度量指标,然后使用它们来构建仪表盘。我们先是使用了 Graphite,后来改用 Prometheus,这样我们就可以比较不同服务的度量指标。

我们根据仪表盘来降低 MTTD,不过很快我们就发现这样子还远远不够。几十个小型的团队有数百个服务需要部署,你需要将出现的问题与各个团队关联起来,包括新代码的部署和基础设施的变更。我们构建了一个小型的服务,它能够返回由工程师和自动化工具所做出的所有变更的清单。我们修改了我们的部署工具,确保每一个变更都能够报告给这个服务,哪怕只是添加了一两个用于伸缩容量的服务实例。

在我们的事故检测流程里,检查最近发生的变更是首要的事情。

快速部署

Martin 说:

因为需要管理的服务太多,所以需要尽快地部署它们,不管是在测试环境还是在生产环境。这通常需要用到部署管道,有了部署管道,部署过程在几个小时内就可以完成。在早期允许部分的人工干预,但在后面需要进行完全自动化。

Martin 建议让这一项先决条件紧跟前面一项,因为从事故中快速恢复涉及到新代码或配置变更的部署,而且部署需要尽快完成。

我完全同意他的看法,不过我要补充一点。对于单体来说,一个笨重的非自动化部署流程是没有问题的。即使单次部署的成本很高,比如繁琐的步骤和出错的几率,但这些付出是能够得到相应的回报的,因为每一次部署都包含了大量的变更,这些变更影响到很多功能,而且是由多个团队共同开发的。

而对于微服务来说,事情却是这样的:一个功能的一个变更可能需要部署多个服务。你需要部署多个服务,但每个服务的部署成本都不高,风险也不大。正如 Martin 所说的,管道刚好适用于这种场景。

我们在 SoundCloud 时为这个先决条件挣扎了好一阵子。我们使用 Capistrano 和 shell 脚本来部署单体应用,流程很长,而且需要人工参与。文件里包含了复杂的部署指令,而且需要考虑到许多情况。

刚开始,我们决定使用任意一门编程语言来开发我们的服务,这样的话运行时团队也会感到很适应。这样确实有一些好处,不过它的不足在于我们无法预测该如何部署应用。我们有 JAR 包,有 Ruby 脚本,有 Go 二进制包……于是我们决定对它们进行标准化。

  • 每个服务需要在代码的根目录提供一个 Makefile 文件。这个文件里必须包含一个 build 目标,就算它只是要调用其他的构建系统,比如 SBT 或 Rake。
  • 在 make 命令执行完毕之后,部署工具会创建一个 SquashFS 文件,里面包含了目录、代码、资源文件和二进制包。
  • 代码里应该要包含一个 Heroku 风格的 procfile 文件,这个文件描述了如何执行每一个流程。在部署好 SquashFS 镜像后,需要更新流程的版本,就像在 Heroku 里所做的那样。

通过这种方式,我们可以对服务进行伸缩,不过整个过程需要太多的人工参与,因此带来了较高的风险。更糟糕的是,这些底层的原语无法直接支持其他的部署技术,比如蓝绿部署、canary 服务器或 AB 测试。为了解决这些问题,很多团队基于这些工具添加了自己开发的胶水代码。由于这些脚本属于辅助项目,所以代码质量参差不齐。我们生产环境的一些大事故就是因为这些脚本引起的。

当我们的服务规模从几十个发展到上百个时,我们开发了更好的部署工具。最大的不同在于,我们把部署从工程师的开发机移动到部署管道上。高度的自动化带来了更快的构建流程,正好满足了每天部署数百个服务的需求。

易于分配的存储

大部分从单体转向微服务的公司都有一个大型的数据库服务器,经过多年沉淀,这个数据库被调教得相当得当,它有多个复本,并且与其他系统很好地集成在一起,如搜索引擎和数据分析引擎。

不过,使用这个单体数据库仍然面临着一些挑战,这些挑战主要与 schema 的更新有关。在改变或移除数据表或数据表的字段时,要手动检查是否有其他代码依赖了旧 schema。多年之后,几乎所有的数据库重构模式都在这个单体数据库上得到应用,并催生了很多内部工具用于处理各种常见的问题。

然而,对于微服务开发团队来说,仍然需要重用公共的 schema。“新增一两个表、字段或试图不是什么大问题”,但类似的事情重复成千上万次,无异于在一点一点把工程师推向死亡的边缘。复杂的变更管理会拖你的后腿,你忙于协调服务之间的数据耦合,而实际上,这些服务之间本应该对彼此一无所知。

这种趋势在很多正在向微服务迁移的企业里蔓延,因为这些企业倾向于在配置和部署上做大量的投入,却忽视了为团队提供一个合理的存储系统。搭建一个 MySQL 服务器只要几分钟时间,但要将它放到生产环境里,仍然要注意许多事项,如复本、备份、安全、调优、监控等。而你的工程师可能在这方面没有什么经验。

在云原生架构里,你可以将这些运维任务外包给其他提供了数据库即服务的厂商。但在 DigitalOcean,我们做不到这点。我们曾经有一个中期的计划,就是为内部提供一个快速简单的 MySQL 数据库分配方案,但向微服务迁移的优先级就要因此做出一些让步。后来,我们并没有从头去构建一套复杂的工具,而是把时间花在整理和文档化相关的资料和脚本上,各个团队可以根据这些资料自己去搭建生产级别的 MySQL 服务器。

易于访问的外围

企业的第一个微服务通常是由一个人或一个小型的团队开发的,他们寻求的解决方案是为了解决他们在开发过程中遇到的问题。由于这个微服务涉及的领域太小了,开发人员可以在开发环境和测试环境很快地完成开发任务。但是在推向生产环境时,他们就会面临一些问题:如何向本地网络之外的用户暴露这些服务呢?

这个与数据库的问题类似,主要问题在于没有人事先考虑过这个问题。单体被暴露给公网的用户,它已经具备了各种功能特性,可以保护好你的服务,比如速率限定、日志、功能标记、安全、监控、告警、请求路由等。

对于微服务来说,常见的做法是通过不同的机器名或特定的路径来发布它们。

这种方式要求客户端(移动客户端或单页应用)协调发送给多个端点的请求。如果只有一两个服务,这种方式是可行的,但随着服务数量的增长,这种方式逐渐暴露出它的缺点。

不仅仅客户端的代码会变得复杂,向公网发布服务本身就不是个轻松的活儿。如果你重视你的用户,那么就要确保能够处理好互联网系统所有可能出现的问题,从恶意用户的破坏到非预期的流量高峰。如果每一个服务都要考虑这些问题,那么总体的成本就可见一斑。

这也是我们在 SoundCloud 开发第一批微服务时碰到的问题。面对数百万用户,我们很快意识到,我们需要对向外暴露的服务做一些限制。我们考虑过将所有的服务置于网关之后,但我们的过程团队很小,同时有许多功能需要交付,我们需要的是快速直接的解决方案。

于是,我们使用了一个单体作为我们的网关。

请求首先会达到单体网关,网关会调用后端的服务。这种方式有一定的用处,不过也存在一些问题。首先,它让网关和服务之间建立了耦合关系,一旦服务发生变更,网关也要做出改动,然后重新部署。另外,我们的网关运行在旧版本的 Rails 上,因为 Rails 对并发支持得不是很好,发送给新服务的请求处理几乎是串行的。随着服务数量的增长,请求的转发时间也随之变长。

后来我们采用了 BFF 模式,同时引入了一种更好的网关。

在 DigitalOcean 的时候,我们遵循了相同的模式,不过因为我们有 3 个单体,所以更早地使用了网关。

认证和授权

在微服务即将进入生产环境时,我们需要面临另一个非常重要的问题:微服务如何知道是谁在发送请求以及他们有哪些权限?最简答的办法就是要求每个请求里包含用户识别信息,然后将其与后端的认证和授权系统或数据库进行比对。

如果只有一两个服务问题不大,但如果有很多的服务,就会给授权系统带来很大的负担。

SoundCloud 的单体网关已经将用户的权限信息缓存在内存里。我们修改了客户端的代码,在 HTTP 请求头部加上这些信息。

在采用了外围网关方案之后,我们让网关向认证服务发送请求,把用户的 URN 和地理位置信息以及 OAuth 信息转发到下游的服务——在音乐行业,你所能访问到的资源不仅取决于你是谁,也取决于你所在的国家。

标准化的 RPC

最后一点是标准化 RPC,这是无可争议的,不过仍然很重要。微服务生态系统里的组件之间需要大量的协作,而且它们的行为很难预料。你要确保它们之间能够进行良好的交互,也就是说,它们需要能够理解它们之间发送的消息,也需要理解它们之间的协议。

在 SoundCloud,我们最开始使用 HTTP 和 JSON,但只使用 HTTP 和 JSON 并不能满足所有的需求。在 HTTP 和 JSON 之外,还有很多问题需要解决,比如如何发送认证信息,如何进行分页,如何进行跟踪,选择何种 RPC 架构风格,如何处理故障等。基于文本的协议也给我们带来了严重的性能问题,于是,数据密集型的团队就转向使用 Thrift。

对于采用了重度分布式架构的公司来说,我会建议他们将 gRPC 作为内部 RPC 的标准。除此之外,对于消息的序列化,比如将消息发送到 Kafka 上,你应该使用 protobuf,这样一来,消息的发送端和获取端都可以使用相同的序列化协议。

gRPC 和 protobuf 本身并不会提供你所需要的一切。在 SoundCloud 和 DigitalOcean 的时候,我们的一个团队专门围绕 RPC 构建了一套工具。最近,我们开始对 service mesh 感兴趣,它是“一个特定的基础设施层,让服务之间的通信更加安全、快捷和可靠”。因为长时间地使用 Finagle,我会更喜欢 linkerd。不过,我们总会有其他更多的选择。

马军伟
关于作者 马军伟
写的不错,支持一下

先给自己定个小目标,日更一新。