微服务架构基本原理学习笔记(二)

上一篇:微服务架构基本原理学习笔记(一

三、微服务架构

  从一个已有的单体架构的应用程序开始进行微服务架构的重构往往是一个不错的选择。随着业务量和功能的增加,我们可以考虑使用微服务架构来扩充应用程序中原有的功能,或者每次添加新功能时,都为其创建一个新的微服务。这比从一开始就选择使用微服务架构进行设计要相对容易一些,因为微服务架构的好处通常不会体现在小型项目中。所以,考虑让项目持续迭代一段时间,直到我们能够非常清晰地确定服务的边界,通过微服务架构来进行功能的划分。

  因此,对于每一个微服务,我们都需要明确它们各自的职责,并定义公共接口。

每个微服务管理各自的数据

  前面我们已经介绍过,微服务是自治的并且可以单独运行和部署,而实现这一特性的关键就是确保每个微服务都拥有自己的数据存储。也就是说,微服务架构中不允许出现多个微服务共享同一个数据的情况。任何想要访问其它微服务中数据的情况都应该通过公共接口来完成。

  你可能在想,如果没有一个中心的共享数据库,如果保证数据的一致性呢?尤其是对于事务而言,数据的一致性至关重要。在微服务架构中,我们不能在单个数据库事务中去更新来自不同的微服务中的数据,我们要么通过较为复杂的分布式事务来实现这一功能,要么通过微服务自己来保证数据的一致性。而后者是通常较为推荐的做法。这就意味着,我们无法保证在一个相对较短的时间内数据的一致性,而是需要等待一段时间之后才能获得整体状态一致的数据。具体的操作是,当其中一个微服务的数据更改时,在一个短暂的时间内其它微服务中的数据无法与其保持一致,这个时候,你需要有一种机制能够应对这种暂时性的数据非一致性,直到所有微服务中的数据最终获得一致性。在微服务中使用缓存机制是一个不错的选择,这可以大大减少从多个微服务聚合数据以执行单个操作的成本。

  查看Github上的eShopOnContainers的微服务架构图,我们可以看到其中不同的微服务都有各自的数据存储。例如Identity微服务使用了SQL Server数据,Ordering微服务也使用了SQL Server数据库,但它和Identity微服务使用的不是同一个数据库,它们之间也不能相互访问。Basket微服务使用的则是完全不同的Redis缓存数据库。

  现在考虑一个实际场景,Ordering微服务中有一个OrderItem实例,它代表用户的一个订单,其中包括ProductId、ProductName和UnitPrice。Catalog微服务中有一个CatalogItem实例,它代表商品信息,其中包括ID、Name和Price。当用户购买商品时,Ordering微服务中不仅会记录商品的ID,同时还会将商品的名称和价格复制到自己的数据库中。那么,如果将来Catalog微服务中的商品信息发生了变化,这个数据并不一定会及时地同步到Ordering微服务中。那么,如果我们只在Ordering微服务中存储商品ID,而当需要的时候通过API去请求Catalog微服务以获取商品的详细信息岂不是更好?不一定!

  Ordering微服务中商品数据的冗余反映的是用户购买商品时的情形,它与存储在Catalog微服务中商品现在的信息并非一致。Ordering微服务并不关心商品当前的价格和名称,它关心的是用户下单时商品的价格和名称。所以事实上,这不是真正的数据冗余,这些数据在不同的微服务上下文中具备不同的含义。微服务之间的这种数据冗余以及不一致性不仅不会导致系统出现问题,相反,它是由不同的业务需求所决定的。

微服务的组成部分

  微服务不一定是在单个服务器或虚拟机上运行的单个进程,它通常至少有两部分组成:一个由代码构成的WebAPI,而另一个则是数据库。所以这至少是两个不同的进程,而且它们通常并不在同一台服务器或虚拟机中运行。进一步地,如果我们对服务进行横向扩展,并同时对数据库进行分片配置,那么一个微服务甚至会在好几个不同的服务器或虚拟机中运行,而且它可能还需要某种定时任务来执行数据维护,并监听和触发各种不同的消息。因此,实际上一个微服务可能会涉及到多个不同的服务器和进程,所有的这些部分一起构成了一个微服务。

  我们对比看一下eShopOnContainers的微服务架构图,其中的Ordering微服务包含一个名为Ordering.API的WebAPI,另外还有一个名为Ordering.BackgroundTasks的后台任务,这是两个独立运行的进程。我们不必让微服务的所有代码都在一个进程中运行。

  从概念上讲,每个微服务及其公共接口都有明确的边界定义,数据只能通过这些公共接口访问。

每个微服务都是可独立部署的

  微服务可独立部署和存在,这就意味着依赖于该微服务的客户端程序不需要同时升级。要做到这一点,你必须确保微服务的公共接口始终保持对旧客户端程序的兼容。实际上,我们通常把这些公共接口称之为微服务与客户端之间的协议,协议是不能单方面更改的。你可能会问,随着业务和需求的增长,这些公共接口怎么能永远保持不变呢?最简单的解决办法是永远只对公共接口进行增量修改,例如增加新的接口,或在已有的接口上对数据增加新的属性。

  如果不可避免地要对公共接口进行重大调整,可以考虑下面两种实践方式:

  一是让开发客户端的团队等待新的微服务上线之后再更新客户端程序,以确保对微服务的调整不会影响到旧客户端程序的运行。

  另外一个推荐的做法是针对不同版本的客户端程序创建自动化测试,并将其加入到持续集成构建中,这样每次部署之前,如果有自动化测试未完成,构建就会失败,我们就可以查找原因并分析是否存在兼容性问题。

  需要注意的一点是,有一种模式可以引入客户端和微服务都使用的共享代码,从而可以非常方便地生成一个客户端包来简化微服务的调用。我们应该尽量避免使用这种方式,不要将微服务的开发和测试与客户端紧密耦合在一起,因为这会导致客户端对微服务的强依赖而迫使它们同时升级。

如何确定微服务的边界

  如何确定微服务的边界是一项比较困难的事情,因为错误的微服务边界定义会导致后期系统性能的下降,并且一旦这些微服务部署到了生产环境,后期再做调整就比较困难了。因此,在项目开始之前,值得我们花一些时间来认真考虑如何确定微服务的边界。

  我们前面也介绍过,从已有的单体架构的应用程序开始进行微服务架构设计会使任务变得相对容易一些。你会发现其中有一些模块本身就与应用程序的其它部分松散耦合,它们之间通过比较清晰的接口访问数据,这些模块比较容易转换成微服务。

  另外一种方式是从数据库层面着手,看看从某些概念上是否可以将部分表组合在一起,形成一个相对独立的部分。因为每个微服务都拥有自己独立的数据,所以从数据库层面抽取相对独立的部分也是一个好的想法,我们应该尽量避免从多个不同的微服务之间获取数据。

  微服务始终应该围绕着业务来进行组织,这方面可以参考领域驱动设计(Domain Driven Design)的概念,它推荐从应用程序的上下文中来确定边界,并为其定义模型,这意味着不同的微服务将使用不同的模型,即使它们使用的数据看起来没什么区别,但对应的业务场景却不同。

  在eShopOnContainers微服务架构中,Ordering微服务和Catalog微服务尽管都与商品信息有关,但它们处在不同的上下文中,因此它们实际上具有不同的属性,可以自由地为同一条信息使用不同的名称。

  在确定微服务的边界时,你可能会遇到一些陷阱,例如对所有数据库中的业务表进行简单的包装并生成对应的CRUD服务,这些服务充其量只能称之为数据库表的实体类,而不能称之为微服务,因为它们只具有添加和更新实体的方法,而并没有将与实体相关的业务逻辑包含进来。另一个导致微服务边界模糊的情况是,当多个微服务相互之间存在循环依赖关系时,会导致频繁的相互通信,我们应该尽量避免这种情况的出现。

  让我们详细了解一下eShopOnContainers微服务架构中的具体实现,来看看以上这些原则在实际应用中是如何体现的。

Catalog Microservice 存储商品的详细信息。
Basket Microservice 跟踪和存储客户的购物篮信息。
Ordering Microservice 处理客户的订单信息。
Identity Microservice 用于处理用户身份认证。
  1. 职责分离,有助于提高弹性。即使Ordering微服务不可用,依然不影响客户浏览商品信息并将其添加到购物车。
  2. 不同的微服务都被设计为处理不同的数据量和访问模式。Catalog微服务需要支持灵活的查询,以满足客户根据各种不同的查询方式来搜索到想要购买的商品。所以,Catalog微服务需要将数据存储在支持大量丰富查询的数据库中以满足业务的需要,例如这里选择了SQL Server。而Basket微服务只需要存储比较短暂的数据,这些数据甚至都不需要写入数据库,所以这里使用了Redis内存缓存来保存客户购物车中的数据。Ordering微服务用来处理用户的订单信息,因此对数据的可靠性有非常严格的要求。另外它还需要处理一些敏感数据,例如客户的收获地址和付款信息等,所以对安全性也有非常高的要求。Identity微服务用来处理身份验证,我们将在后面的微服务安全部分对其进行详细说明。
  3. 这不是唯一的确定微服务的方式,你可以根据不同的业务需求对其中的功能进行重新组合。就目前来看,这种设计还不错。

四、构建微服务

  当我们开发微服务应用程序时,我们希望它能运行在不同的环境中,例如开发人员希望在自己工作的电脑上运行和调试代码;测试人员希望在临时搭建的测试环境中运行,当然也可能是在云上运行;当微服务正式发布之后,它会在我们的生产环境中运行。那么我们如何托管微服务,使其非常方便地运行在不同的环境中呢?

  下面列出了几种不同的方式:

  1. 传统的方式是使用虚拟机。你可以为每一个微服务选择一台虚拟机,当然如果你的微服务数量特别多的话,这么做成本可能会比较高。你也可以选择将多个微服务打包到一台虚拟机上,但是你需要为其中的每一个微服务安装不同的框架和依赖包,这会让初始化工作变得较为繁琐。
  2. 第二种方式是选择PASS(Platform as a Service)平台。有许多云提供商都提供了微服务的托管服务,你只需要专注于微服务的具体实现, PASS平台负责微服务的管理和基础设施,并可以为每个内置负载均衡的微服务提供自动扩展和DNS条目,另外还有标准的安全和监控功能等。
  3. 第三种方式是选择使用容器。这是当下最流行的方式之一。容器可以将应用程序及其所有的依赖项都打包在一起,然后非常方便地在任何容器主机上轻松移植并运行。容器主机可以是本地工作的电脑,也可以在云上,这大大简化了开发和部署的任务。示例应用程序eShopOnContainers就使用了容器来构建微服务,具体步骤可以查看Github上的文档。容器可以使构建和运行微服务的过程变得简单,如果我们单独为每一个微服务安装所有依赖的软件并配置所有内容,将会是一个非常痛苦的过程,可能需要好几天的时间,期间也可能会遇到各种各样的问题,而容器会使这一切变得非常简单。而且,许多开发工具也允许将调试器附加到容器内运行的代码,这对开发人员debug微服务也带来了很多的便利。

如何开始创建一个微服务

  首先,你需要一个用来保存源代码的仓库,例如Github。虽然从技术上来说你可以将所有微服务的代码都保存在同一个源代码仓库中,但是如果微服务的数量很多,这会大大增加微服务间的耦合程度,这显然违背了微服务设计的初衷。

  其次,我们还需要一个能够自动化持续集成构建微服务的系统,每当我们提交代码到源代码管理器的时候,它都会自动构建一个新的微服务版本,同时还会执行自动化测试,如果测试未通过,则新的构建就会失败,并自动发消息给相应的开发人员。测试是构建微服务的一个非常重要的部分。

测试微服务

  测试是构建微服务中一个非常关键的部分。在构建微服务的过程中,有几种不同类型的测试:

  1. 单元测试。单元测试是针对代码级别的,通常运行比较快。在单元测试中,我们要尽可能保证高的代码覆盖率,尤其是那些针对特定业务的逻辑和模块。
  2. 集成测试。也叫做服务级别的测试,是针对于单个微服务的测试。通常我们需要将单个微服务部署到服务器中,并进行相应的配置,然后通过调用暴露出来的公共接口来测试微服务的功能是否正常。相对于单元测试而言,集成测试更难编写,但它们对于微服务的质量和稳定性来说非常有价值,所以值得我们花时间去创建一个测试框架,然后为每个微服务创建集成测试。这些测试也应该作为自动化构建微服务的一部分。
  3. 端到端测试。这些测试是模拟生产环境中所有运行的微服务,我们可以通过UI界面来执行一些关键的业务操作流程,以保证尽可能多的微服务之间的协同工作是否能否达到预期的目标。这部分的测试在编写和维护方面更加困难,而且往往很容易出错,因为任何一个底层逻辑的改变都有可能导致测试失败。你也可以尝试其它类型的测试来验证尽可能多的功能,但往往端到端的测试在验证某些关键功能方面仍然具有价值,例如在冒烟测试中快速检测系统关键点的功能是否正常。

微服务模板

  在创建微服务时,并不是每次都从零开始。从一个标准模板开始创建微服务可以省去很多工作。你可以根据需要在自己的代码仓库中维护微服务模板,也可以在某个特定的微服务的基础上进行改写。有许多通用的功能都可以标准化,例如:

  • 日志(Logging),它将系统中所有微服务的日志集中管理。
  • 健康检查(Health Checking),如果每个微服务都可以报告自身的运行状态,告知是否正在运行,是否可以与依赖的其它微服务进行通信,这将是一个不错的设计。
  • 配置(Configuration),让所有微服务都采用统一的方式进行配置不失为一个好的想法。
  • 身份认证(Authentication),我们可以使用一些标准的身份认证机制,这可以降低开发过程中一些错误配置而导致我们的微服务存在安全漏洞。
  • 构建脚本(Build Scripts),使用标准的构建方法可以避免我们少走弯路。例如eShopOnContainers中使用容器,每个微服务都使用一个Docker文件来生成容器镜像。

  当然,你可以根据需要将某些特定的功能添加到模板中。模板的好处是提供了开箱即用功能(Out of Box),这大大减少了启动和运行微服务所需的时间,并且还可以确保系统中所有微服务的一致性。不过这种所谓的一致性不应该限制微服务所使用的技术,例如我们不应该限制所有的微服务都使用同一种编程语言,尽管这可以给开发人员在不同的微服务之间工作带来便利,但是我们不应该限制这种技术自由,微服务的开发团队可以自由选择最佳的开发工具。我们的目标是使微服务开发团队尽可能高效地工作,将时间花在实现微服务的业务需求上,而不是围绕微服务技术本身。

  每个开发人员都应该具备独立处理和运行一个微服务的能力,例如在集成测试环境中检查各个功能点是否正常。而且,每个开发人员也应该能够在一个完整的系统中测试他们开发的某个功能。开发人员可以在本地环境中运行所有的内容,也可以在云上直接访问整个系统。但是无论采用哪种方式,工作流程都应该尽量简单并可以自动完成。

posted @ 2023-06-01 10:24  Jaxu  阅读(340)  评论(4编辑  收藏  举报