在上一篇文章《我们如何衡量一个微服务实施的成功》里,我们介绍了衡量一个微服务改造成功的七个特征,分别是:
- 很多个代码库,以及一一对应的流水线。
- 应用可以随时部署,并不需要等待。
- 大量的自动化测试。
- 更少的变更事故。
- 更低的发布风险。
- 可以按需扩展。
- 更多的自动化手段。
而本篇文章所介绍的案例,也符合这篇文章中对“微服务实施成功”的定义。不过,我们将通过以下五个方面来介绍我们是如何做到达到这七点的:
- 通过度量驱动架构的微服务化;
- 微服务平台的演进;
- 数据库的独立演进;
- 服务间的轻量级通信;
- 微服务的全链路跟踪;
微服务演进的技术背景
2013年,当我加入这个“微服务改造”项目中的时候,微服务远没有像今天这么火。那个时候我还不知道这种架构演进的方式叫做“微服务”。直到我离开这个项目把其中的经验带到其它项目里,才对敏捷,DevOps和微服务有了进一步的认识。
当时,我们刚刚协助客户把应用程序从自建数据中心迁移到亚马逊云计算服务(AWS)上,并通过 DevOps 等实践做到了按月发布。然而,新的挑战接踵而至。当客户决定开始做微服务之前,遇到了以下三点问题:
- 运维风险高,发布的时候需要整体发布。除了累积了应用变更以外,还有基础设施的变更。
- 开发效率低,由于单体应用存储在一个代码库里。导致各功能,项目,维护团队之间产生依赖,交付效率很低。
- 内部多个应用系统之间需要集成,但缺乏单一可信数据源(Single Source of Truth)。
作为很早就采用敏捷方式开发的企业来说,该企业很多敏捷实践都做的非常成熟,并往往作为澳大利亚敏捷成功的案例标杆。在我加入的时候,客户已经采用持续集成很长时间了。而迁移到 AWS,还需要将部署和运维部分自动化,从技术层面为 DevOps 做了很好的准备。那时候我们所依赖的仍然是用 Chef 去构建自动化的脚本进行部署,并开始采用 Ansible 这种技术做发布的标准化。
通过度量驱动架构的微服务化
我们所拥有的是一个基于 Spring 2.5 的 Java 遗留系统,各个系统之间由 ESB (Enterprise Service Bus 企业服务总线)串联起来。多个不同的业务线(Line of Business,LoB)拥有各自独立的产品组件,但都是基于同一套代码库。
这样的痛点很明显:
- 每个业务线都要有自己的子产品,但大家都基于同一份代码库。
- 每个业务线对自己产品的改动,会影响到其它的系统。
- 由于不同的系统的组件依赖于不同的环境和不同的数据库,所以部署所带来的风险很高。
随着开发人员的不断增加,以上的痛点越来越明显,我们发现很多工作因为开发阻塞而无法前行。于是就有了一个最基础的度量:发布阻塞时间。
当我们把敏捷看板构建起来,我们可以很清楚的看到需求分析、开发、测试的各环节时间。当时并没有采用 DevOps,我们的持续发布也仅限于 Staging(准生产环境),而各个环节内可以采用更具有生产力的实践我们可以缩短环节时间,降低浪费。但,阻塞时间则随着需求的增加而增加。
当阻塞时间在上涨的时候,主观的组织规划已经和应用系统规划不符了。于是,产品则根据业务线被划分成了三个产品,如下图所示:
于是有了三个代码库,和三条不同的流水线。每个业务线都负责构建自己的代码库和周边生态。这样虽然会带来代码的重复,让很多有 DRY(Don't Repeat Yourself )癖的架构师难以接受。但毕竟各产品未来要走自己的路,因此,为了让各业务线不阻塞,各自采用各自的代码库进行发布。于是原先的团队随着代码库的分离而分隔成了不同的团队。但是,Ops 团队却没有分隔开,而是作为通用能力继续支持着各产品线的发展。
这也就是康威定律所说的:“设计系统的组织,其产生的设计等同于组织之内、组织之间的沟通结构。”
这一次的拆分尝到了甜头,除了各个业务线开发阻塞时间缩短以外,各个业务线的产品的发布失败率和故障率也降低了。于是我们继续采用工具提升发布的成功率和效率,直到我们发现我们系统里的 ESB 成为了我们的瓶颈。于是我们开始进行了微服务的拆分。
我们预期的策略是采用“拆迁者模式”:即新建一套子系统,再统一的进行迁移,一步到位。能够这样做的前提是要有足够的自动化测试覆盖当前所有的业务场景。于是我们根据我们需要拆分的功能先编写自动化测试,在自动化测试通过的情况下可以保障我新编写的代码不会影响现有的功能,包括数据迁移后的测试。
然而,拆迁者模式最大的挑战来自于切换风险,为了避免切换造成的风险就要补全自动化测试,这样的成本是巨大的。除非很早就开始做好了责任独立的设计。否则,不要用拆迁者模式。
另外两种模式就是“绞杀者模式”和“修缮者模式”。前者有一个别名,叫做“停止挖坑”,意思就是不要在当前的系统里继续增加功能,而是采用松耦合的方式增加新的功能,使得老的功能慢慢被绞杀掉。这种模式的前提就是要确认遗留系统不再进行功能新增,只做 Bug 修复和例行维护。这样带来的变更风险最小,但演进时间较长。对于“新遗留系统”——刚刚开始转入维护不到半年的新系统,可以采用这种方式。
然而,我们所碰到的应用系统则是一堆运行时间超过5年的遗留系统。于是我们采用了“修缮者模式”。修缮者模式源于古老的软件工程格言:“任何问题都可以通过增加一个中间层解决”。
我们首先做了一个前后端分离,采用 RESTful API 作为后台,向 PC 浏览器和手机 App 提供数据交互。这样,无需为移动应用单独编写后台应用,只需要复用之前写好的 API 就可以了。这就是当前很多应用进行微服务改造的第一步。
到了后期,我们发现有些需要很多 API 需要进行转化,所以我们当时做了一个叫做 Syndication API 的东西,它实际上一个一个 API 的集合。通过反向代理重新暴露后端的数据和接口。这当时是为了能够给 Mobile 端提供数据所准备的过度方案。所以我们采用了 Ruby 这种可以快速开发的语言(讲真,Ruby 开发的项目都不大好维护,一部分原因是 Ruby 程序员水平差异太大,另一部分原因是 Ruby 版本和各组件更新的问题)。后来我们发现,完成一个功能需要给 Web 和 Mobile 端做重复的开发。所以,我们决定在前后端分离的基础上逐渐替换掉老的 Web 应用,即便它运行的很稳定。
为了降低风险,我们就再一次对 Syndication API 进行了拆分。把一个单体 API 集合根据 LoB 功能的依赖程度拆分成了 API 的组。这样,我们可以在用户端无感知的情况下修改后台的部署架构。这时候,虽然没有做到微服务,但我们通过各自的 API 分离出来了完整的业务并在遗留系统之间创建了一个适配层隔离风险。以后我们只需要写代码替代原有的逻辑就可以了。
这样,我们构建一个防腐层,将微服务和遗留系统隔离开。对新的微服务化组件采用独立的代码库,进行持续交付和持续部署。那个时候没有 Docker 这样成熟的技术,也没有 Spring 全家桶这么便利的框架。所以我们选择了 Ruby 和 Ansible 进行开发和部署。大体过程如下:
- 构建新的自动化测试:采用 Cucumber ,Capybara 和 Selenium 来实现前端测试。Junit 和 Moco 做后端测试。
- 构建隔离层:通过把原先的实现类(Class)抽象成接口(Interface)来做到松耦合,并在 Spring 中注册不同名称的实现。
- 增加特性开关:一部分在 Spring 的
applicationContext.xml
文件中实现,一部分用 Redis 在线实现。 - 用自动化测试驱动微服务的开发:有了之前的自动化测试,我们只需要将特性开关打开,就可以使用新的类。哪里的测试失败了,就提示我们哪里应该被修复。当然,在过程中会增加新的自动化测试。
- 发布运行:采用 AWS 和 Ansible 构建轻量级的镜像发布。
- 删除特性开关和遗留旧代码:上线6个月之后,我们把接口保留住,之前的实现类相关代码和配置从源代码中删除。
选择模块也有讲究,有以下几个策略:
- 前后端拆分
- 把经常变更的部分拆分
- 把公共的部分进行拆分
- 根据业务拆分
- 根据领域模型拆分
当时我们是根据系统菜单的导航进行拆分的,因为用户菜单本身就是分割好的业务。这是大粒度的拆分,然后就可以定下原则,针对不同的技术特点和运维特点进行小粒度的拆分。
此外,在选择拆分策略的过程中,我们涉及两方面的估算(度量),一方面是成本,一方面是收益。
成本里除了人员的成本以外,还包括风险。在度量风险之前,要问这几个问题:
- 假设我们的拆分一定会失败,这个失败会带来多大的影响?
- 假设我们要修复,成本最低的修复方案会花多少成本。
这里的影响我们可以把“故障范围”、“故障时间”、“单位范围故障成本” 三者相乘得出来一个估算值。然后再加上微服务的开发成本和回退成本三个部分,构成微服务开发的总成本。这种算法虽然简单粗暴,但也能说明问题。
而微服务带来的收益,则是将上文度量的阻塞时间(以人天为单位),乘以研发人数和系统运行时间构建出来的一个函数。而这个函数也可以说明微服务带来的系统规模增长的投资回报周期。在不同的团队里,这个回报周期都是不一样的。一般都在 6 个月以上。也就是说,投入微服务拆分后 6 个月,微服务所带来的投资产出才会持平。
“重要的不是技术有多先进,而在于你清醒的认识到新技术引入带来的成本和收益。”
有了以上几方面的度量,我们就对相应系统和子系统是否要进行微服务有了一个相对清晰的认识。并不是所有的微服务拆分都划算,有些系统保持原样,采用绞杀者模式慢慢迁移可能更好。如果采用拆迁者模式,急功近利带来的问题可能更多。所以,一个成熟的微服务架构应该是一个混合型的系统,如下图所示:
上图 是一个分层架构,最上面一层是前台,其次是后台,后台之间的不同微服务也采用了不同的语言。有些微服务则仅仅是一个对外的 API,有些系统并未转化成微服务。例如上图左侧的 Monolith,就是一个 Java 的单块应用。其中MS 代表 Microservice。
我遇到的很多想采用微服务的团队往往纠结于自己“是不是”一个微服务架构。但我觉得你可以在架构中先“有一个微服务”,看看它带来的投入和回报,再考虑如何扩展自己的微服务。
微服务平台
在微服务的演进道路中,随着微服务的数量增加,微服务的治理成为了一个突出的问题。而随着技术的进步,微服务的治理思想和工具也发生了变化。我们的微服务演进经历了以下四个阶段:
- 阶段一:自部署的生命周期管理工具
- 阶段二:公共的微服务管理工具
- 阶段三:基于 Docker 容器的微服务管理工具
- 阶段四:基于 容器平台的微服务管理工具
阶段一:自部署的生命周期管理工具(2013年之前)
在微服务实践早期的时候,我们希望我们新部署的应用可以没有任何依赖。再加上 DevOps 实践的影响,我们期望每个微服务团队都是一个全功能的 DevOps 团队。
我们会在代码库里建立一个 deploy 文件夹,把自动化的部署脚本放在里面,集成一些 APM 工具和日志收集代理。并根据不同的环境构建配置文件。利用基础设施即代码技术自动化完成,并且集成到持续部署流水线中进行管理。那时候并没有 Docker 这样方便的技术。于是,我们采用一个代码库,一条流水线,一个虚拟机镜像的方式进行部署。
这时候微服务的实践还有一个重要的点是去容器化,这里的容器指的是像 WebLogic 和 Tomcat 这样的 Java 应用容器,而不是 Docker 这样的容器技术。对于 Java 来说,当时可以选择 DropWizard,它可以把 Web 应用打成一个可执行的 JAR 包运行,现在的做法就是用 Spring Boot。也可以选择 Jetty,用 Gradle 把它集成到 build 文件里,作为一个任务运行。如果你是 Ruby,就可以用 Ruby On Rails 或者 Sinatra 这样的框架来运行。虚拟机镜像里只需要安装一个语言运行时和固定的应用启动点就可以了。
这样就做到了独立开发、独立部署。降低了应用复杂度并减少了以来,这可以使得团队更加高产。
阶段二:公共的微服务管理工具(2013年——2014年)
随着微服务数量的增长,你会发现每个微服务工程都有同样的基础设施管理部分。于是我们通过代码重构,把公共的部分提取出来,变成了一个公共的微服务管理工具,它支持微服务的全生命周期管理。这样的工具一般是用 Gradle (Java应用)或者 Rake (Ruby 应用)来构建的。他们可以做到微服务从构建(build)到部署(deployment)的一系列任务。
通用的管理工具的另外一面就是应用的开发规范。两方面必须同时存在,否则这样的工具会失去意义。所以,微服务管理平台的意义在于不光能够节约很多重复建设的成本,更能够将最佳实践变成一种制度,进行快速复制和推广。
你可能会有疑问,微服务本质上是要做去中心化的。但这么做不就催生了新的中心化吗?
这个问题,对了一半。关键的部分在于工具和应用的依赖程度。你可以把这一类工具单独当做一个特殊的“微服务”来对待:它应该和其它应用之间松耦合的,提供所有应用都共享的功能,但必须要和业务部分独立开,无侵入性的和微服务应用程序组合在一起。
催生这个工具诞生的另外一个原因就是组织内存在独立且共享的运维团队。大部分的企业应用系统的开发和运行平台维护是不同的两个团队,这就导致了开发和运维的分离,这样不利于 DevOps 的组织形成。然而,在我的课程《DevOps 转型实战中》,我介绍了这种组织形式下实践 DevOps 的方式,就是让运维团队的成员去不同的开发团队内“轮岗”:让每一个微服务团队在改造的过程中成为一个 DevOps 团队,作为一个运维团队的使者。在改造结束后回归到运维团队,将经验分享并带给其它的开发团队,这样可以减少组织中的运维浪费。
然而,把业务代码和运维代码解耦并不是一件很容易的事情,当时也是微服务推广的一个技术难点,困扰了很多人。直到 Docker 的出现。
阶段三:基于 Docker 的微服务管理工具(2014年——2016年)
Docker 成为了 DevOps 和微服务事实上的推手。可以说,没有 DevOps 的实践和 Docker 这个工具,就没有今天微服务的流行。
作为最早将 Docker 应用到生产环境的团队,我们最初仅仅是把 Docker 作为一个“轻量级的虚拟机”(Light Weight Virtual Machine)来看。它可以快速的构建一个干净、稳定的运行时环境(Runtime Environment)并且做到快速启动和水平扩展,十分让人兴奋。
于是,我们把每个应用都通过 Docker 封装。由于那时候没有 docker-compose(其前身是一个名为 fig 的工具)和 k8s 的管理平台。也只能用 Shell 脚本来管理,然而 Shell 脚本的结构化能力有限,于是我们的团队的一个运维工程师在周末用 Python 自己撸了一个管理工具出来。那时是我们能找到的唯一容器编排工具,可惜作为最早的 Docker 编排工具没有开源。于是就有了 fig 这样相同的工具出现。随着 Docker 后期再开源社区的一系列收购工作,Docker 的开发话语权慢慢被 Docker 公司回收,这都是后话。
我们把前端代码和后端代码封装起来,通过 docker 的 link 功能构建了统一的内部 API 接入点。这样就可以减少自动化部署代码中不同环境之间的配置。也可以很轻易的把运维和开发解耦,降低 DevOps 团队中的沟通成本。
然而这里有个非常不好的实践就是不停的构建 Docker 镜像。从运维的角度说,不断的构建 Docker 镜像会导致不必要的网络流量和存储资源,特别是很多按流量和空间付费的云计算服务。另一方面,构建镜像会增加部署流程时间,虽然 Docker 的构建和下载会启用缓存,但是一些基础镜像的变更就会带来所有镜像的重新构建。
这就是容器的状态化,状态化的容器不算是一个好的实践。但是,这也比没有容器之前的状态要好很多。
所以,我推荐把 Docker 容器当做一个稳定的运行时,不要频繁的构建镜像。通过 Volume 参数将宿主机中的文件挂载到容器里,这就是将 Docker 镜像里的状态移除,做到容器的无状态化。这样,容器镜像会稳定且高效。
阶段四:基于容器平台的微服务平台(2015年至今)
当微服务通过 Docker 承载之后,微服务借由 Docker 的快速扩展和运维隔离两项优势快速发展起来。但同时也带来了很多问题。就像我在前文中说的,微服务本质上把应用的内部复杂性转化成外部复杂性。并且用具备弹性的基础基础设施来承载外部复杂性。很多公共组件,例如微服务的注册和发现、一致性、日志、APM等慢慢的容器化。包括容器的编排和资源伸缩等基础能力、以及数据库等都用容器化统一起来。
这样,容器就成为了统一的抽象,作为所有应用的通用运行链接格式(Generic ELF,Executable and Linkable Format )和通用运行时(Generic Runtime)。
这也让我们从“胖进程”(Rich Process)的角度来重新看待容器技术,和“轻量级虚拟机”不同。我们把 Dockerfile 看做是语言无关的源代码,Docker build 看做是编译和链接,Docker 镜像看做是构建出的可执行文件,Docker 容器看做是进程。
到了这个阶段,容器作为一等公民,就需要一个操作系统。而且,这种操作系统是跨平台的。无论是物理机,虚拟机还是云计算实例,都可以无缝的和容器进行集成,这时候就需要统一的容器解决方案。
于是,三大容器编排平台应运而生,分别是基于 Docker Swarm 的 Docker EE,Mesosphare 和 Kubernetes。当然,Kubernetes 已经作为绝对的赢家,并且在 CNCF 的支持下,形成了完整的微服务生态圈。所以到了现在这个阶段,CNCF 的相关技术才是微服务发展的方向,每个工具环环相扣,形成了一个完整的微服务生态。
阶段五:标准化的微服务架构参考模型 ?
随着微服务生态的渐渐成熟,特别是在开源社区和 CNCF 的作用下。微服务的基础设施基本上已经不可能有太多的创新点,大部分的优秀实践都被整合成为了开源项目,逐步从 CNCF 孵化毕业。2017年,很多实践微服务的企业开始构建自己的微服务化产品。未来,微服务的基础设施会标准化并且将提供更加透明且廉价的云原生解决方案。随之而来的是公共领域的应用解决方案,例如用户管理和登录这样的基础组件会第一个被微服务化。
毕竟,我们没有必要重新发明那么多轮子……
独立存储/混合存储
成功的微服务的另一个特征就是数据库可以进行拆分和按需扩展,这样你可以独立维护。但是如果你的数据库性能足够好或者你数据库结构并不是很好,你可以保持这种方式。然而,很多变更频繁的系统会有很复杂的数据模型,而这样的数据模型往往是制约应用架构演进的最大瓶颈。特别是,很多关系型数据库的严格结构约束着应用的方式。
当我们通过领域驱动设计重新对应用架构进行划分后,你会发现数据模型往往是一团糟:存在着很多重复的表和重复的记录,包括一些临时的方案,表之间的关系异常复杂,做一点改动都会“牵一发动全身”。
这时会有两种解决方案,但无论哪一种方案,都会带来冗余数据和数据迁移。但无论如何,都要坚持“单一可信数据源”,也就是 Single Source of Truth 原则。
第一种方案比较简单,就是采用 NoSQL 数据库单独承载一个或多个微服务的数据访问请求,例如 MongoDB,CouchDB 等,用 key-value 这种松散的结构来存储数据。并把对数据模型的约束放在应用代码里而不是数据模型的定义里。这样就可以更灵活的组织数据,而不用担心数据在数据库内的定义。我们往往把经常变更结构的数据存放在 NoSQL 数据库里,而稳定的数据结构存储在关系型数据库里,然后进行数据库迁移。
第二种方案比较复杂,就是做关系型数据库的迁移。这种比较复杂,但一定要记住一点:不要变更遗留库!不要变更遗留库!不要变更遗留库!重要的话说三遍。
原因很简单:不值得,特别是在一个经历了不同架构师和开发人员流动的遗留系统上。你会发现你在变更的时候会遇到多方的阻力,而且验证方案特别费时费力。这时候如果不停劝阻强硬执行,只会“大力出悲剧”。
这时候,我们只需要将应用和对应的数据版本化。为新应用建立一个数据库,某个时间点之后的数据,全部进入新库,老数据库相对稳定,只读不写。这时候无论数据库和代码,一定会有冗余数据,但不要紧,你只要确定新库是最终的单一可信数据源即可。你可以增加增加新功能,让用户自己做数据迁移,迁移后要把老数据库中对应的数据删除,或者打标记。经过一段时间后,剩下的遗留数据就会越来越少,这就是数据库迁移的“绞杀者模式”。
另外一种是数据库迁移的“修缮者模式”:先构建一个新的 API,将它对应的数据结构作为一个数据模型进行直接查询和存储,如果查询不到,则到老的数据库中进行查询,然后组装一份新的数据存储到新库中,再进行操作。
另外一个注意点就是:应用和数据库一起迁移!应用和数据库一起迁移!应用和数据库一起迁移!
很多架构师在迁移的时候为了快速将功能上线,不迁移数据库,仅仅变更 API,会在代码中保留一部分兼容性代码。这就留下了技术债。而且,这种兼容性妥协会带来 n 个中间过渡版本,永远到达不了彼岸。
并不是说数据库的拆分是必须的。刚开始,我们往往会单一数据库,多微服务访问的形式。到后来,我们就会把它拆分成右边的形式。
你可能会问我,为什么一个库会被多个微服务访问,不是应该一个微服务对应一个库吗?要么就是这几个微服务不应该被拆分,而应该合并?
微服务和库的对应关系受以下几个因素影响:
- 不同的微服务的 SLA 是不同的。为了做到按需扩展,有必要在多个微服务可能会访问同一个库。
- 减少冗余数据,为了保证“单一可靠数据源”并减少冗余数据,几个微服务需要访问同一个库。
- 业务特性和性能特性。这方面主要的策略有读写分离、动静分离等。
- 数据库承载着服务间的通信功能。
在数据库拆分的时候,要注意数据的冗余和一致性问题。为了提升效率,独立数据库里的适当冗余是必要的。但是,如果为了避免冗余,而不断跨库和跨 API 查询很密集的是偶,很有可能你的微服务拆分错误。毕竟,跨网络的访问性能远远不如在一个进程上下文中的性能。
从数据库的查询频率和性能来进行数据库的拆分和重组也是拆分微服务的技巧之一。一般的原则是:“先拆表,后拆库。关联查询先拆分后合并”。这会是一个反复校准的过程,很难一次成功。另外,我比较推荐把关系写到应用逻辑里而不是数据库里,这样就可以减少一些底层的依赖,可以隔离数据库和表集中管理带来的连锁问题。
轻量级服务间通信
去 ESB 化
就像前文所述,最早的时候我们采用 ESB (企业消息总线,Enterprise Service Bus)集成各个异构的系统。通过统一的接入方式将不同的系统集成到一起。在当时,这是流行的 SOA 架构的核心理念。但问题随之而来:随着集成到 ESB 上的系统越来越多,你会发现 ESB 要处理各种系统之间的协议转换和数据转换就会带了很多额外的工作量和性能开销。最主要的是 ESB 成为了核心的依赖,如果 ESB 维护,所有的服务都会被阻塞住。慢慢的,松散耦合的不同系统和服务会因为 ESB 耦合又成为了一整块应用。
然而,当时并没有新兴的解决方案能够解决这些痛点,直到移动互联网技术的兴起。当我们开始做前后端分离的时候大量采用基于 Ruby 和 Scala 的轻量级应用框架和 json 这样的轻量级协议,这些低成本的快速项目给客户和我们都带来了信心。于是我们开始把集成在 ESB 上的系统根据业务线逐个剥离下来,使每一个单独的应用可以独立发布和部署。
这样,随着引入轻量级的框架和协议不断引入,我们就离传统的,重量级的 SOA 越来越远。这是后来我们“成为”微服务的重要标志之一。
REST 还是 RPC ?
REST 还是 RPC,这是常见的两种微服务通信风格,最重要的区别是你看待微服务的方式。
我们并没有采用 RPC,而采用了 REST。我们把微服务抽象成为了资源,基于资源对外提供服务。这样,我们很清楚应该如果对外暴露什么内容,同时应该隐藏什么细节。这点很可能是和 AWS 学习的,它们把每一个服务都抽象成了一个状态机,加上我们又基于 AWS 进行部署,所以 REST 成为了一个自然的选择。
RPC 是另外一个选择,但 RPC 的下述缺点是我们关注的:
- 调试复杂性,RPC 有时候会隐藏很多细节,这些细节会变成日后调试的深坑。
- 脆弱性,使用 RPC 的前提是“网络是可靠的”,事实上网络并不会那么可靠,尤其是异构系统中。
- 技术强绑定,例如 Java RMI 这种技术,就需要双方使用同样的技术栈,这实际上破坏了微服务架构本身的好处。
这也并不是说 RPC 一无是处,RPC 很容易使用,你很容易就可以给 RPC 的客户端和服务端打桩并开始开发。而且很多 RPC 的协议本质上是 二进制的,能够自己处理序列化,性能会比 REST 高很多。
采用 MQ 通信
发布者 - 订阅者 本身就是一个比较理想的系统间松耦合的通信方式。然而,ESB 的侵入性实现使之成为了负担。然而基于消息队列的方式则会轻量很多。
微服务倾向于统一的数据交流格式和异步的调用,减少了阻塞的发生并提高了系统的性能。于是我们就将 ESB 换成了消息队列和 API 调用。而消息队列和 RESTful API Call 本身就算是一种系统拆分的重要方式。它把系统的内部依赖转化到了外部。
一个成功的微服务拆分,服务间的依赖会少。与此伴随着应用系统内的同步调用减少,异步调用增多。采用异步调用取代同步调用也可以看作是微服务拆分的一种方式。
现在,大家都会采用 Kafka 作为消息队列。但是,请警惕不要把 Kafka 变成了另外一种 ESB。
采用数据库进行通信
虽然我并不推荐这种数据传递的方式,因为这本质上就是一种底层的耦合。但采用数据库的表作为一种过渡手段在复杂的数据库拆分的时候起到了关键作用。然而,在这种模式下一定要确立“写的微服务”和“读的微服务”分离,否则会变成双向依赖。一般会采用 CQRS (命令查询责任分离)模式来拆分微服务。诸如“增、删、改”这样的命令服务要和查询服务分开来。
选择最适合你的通信方式
这里需要说明的是,我们要从业务场景和系统的实际情况出发来选择合适的通信场景。微服务倾向于透明的格式和轻量级的协议。重要的是,我们选择方法时要尊重“网络不总是很稳定”的事实来进行设计。
另一方面,为了快速定位风险和问题,我们可以通过消息队列和 RESTful API 的方式是把内部的依赖暴露到了外部,使得内部的“暗知识”可以充分暴露。减少了系统的风险和修复时间。
而如果你的微服务通讯太频繁了并带来了额外的风险和成本,有可能你的微服务就已经拆错了。在这种情况下,你可能需要考虑合并微服务。这取决于系统的性能和稳定性,也取决于你的维护成本。
一个成功的微服务拆分也带来了开发的独立性,由于你的服务是异步调用的,应该可以独立部署和发布,没有其他的依赖且不会造成额外的影响。如果你的微服务拆分之后,开发的流程仍然很长,就要考虑组织流程上的拆分了。
微服务全链路监控
上图这是我们采用 NewRelic 做的微服务的监控,可以看到左边是一些业务系统,接着中间的微服务,最后右是数据库。为了保密,我把微服务的名字用蓝色的框覆盖了,你可以看到。这是一个 NodeJS + PHP 和 MySQL 的混合架构。
你的微服务运维也需要有微服务相互的健康检查的连接图。以前可能只需要关注一个应用的表现。而到了微服务架构后,你需要关注每一个节点。只有一条依赖上的所有服务都健康了才算健康,但是中间如果有一个不可用的话,哪怕它的失败率很低,如果你的依赖链很长的话,你的应用的健康度就是这些节点的可用率相乘的结果。比如你有三个依赖的微服务,他们的可用率都是99%,那么业务的可用率就是 99% * 99% * 99% = 0.970299,也就是 97.02%。但三个微服务如果不相互依赖,它们的可用率仍然是 99%。所以,服务间依赖越多,系统的风险越高。这也从数学的角度上证明了微服务的优势。
当微服务数量多了之后,我们就不能考虑单个应用的成功性,我们考虑的是这一块集群的失败率做整体监控,得到系统监控的可用率。当原先的系统内部依赖暴露到外部之后,运维的工作就不仅仅是关注之前的“大黑盒”了,这就需要开发和运维共同合作,这也是微服务团队必须是 DevOps 团队的主要原因。
此外,从开发的角度讲,由于一个业务跨多个微服务,我们很难跟踪业务的使用情况。我们所采用的方式是为每一类事物设计状态上下文。包括 id 和错误编码,错误上下文,错误编码根据类别分为:0 正常,正数:服务请求端错误,负数:服务响应端错误,然后根据每个场景设计回滚流程。你也可以通过 HTTP 状态来区分,HTTP 200 就向下传递。HTTP 4XX 或者 5XX 就向上回滚并记录错误信息。这样你就知道是哪个微服务出现了错误。
另外一个技巧就是把自动化测试当做生产环境的业务监控,我们在生产环境通过运行功能性的自动化测试来验证生产环境业务的正确性。你只需要为它分配一个特殊的账号就可以了。
我们做微服务最简单的路径一般来说都是先做投入产出的分析,然后可能做前后端的分离,并做到前后端的持续部署。这里的要点是需要采用自动化的方式做好基础设施治理,如果没有 DevOps 的组织这一点就很难。我们通常的做法是给微服务单独开辟一套新的设计,并把微服务团队单独剥离,因为这是两种不同的文化和流程。最后做到全功能的 DevOps 团队和微服务模板,而这些基础设施的自动化是可以复制的。可以为我们批量拆分微服务提供一个很好的开始。
技术只是一个方面
微服务的转型往往是一个结果,而不是原因。特别是微服务被当做一个“技术”被引进的时候。然而,技术方案的落地是离不开人的,他是组织和技术相互结合发展的一个结果。如何为微服务的运营提供高效的组织结构?请看下一篇《成功微服务实施的组织演进》