服务保护和分布式事务
查询购物车的业务,假如商品服务业务并发较高,占用过多 Tomcat 连接。可能会导致商品服务的所有接口响应时间增加,延迟变高,甚至是长时间阻塞直至查询失败。
此时查询购物车业务需要查询并等待商品查询结果,从而导致查询购物车列表业务的响应时间也变长,甚至也阻塞直至无法访问。而此时如果查询购物车的请求较多,可能导致购物车服务的 Tomcat 连接占用较多,所有接口的响应时间都会增加,整个服务性能很差, 甚至不可用。
依次类推,整个微服务群中与购物车服务、商品服务等有调用关系的服务可能都会出现问题,最终导致整个集群不可用。
这就是级联失败问题,或者叫雪崩问题。
1.微服务保护
1.1 服务保护方案
- 请求限流
- 线程隔离
- 服务熔断
1.1.1 请求限流
服务故障最重要原因,就是并发太高!解决了这个问题,就能避免大部分故障。当然,接口的并发不是一直很高,而是突发的。因此请求限流,就是限制或控制接口访问的并发流量,避免服务因流量激增而出现故障。
请求限流往往会有一个限流器,数量高低起伏的并发请求曲线,经过限流器就变的非常平稳。这就像是水电站的大坝,起到蓄水的作用,可以通过开关控制水流出的大小,让下游水流始终维持在一个平稳的量。
1.1.2 线程隔离
当一个业务接口响应时间长,而且并发高时,就可能耗尽服务器的线程资源,导致服务内的其它接口受到影响。所以我们必须把这种影响降低,或者缩减影响的范围。线程隔离正是解决这个问题的好办法。
线程隔离:也叫做舱壁模式,模拟船舱隔板的防水原理。通过限定每个业务能使用的线程数量而将故障业务隔离,避免故障扩散。
1.1.3 服务熔断
线程隔离虽然避免了雪崩问题,但故障服务(商品服务)依然会拖慢购物车服务(服务调用方)的接口响应速度。而且商品查询的故障依然会导致查询购物车功能出现故障,购物车业务也变的不可用了。
所以,我们要做两件事情:
- 编写服务降级逻辑:就是服务调用失败后的处理逻辑,根据业务场景,可以抛出异常,也可以返回友好提示或默认数据。
- 异常统计和熔断:统计服务提供方的异常比例,当比例过高表明该接口会影响到其它服务,应该拒绝调用该接口,而是直接走降级逻辑。
1.2 Sentinel
1.2.1.微服务启动
Sentinel 的使用可以分为两个部分:
- 核心库(Jar 包):不依赖任何框架/库,能够运行于 Java 8 及以上的版本的运行时环境,同时对 Dubbo / Spring Cloud 等框架也有较好的支持。在项目中引入依赖即可实现服务限流、隔离、熔断等功能。
- 控制台(Dashboard):Dashboard 主要负责管理推送规则、监控、管理机器信息等。
启动 Sentinel 控制台:
1 |
|
1.2.2.微服务整合
我们在 cart-service 模块中整合 sentinel,连接 sentinel-dashboard 控制台,步骤如下:
1)引入 sentinel 依赖
1 |
|
修改 application.yaml 文件,添加下面内容:
1 |
|
重启服务就可以在 Sentinel 控制台看到 cart-service 服务的监控信息了。
http://localhost:8090/#/dashboard/identity/cart-service
簇点链路,就是单机调用链路,是一次请求进入服务后经过的每一个被 Sentinel 监控的资源。默认情况下,Sentinel 会监控 SpringMVC 的每一个 Endpoint(接口)。
因此,我们看到/carts 这个接口路径就是其中一个簇点,我们可以对其进行限流、熔断、隔离等保护措施。
默认情况下 Sentinel 会把路径作为簇点资源的名称,无法区分路径相同但请求方式不同的接口,查询、删除、修改等都被识别为一个簇点资源,这显然是不合适的。
所以我们可以选择打开 Sentinel 的请求方式前缀,把请求方式 + 请求路径作为簇点资源名:
首先,在 cart-service 的 application.yml 中添加下面的配置:
1 |
|
1.3 请求限流
打开控制台,在簇点链路后面点击流控按钮,即可对其做限流配置。
1.4 线程隔离
修改 cart-service 模块的 application.yml 文件,开启 Feign 的 sentinel 功能:
1 |
|
需要注意的是,默认情况下 SpringBoot 项目的 tomcat 最大线程数是 200,允许的最大连接是 8192,单机测试很难打满。
所以我们需要配置一下 cart-service 模块的 application.yml 文件,修改 tomcat 连接:
1 |
|
接下来,点击查询商品的 FeignClient 对应的簇点资源后面的流控按钮进行配置即可。
1.5.服务熔断
之前 我们利用线程隔离对查询购物车业务进行隔离,保护了购物车服务的其它接口。由于查询商品的功能耗时较高(我们模拟了 500 毫秒延时),再加上线程隔离限定了线程数为 5,导致接口吞吐能力有限,最终 QPS 只有 10 左右。这就导致了几个问题:
第一,超出的 QPS 上限的请求就只能抛出异常,从而导致购物车的查询失败。但从业务角度来说,即便没有查询到最新的商品信息,购物车也应该展示给用户,用户体验更好。也就是给查询失败设置一个降级处理逻辑。
第二,由于查询商品的延迟较高(模拟的 500ms),从而导致查询购物车的响应时间也变的很长。这样不仅拖慢了购物车服务,消耗了购物车服务的更多资源,而且用户体验也很差。对于商品服务这种不太健康的接口,我们应该直接停止调用,直接走降级逻辑,避免影响到当前服务。也就是将商品查询接口熔断。
1.5.1 编写降级逻辑
触发限流或熔断后的请求不一定要直接报错,也可以返回一些默认数据或者友好提示,用户体验会更好。
给 FeignClient 编写失败后的降级逻辑有两种方式:
- 方式一:FallbackClass,无法对远程调用的异常做处理
- 方式二:FallbackFactory,可以对远程调用的异常做处理,我们一般选择这种方式。
在 hm-api 模块中给 ItemClient 定义降级处理类,实现 ItemClientFallbackFactory
1 |
|
步骤二:在 hm-api 模块中的 com.hmall.api.config.DefaultFeignConfig 类中将 ItemClientFallback 注册为一个 Bean:
1 |
|
步骤三:在 hm-api 模块中的 ItemClient 接口中使用 ItemClientFallbackFactory:
1 |
|
1.5.2. 服务熔断
查询商品的 RT 较高(模拟的 500ms),从而导致查询购物车的 RT 也变的很长。这样不仅拖慢了购物车服务,消耗了购物车服务的更多资源,而且用户体验也很差。
对于商品服务这种不太健康的接口,我们应该停止调用,直接走降级逻辑,避免影响到当前服务。也就是将商品查询接口熔断。当商品服务接口恢复正常后,再允许调用。这其实就是断路器的工作模式了。
Sentinel 中的断路器不仅可以统计某个接口的慢请求比例,还可以统计异常请求比例。当这些比例超出阈值时,就会熔断该接口,即拦截访问该接口的一切请求,降级处理;当该接口恢复正常时,再放行对于该接口的请求。
断路器的工作状态切换有一个状态机来控制:
状态机包括三个状态:
- closed:关闭状态,断路器放行所有请求,并开始统计异常比例、慢请求比例。超过阈值则切换到 open 状态
- open:打开状态,服务调用被熔断,访问被熔断服务的请求会被拒绝,快速失败,直接走降级逻辑。Open 状态持续一段时间后会进入 half-open 状态
- half-open:半开状态,放行一次请求,根据执行结果来判断接下来的操作。
- 请求成功:则切换到 closed 状态
- 请求失败:则切换到 open 状态
2.分布式事务
我们知道每一个分支事务就是传统的单体事务,都可以满足 ACID 特性,但全局事务跨越多个服务、多个数据库,是否还能满足呢?
参与事务的多个子业务在不同的微服务,跨越了不同的数据库。虽然每个单独的业务都能在本地遵循 ACID,但是它们互相之间没有感知,不知道有人失败了,无法保证最终结果的统一,也就无法遵循 ACID 的事务特性了。
这就是分布式事务问题,出现以下情况之一就可能产生分布式事务问题:
- 业务跨多个服务实现
- 业务跨多个数据源实现
2.1 认识 Seata
其实分布式事务产生的一个重要原因,就是参与事务的多个分支事务互相无感知,不知道彼此的执行状态。因此解决分布式事务的思想非常简单:
就是找一个统一的事务协调者,与多个分支事务通信,检测每个分支事务的执行状态,保证全局事务下的每一个分支事务同时成功或失败即可。大多数的分布式事务框架都是基于这个理论来实现的。
Seata 也不例外,在 Seata 的事务管理中有三个重要的角色:
- TC (Transaction Coordinator) - 事务协调者:维护全局和分支事务的状态,协调全局事务提交或回滚。
- TM (Transaction Manager) - 事务管理器:定义全局事务的范围、开始全局事务、提交或回滚全局事务。
- RM (Resource Manager) - 资源管理器:管理分支事务,与 TC 交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
其中,TM 和 RM 可以理解为 Seata 的客户端部分,引入到参与事务的微服务依赖中即可。将来 TM 和 RM 就会协助微服务,实现本地分支事务与 TC 之间交互,实现事务的提交或回滚。
而 TC 服务则是事务协调中心,是一个独立的微服务,需要单独部署。
2.2.部署 TC 服务
Seata 支持多种存储模式,但考虑到持久化的需要,我们一般选择基于数据库存储。执行课前资料提供的《seata-tc.sql》,导入数据库表
我们将整个 seata 文件夹拷贝到虚拟机的/root 目录
需要注意,要确保 nacos、mysql 都在 hm-net 网络中。如果某个容器不再 hm-net 网络,可以参考下面的命令将某容器加入指定网络:
1 |
|
1 |
|
2.3 微服务集成 Seata
java 17 以上版本需要添加以下参数
1 |
|
2.3.1.引入依赖
为了方便各个微服务集成 seata,我们需要把 seata 配置共享到 nacos,因此 trade-service 模块不仅仅要引入 seata 依赖,还要引入 nacos 依赖:
1 |
|
2.3.2.改造配置
首先在 nacos 上添加一个共享的 seata 配置,命名为 shared-seata.yaml:
内容如下:
1 |
|
然后,改造 trade-service 模块,添加 bootstrap.yaml:
内容如下:
1 |
|
可以看到这里加载了共享的 seata 配置。
然后改造 application.yaml 文件,内容如下:
1 |
|
参考上述办法分别改造 hm-cart 和 hm-item 两个微服务模块。
2.3.3.添加数据库表
seata 的客户端在解决分布式事务的时候需要记录一些中间数据,保存在数据库中。因此我们要先准备一个这样的表。
将课前资料的 seata-at.sql 分别文件导入 hm-trade、hm-cart、hm-item 三个数据库中:
OK,至此为止,微服务整合的工作就完成了。可以参考上述方式对 hm-item 和 hm-cart 模块完成整合改造。
简述 AT 模式与 XA 模式最大的区别是什么?
- XA 模式一阶段不提交事务,锁定资源;AT 模式一阶段直接提交,不锁定资源。
- XA 模式依赖数据库机制实现回滚;AT 模式利用数据快照实现数据回滚。
- XA 模式强一致;AT 模式最终一致
可见,AT 模式使用起来更加简单,无业务侵入,性能更好。因此企业 90%的分布式事务都可以用 AT 模式来解决。