.NET 微服务 2 架构设计理论(一)
soa体系架构
- 面向服务的体系结构 (soa) ,通过将应用程序分解为多个服务(通常为 http 服务,wcf服务等),将其分为不同类型(例如子系统或层),从而来划分应用程序的结构。
- 微服务源自 soa,但 soa 不同于微服务体系结构。 诸如大型*代理、组织级别的*业务流程协调程序和企业服务总线 (esb) 等功能在 soa 中很典型。 但在大多数情况下,这些是微服务社区中的反模式。
微服务架构
- 微服务体系结构是一种将服务器应用程序生成为一组小型服务的方法。
- 每个服务都在自己的进程中运行,并使用 http/https、websocket 或 amqp 等协议与其他进程进行通信。
- 每个微服务在特定的上下文边界内实现特定的端到端域或业务功能,每个微服务都必须自主开发,并且可以独立部署。
- 最后,每个微服务都应拥有其自己的相关域数据模型和域逻辑(主权和分散式数据管理),并且可以基于不同的数据存储技术(sql、nosql)和不同的编程语言。
- 在标识和设计微服务时,只要与其他微服务不存在过多的直接依赖项,就应尝试让它们尽可能地小。 比微服务的大小更重要的是,它必须具有内部内聚,并且独立于其他服务。
- 单独缩放需要更多处理能力或网络带宽以支撑需求的功能区域,而不用横向扩展应用程序中不需要缩放的其他区域。 这意味着节省成本,因为所需硬件更少。
微服务特点
- 对服务和基础结构的监视和运行状况检查。
- 服务(即云和业务流程协调程序)的可缩放基础结构。
- 多个级别的安全设计和实现:身份验证、授权、机密管理、安全通信等。
- 快速应用程序交付,通常不同的团队重点负责不同的微服务。
- devops 和 ci/cd 实践和基础结构。
微服务数据库设计
传统单一的中心化数据库和微服务一个服务一个数据库
- 单一关系型数据库的单体式应用有两个重要优点:在应用程序的所有数据库表和数据层面,都支持 acid 事务和 sql 语言。
- 如果多个服务访问相同数据,数据的更新就要求协调同步到所有服务,这会破坏微服务生命周期的自治性。当业务流程跨越多个微服务时,最终一致性是唯一的办法。
- 不同微服务通常使用不同种类的数据库。一些场景下,nosql 数据库比 sql 数据库有着更方便的数据模型和更好的性能与扩展性。
- 基于微服务的应用通常会混合使用 sql 和 nosql 数据库,这种做法有时会被称为混合数据持久化(polyglot persistence)。 基于容器和微服务的应用架构 这种隔离的混合数据持久化架构有很多优点,包括服务的松散耦合,更好的性能、扩展性,更低成本和 可管理性等。
微服务和限界上下文模式的关系
- 微服务的理论源自领域驱动设计(ddd)的限界上下文(bc)模式。ddd 将大型业务划分成多个 bc,并明确它们的边界,每个 bc 必须有自己的模型和数据库。类似的,每个微服务也拥有跟自己相关的数据。
- 不同限界上下文的表示语言中的信息(主要是领域实体)可以有不同名称,即使不同领域实体共用相同标识(即通过唯一 id 从存储中读取实体)。例如在一个用户资料的限界上下文中,用户 user 领域实体可能跟订单限界上下文的买家 buyer 领域实体共享标识。
- 微服务与限界上下文非常类似,但微服务是一种分布式服务,每个微服务都以独立进程的方式创建,并且必须使用先前提到的分布式协议,如 http/https、websockets 或 amqp。然而限界上下文模式并没有明确一个 bc 到底是分布式服务或只是单体式部署的应用中的简单逻辑边界(例如常见的子系统)。
微服务的逻辑架构和物理架构
- 创建微服务并不要求必须使用某种技术,例如 docker 容器就不是必需的。微服务能够作为普通进程运行,微服务是一种逻辑架构。
- 即使微服务能够以独立的服务、进程或容器方式来实现,这种在业务微服务和物理服务或容器之间的同等性不是必要的,尤其是当我们需要创建一个由成百上千的服务组成的大型复杂应用时。系统的逻辑架构和逻辑边界与物理或部署架构没必要一一对应,虽然可行,但通常并不这样做。
- 您或许已经确定了业务微服务或限界上下文,但不意味着最佳实现方式就是为每个业务微服务创建单独的服务(例如作为一个 asp.net web api)或单独的 docker 容器。没有规定说每个业务微服务必须使用单独服务或容器来实现。因此,业务微服务或限界上下文是一个逻辑架构,或许确实(也可以不是)与物理架构一致。重点在于,业务微服务或限界上下文必须自主地进行代码和状态的独立版本控制、部署和扩展。
- 如图所示,目录业务微服务由多个服务或进程组成,它们可以是多个 asp.net web api 或使用http 或其他协议的服务。更重要的是,这些服务共享相同的数据,因为它们都结合在一个相同的业务领域里。
- 本例中的 web api 服务和搜索服务使用了同一个数据源,它们共享相同的数据模型。所以在物理实现业务微服务时,对功能进行了拆分,以便能够按需向上或向下扩展每个内部服务。例如 web api 通常需要更多的运行实例,反之亦然。
- 简而言之,微服务的逻辑架构通常与物理架构并不相同。
分布式数据管理的挑战和解决方案
挑战 1:如何定义微服务边界
- 首先,需要关注应用的逻辑领域模型和相关数据。必须尝试识别同一个应用中解耦后的数据孤岛和不同的上下文。
- 每个上下文都可以有不同的商业语言(不同的业务术语),上下文的定义和管理应该独立进行。在不同上下文中使用的术语和实体可能听起来相似,但有时在特定上下文中的一个业务概念在另一个上下文中可能会被用于不同的目,甚至可能使用不同名称。例如,同一个“用户”,可以是身份或会员系统上下文中的“用户”,是 crm 中的“客户”,甚至还是订单上下文中的“买方”等。
- 识别多个应用上下文以及每个上下文所对应不同领域之间边界的方法,也能用来识别每个业务微服务和相关领域模型和数据的边界。
- 后面的“识别微服务的领域模型边界”将详细介绍识别方法和领域驱动设计。
挑战 2:如何创建从多个微服务获取数据的查询
第二个挑战在于:如何实现从多个微服务获取数据的查询,同时避免远程客户端和微服务之间不必要的通信。
例如一个移动 app 需要一个页面来展示由购物篮、产品目录和用户身份微服务包含的用户信息。
再如一个复杂的报表系统涉及到位于多个微服务的多个表。
适合的解决方案取决于查询的复杂性。但无论如何都需要一种方式来聚合信息,以提高系统的通信效率。最流行的解决方案如下。
api 网关:对于从多个拥有不同数据库的微服务进行的简单数据聚合,推荐的方式是通过名为 api 网关的机制聚合微服务。然而使用这种模式时需要当心,它可能成为系统瓶颈,也可能违反微服务自治的原则。为了降低这些可能性,可以使用多个细粒度的 api 网关,每个网关主要面向系统的一个垂直“切片”即业务领域。
cqrs 查询/读取表:另一种聚合多个微服务数据的方案是物化视图模式(materialized view pattern),这种方案会提前(在实际查询发生前准备好非规范的数据)生成包含多个微服务数据的只读表,并且这种表会使用适合客户端应用需求的格式。
假设有一个移动 app 的界面,如果有一个数据库,就可以使用一个 sql 查询获取界面所需的全部数据,该查询会针对多个表执行复杂的联接。
但如果使用了分布在不同微服务中的多个数据库,就不能查询这些数据库并创建 sql 联接。此时复杂的查询将变成巨大的挑战。为此可以使用 cqrs 方案:在不同数据库中创建一个只用作查询的非规范表,这个表可以针对复杂查询所需的数据专门设计,把应用界面所需的字段和查询表的字段一一对应。这样的查询表还可以用在报表中。
这种方式不仅解决了最初的问题(如何跨微服务查询和联接),与复杂的 sql 联接语句相比还能进一步提升性能,因为应用所需的数据已经在查询表里了。当然,使用命令查询职责分离(cqrs)的查询/读取表需要额外的开发工作,并且需要面对数据最终一致性问题。然而在需要高性能和高扩展性的协作场景(或者竞争场景,取决于视角)下,应该使用 cqrs 来处理多数据库。
中心数据库的“冷数据”:对于可能不需要实时数据的复杂报表和查询,此时一种通用方案是将“热数据”(微服务里的交易数据)导出为“冷数据”存储到报表专用的大型数据库中。这里的中心数据库系统可以是基于大数据的系统,如 hadoop,也可以是数据仓库,如 azure sql 数据仓库,甚至可以是单独的报表专用 sql 数据库(如果容量不是问题的话)。
需要注意的是,中心数据库应该只用于不需要实时数据的报表查询,作为事实数据源的原始更新和交易数据必须在微服务中。为了同步数据,可采用事件驱动通信(下一节会详细介绍),或使用数据库基础结构提供的导入/导出工具。如果使用事件驱动通信的方式,整合流程将与上文提到的使用 cqrs 查询表获取数据的方式类似。
然而,如果应用在设计上需要不断从多个微服务里进行聚合数据并进行复杂查询,上述设计将变得非常糟糕,毕竟微服务之间应该尽可能地保持相互独立(使用中心冷数据库的报表分析系统除外)。通常在遇到此类问题后,我们也许会合并微服务。我们需要在每个微服务的自治式进化和部署,以及强依赖、高内聚和数据聚合之间进行权衡。
挑战 3:如何在多个微服务之间实现一致性
如上文所述,每个微服务拥有的数据是私有的,只能通过微服务 api 来访问。因此会遇到这样一个挑战:如何跨多个微服务保持一致性的同时实现端到端的业务逻辑。
为了分析这个问题,让我们看看示例应用 eshoponcontainers 中的一个例子。目录(catalog)微服务维护着所有产品信息,包括价格。购物篮(basket)微服务管理着用户加入购物篮的产品临时数据,包括添加到购物篮时的产品价格。当目录服务中一个产品的价格发生变化后,购物篮中相同产品的价格也应该变化,另外,系统应该告诉用户说购物篮里的某个产品的价格变了。
假如这个应用有一个单体式版本,当产品表中的价格发生变化时,产品子系统能够简单地使用 acid 事务来更新购物篮表里的价格。
然而在微服务应用里,产品表和购物篮表被各自的微服务所占有。任何微服务不应该在自己的事务中包含其他微服务的表或存储,即使是直接查询也是不可以的。目录微服务不能直接更新购物篮表,因为购物篮表被购物篮微服务占有。要更新购物篮微服务,产品微服务应该使用基于异步通信,如集成事件(消息和基于事件的通信)的最终一致性。
根据 cap 理论,我们需要在可用性和强 acid 一致性之间作出选择。大多数微服务场景要求高可用性和高扩展性,而非强一致性。重要应用必须保持随时在线,开发人员可以使用弱一致性或最终一致性的技术来做到强一致性。此外,acid 风格或两步提交事务不仅违背微服务原则,大多数 nosql 数据库(如 azure cosmosdb、mongodb 等)也不支持两步提交事务。然而,跨服务和数据库维护数据的一致性非常重要。
因此,总结来说,解决这个问题的方法之一是,在微服务之间使用事件驱动通信和发布订阅系统来实现最终一致性。
挑战 4:如何在多个微服务之间通信
跨微服务边界的通信是一个真正的挑战。这里所谓的“通信”不是指该用什么协议(http 和 rest、amqp、消息等),而是指该使用什么样的通信方式,尤其是如何耦合微服务。取决于耦合等级,在宕机发生时,会对系统产生不同程度的影响。
诸如微服务这样的分布式系统中,有很多内容需要四处移动,组成分布式服务的诸多服务器或主机等组件最终都可能面临故障,部分宕机甚至大面积失效也会发生。面对这样的分布式系统,需要在设计微服务和跨服务通信时就考虑到这些常见的风险。
举例来说,假设客户端应用发起了一个 http api 请求,需要调用另一个独立的微服务,例如订单微服务,如果订单微服务随后在同一个请求/响应周期内使用 http 调用另一个微服务,这就创建了一个http 调用链条。听起来也许合理,然而如果打算这样做,有些重要的问题需要考虑:
- 阻塞和性能低下。由于 http 的同步本质,最初的请求在所有内部 http 请求全部完成前不会获得响应结果。假设这样的请求量在逐步增长,同时某个中间微服务的 http 调用被阻塞,结果就是性能受到影响,并且整体扩展性由于额外 http 请求的增加遇到几何级增长的影响。
- 使用 http 耦合微服务。业务微服务不应该被其他业务微服务耦合。理想情况下,它们不应该“知道”其他微服务的存在。如果我们的应用像示例一样依靠耦合的微服务,几乎无法实现每个微服务的自治。
- 任何微服务引起的宕机。如果实现使用 http 调用的链式微服务,当任何一个微服务宕机(最终每个微服务都可能宕机)时,整个微服务链会挂掉。微服务系统应该设计成在部分宕机情况下尽可能地继续正常运行。即使客户端逻辑使用了越来越快和灵敏的重试机制,http 调用链越复杂,实现基于 http 的容错策略过程就越复杂。
实际上,如果内部微服务采用上述链式 http 请求来通信,可能会产生这样一种争议:这实际上是一种单体式应用,它的进程间是基于 http 的,而没有使用进程内通信机制。
因此,为了促进微服务的自治并获得更高弹性,应该减少使用跨服务的链式请求/响应通信。建议微服务间的通信只使用异步交互,例如使用基于消息或事件的异步通信,或者使用与原始 http 请求/响应独立的 http 轮询(polling)方式。
参考文档
推荐阅读
-
使用 Topshelf 组件一步一步创建 Windows 服务 (2) 使用Quartz.net 调度
-
微信公共服务平台开发(.Net 的实现)2-------获得ACCESSTOKEN
-
.NET 微服务 2 架构设计理论(一)
-
搭建一个大型网站架构的实验环境(Squid缓存服务器篇)第1/2页
-
使用 Topshelf 组件一步一步创建 Windows 服务 (2) 使用Quartz.net 调度
-
.NET 微服务 2 架构设计理论(一)
-
微信公共服务平台开发(.Net 的实现)2-------获得ACCESSTOKEN
-
搭建一个大型网站架构的实验环境(Squid缓存服务器篇)第1/2页
-
asp.net开发微信公众平台(2)多层架构框架搭建和入口实现
-
asp.net开发微信公众平台(2)多层架构框架搭建和入口实现