微服务架构已经成为现代应用开发的主流选择。虽然它解决了某些问题,但它不是灵丹妙药,也有几个缺点。因此,我们需要讨论微服务的设计模式,帮助我们解决一些问题。
在深入研究设计模式之前,我们需要了解微服务的构建原则:
可扩展性
可用性
弹性
独立、自主
去中心化治理
故障隔离
自动配置
通过 DevOps 持续交付
但是,应用所有这些原则会带来一些挑战和问题。接下来,让我们讨论这些问题及其解决方案。
微服务的目标之一是让服务松散耦合,应用单一职责原则。但是,将应用程序分解成更小的部分必须符合逻辑。我们如何将应用程序分解为小服务?
一种策略是按业务能力分解—-业务能力是企业为了创造价值而做的事情,给定业务的能力集取决于业务类型。例如,保险公司的能力通常包括销售、营销、承保、理赔处理、计费等。每个业务能力都可以被认为是一种服务。
使用业务能力分解应用程序可能是一个好的开始,但你会遇到所谓的“上帝类(God Classes)”,它并不容易分解。这些类将在多个服务中通用。例如,Order 类将用于订单的管理、接单、订单交付等,我们如何分解它们?
备注:
”去除上帝类”是指把一个看似功能很强且很难维护的类,按照职责把自己的属性或方法分派到各自的类中或分解成功能明确的类,从而去掉上帝类。
对于“God Classes”问题,可以通过DDD(领域驱动设计)来拯救。它使用子域和有界上下文概念来解决这个问题。DDD 将为企业创建的整个域模型分解为子域,每个子域都有一个模型,该模型的范围将称为有界上下文。每个微服务都将围绕有界上下文开发。
注意:识别子域并非易事。它需要对业务的了解。与业务能力一样,子域是通过分析业务及其组织结构并确定不同的专业领域来确定的。
到目前为止,我们讨论的设计模式是分解绿地应用程序(Greenfield),但我们所做的 80% 的工作是处理棕地应用程序(Brownfield),它们是大型的单体应用程序。将上述所有设计模式应用于它们将很困难,因为把它们分解成更小的部分是一项艰巨的任务。
备注:
绿地应用程序(Greenfield)
即一个新项目,从头开始一个新的软件项目。类比是在绿地上施工,无需改造或拆除现有结构。
(来自http://en.wikipedia.org/wiki/Greenfield_project)
棕地应用程序(Brownfield)
Brownfield开发是IT行业中常用的术语,用于描述现有结构首先需要拆除的网站,即在现有软件项目中构建。
(来自http://en.wikipedia.org/wiki/Brownfield_(software_development))
这就需要扼杀者模式(Strangler)来拯救。
扼杀者模式是基于一个藤蔓扼杀它所缠绕的树的类比。此解决方案适用于 Web 应用程序,可以将服务分解为不同的域并作为单独的服务托管。一次做一个域,这将创建两个独立的应用程序,它们并排在同一个 URI 空间中。最终,新重构的应用程序“扼杀”或替换原始应用程序,直到你最终可以放弃传统的单体应用程序。
[澳大利亚]地区的自然奇观之一是巨大的扼杀者藤蔓。他们在无花果树的树枝上播种,并逐渐沿着树下工作,直到扎根在土壤中。多年来,它们长成奇妙而美丽的形状,同时勒死并杀死了作为寄主的树。
当应用程序分解为较小的微服务时,需要解决一些问题:
如何调用多个微服务。
在不同的终端设备(如PC、APP和Pad)上,因为 UI 不同,应用程序需要后端服务的不同数据。
不同的消费者可能需要不同格式的响应数据。谁将进行数据转换?
如何处理不同类型的协议。
API 网关有助于解决微服务带来的许多问题:
API 网关是任何微服务调用的单一入口点。
它可以作为代理服务将请求路由到相关的微服务。
它可以将请求发送到多个服务并汇总结果以发回给请求者。
一刀切的 API 无法解决消费者的所有需求;此解决方案可以为每种特定类型的客户端创建细粒度的 API。
它还可以将请求协议(例如 AMQP)转换为另一种协议(例如 HTTP),反之亦然。
它还可以对不同的微服务,建立统一的身份验证/授权。
我们已经讨论了解决 API 网关模式中的聚合数据问题。但是,我们将在这里全面讨论它。当将业务功能分解为几个较小的代码段时,有必要考虑如何聚合每个服务返回的数据。这个责任不能留给消费者,因为它可能需要了解生产者的内部实现。
聚合器模式有助于解决这个问题。它可以聚合来自不同服务的数据,然后再发送给消费者。这可以通过两种方式完成:
复合微服务:将调用所有需要的微服务,合并数据,并在应答之前转换数据。
API 网关:还可以将请求划分为多个微服务,并在将数据发送给消费者之前聚合数据。
如果还要处理业务逻辑,建议选择复合微服务。
当通过分解业务能力/子域来开发服务时,客户端必须从多个微服务中拉取数据。在单体世界中,客户端一般只需要调用一次后端服务来查询或刷新所有数据。然而,现在情况将不一样了,我们需要了解如何去做。
对于微服务,客户端可能需要设计为具有多个部分/区域的骨架。每个部分都会调用一个单独的后端微服务来拉取数据。这称为单页应用程序 (Single Page Applications,SPA),像 AngularJS 和 ReactJS 这样的框架有助于轻松地做到这一点。在需要刷新时,应用程序能够刷特定区域而不是整个页面。
如何定义微服务的数据库架构也是一个问题:
服务必须是松耦合的。它们可以独立开发、部署和扩展。
业务请求处理可能会跨越多个服务。
一些事务需要查询多个服务拥有的数据。
数据库有时必须被复制和分片才能扩展。
不同的服务有不同的数据存储要求。
为了解决上述问题,每个微服务必须设计一个数据库,它必须仅供该服务专用。它也应该只能通过微服务 API 访问,而不能被其他服务直接访问。例如,对于关系数据库,我们可以使用 private-tables-per-service、schema-per-service 或 database-server-per-service。每个微服务都应该有一个单独的数据库 id,以便可以给予单独的访问权限,并防止它使用其他服务表。
备注:
Private-tables-per-service——每一服务拥有一系列只能被该服务访问的表
Schema-per-service——每一服务拥有一个为该服务所私有的数据库Schema
Database-server-per-service——每一服务拥有自己的数据库
每个服务一个数据库是微服务的理想选择。当应用程序是全新的并且要使用 DDD 开发时,这是可能的。但是,如果应用程序是单体应用程序并试图改进为微服务架构,那么就不是那么容易了。在这种情况下,合适的架构是什么?
共享数据库并不理想,但这是上述场景的解决方案。大多数人认为这是微服务的反模式,但对于棕地应用程序,这是将应用程序分解为更小部分的良好开端。在这种模式中,一个数据库可以供多个微服务调用,但最多只能限制在 2-3 个,否则扩展性、自治性和独立性将难以执行。
一旦我们实现了每个服务的数据库,就需要查询。那么,对于来自多个服务的联合数据,我们如何在微服务架构中实现查询呢?
CQRS 建议将应用程序分成两部分——命令端和查询端。
CQRS 将系统中的操作分为两类,即「命令」(Command) 与「查询」(Query)。命令则是对会引起数据发生变化操作的总称,即我们常说的新增,更新,删除这些操作,都是命令。而查询则和字面意思一样,即不会对数据产生变化的操作,只是按照某些条件查找数据。
查询端使用物化视图处理查询部分。视图会被保存在订阅了事件的服务中,每个服务在更新数据时会发布出这些事件。例如,网店可以通过维护一个客户信息和订单信息的Join视图来查询特定区域客户和他们的近期订单。该视图由订阅了客户信息事件和订单信息事件的服务进行更新。
当每个服务都有自己的数据库,一个请求事务跨越QQ号买号平台地图多个服务时,我们如何保证跨服务的数据一致性呢?例如,对于电子商务应用程序,应用程序必须确保新订单不会超过客户的信用额度。由于 Orders 和 Customers 位于不同的数据库中,因此应用程序不能简单地使用本地 ACID 事务。
Saga 代表一个高级业务流程,它由多个子请求组成,每个子请求都更新单个服务中的数据。每个请求都有一个补偿请求,在请求失败时执行。
它可以通过两种方式实现:
事件编排 (Event Choreography):没有中央协调器(没有单点风险)时,每个服务产生并观察其他服务的事件,并决定是否应采取行动。
命令协调(Order Orchestrator):中央协调器负责集中处理事件的决策和业务逻辑排序。。
考虑一个用例,其中应用程序由在多台机器上运行的多个服务实例组成。请求通常跨越多个服务实例,每个服务实例都会生成一个标准化格式的日志文件,我们如何通过日志了解特定请求的应用程序行为?
我们需要一个集中的日志服务来聚合来自每个服务实例的日志。用户可以搜索和分析日志,还可以在日志中配置出现某些消息时触发的警报。例如,PCF 确实有 Loggeregator,它从 PCF 平台的每个组件(路由器、控制器、diego 等)以及应用程序收集日志。AWS Cloud Watch 也有同样的作用。
当服务数量不断增加时,密切关注应用性能变得至关重要,以便可以出现问题时发送警报。我们应该如何收集指标来监控应用程序性能?
需要度量服务来收集有关单个操作的统计信息。有两种聚合指标的模型:
Push — 服务将指标推送到指标服务,例如 NewRelic、AppDynamics、Prometheus
Pull — 指标服务从服务中提取指标,例如 Prometheus
在微服务架构中,请求通常跨越多个服务。那么,我们如何跟踪一个请求来排查问题呢?
我们需要一项服务
为每个外部请求分配一个唯一的外部请求 ID。
将外部请求 ID 传递给所有服务。
在所有日志消息中包含外部请求 ID。
集中式记录和处理外部请求执行时操作的信息。
Spring Cloud Slueth 和 Zipkin 服务器是一类常见的实现。
实施微服务架构后,有可能遇到服务已启动但无法处理请求的问题。在这种情况下,你如何确保请求不会发送到那些失败的实例?
每个服务都需要有一个端点,可用于检查应用程序的健康状况,例如 /health, 此 API 应检查主机的状态、与其他服务/基础设施的连接以及任何特定逻辑。
Spring Boot Actuator 实现了一个/health 端点,并且该实现也可以自定义。
“cross-cutting concerns”指的是两个非常不一样的组件存在一些类似的功能。
服务通常也调用其他服务和数据库。对于每个环境,如 dev、QA、UAT、prod,某些配置属性可能不同。任何这些属性的更改都可能需要重新构建和重新部署服务。我们如何避免因配置更改而修改代码?
外部化所有配置,包括凭据。应用程序应在启动时动态加载它们。
Spring Cloud 配置服务器提供了将属性外部化到 GitHub 并将它们作为环境属性加载的选项。这些可以在启动时由应用程序访问,也可以在不重新启动服务器的情况下刷新。
当微服务出现后,我们需要解决服务调用方面的几个问题:
使用容器技术,IP 地址被动态分配给服务实例。每次地址更改时,消费者服务都可能中断并需要手动更改。
每个服务 URL 都必须被消费者记住并紧密耦合。
那么消费者或路由器如何知道所有可用的服务实例和位置呢?
需要创建一个服务注册中心来保存每个生产者服务的元数据。服务实例应在启动时注册到注册表,并在停止时取消注册。消费者或路由器应查询注册表并找出服务的位置。注册中心还需要对生产者服务进行健康检查,以确保只有服务的工作实例可供通过它使用。有两种类型的服务发现:客户端和服务器端。
服务发现(客户端)的一个例子是 Netflix Eureka,服务发现(服务器端)的一个例子是 AWS ALB。
一个服务一般会调用其他服务来查询数据,但下游服务有可能宕机。这样做有两个问题:第一,请求会一直到宕机的服务,耗尽网络资源,降低性能。其次,用户体验会很差且不可预测。我们如何避免级联服务故障并优雅地处理故障呢?
消费者应该通过代理调用远程服务,该代理的行为方式与电路断路器类似。当连续失败次数超过阈值时,断路器跳闸,并且在超时期限内,所有调用远程服务的尝试都将立即失败。超时到期后,断路器允许有限数量的测试请求通过。如果这些请求成功,断路器将恢复正常操作。否则,如果出现故障,超时时间将重新开始。
Netflix Hystrix 是断路器模式的一个很好的实现。它还可以在断路器跳闸时使用回退机制。这提供了更好的用户体验。
使用微服务架构,一个应用可以有多个微服务。如果我们停止所有服务,然后部署新版本,停机时间将会非常长,并且会影响业务。此外,回滚将是一场噩梦。我们如何避免或减少部署期间服务的停机时间?
可以实施蓝绿部署策略以减少或消除停机时间。它通过运行两个相同的生产环境 Blue 和 Green 来实现这一点。让我们假设 Green 是现有的实时实例,而 Blue 是应用程序的新版本。在任何时候,只有一个环境处于活动状态,实时环境服务于所有生产流量。几乎所有云平台都提供了实施蓝绿部署的选项。
还有许多其他与微服务架构一起使用的模式,如 Sidecar、链式微服务、分支微服务、事件溯源模式、持续交付模式等。如果你还知道其他的微服务模式,欢迎留言交流。