SpringCloud —— Sentinel详解

一、前言

在前面的课程中,我们可以发现一个问题,就是即使是低耦合的微服务架构,在微服务和微服务之间依然是有耦合的,比如相互之间的远程调用,这就导致一个微服务出现异常(不一定是崩溃,也有可能是请求处理速度变慢,响应时间过长),另一个微服务也会受到影响,同时,在调用链上的其他微服务也有可能受到影响,所以这是一个安全问题,我们在这里会给出几种方式用于解决这个问题。

二、Sentinel

Sentinel是一个中间件,可以通过流量控制、线程隔离、服务熔断的方式来降低异常发生后的风险。

1.快速入门

首先要在官网下载Sentinel,然后解压,在无中文的目录中使用命令行打开,命令如下:

bash 复制代码
java -Dserver.port=8090 -Dcsp.sentinel.dashboard.server=localhost:8090 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard.jar

从这里就可以看出,我们是直接在windows上启动的Sentinel,但是Sentinel的客户端是使用SpringBoot开发的,最终启动后会占用tomcat默认的8080端口,在同一台电脑中,我们的项目网关端口也是8080,所以端口会冲突。

解决办法有两个,第一是用上述配置,在启动时更改端口号为8090,第二是在虚拟机中的docker容器中启动,这样就只会占用宿主机的8080端口。

这里我们选用前一种,比较快捷直观,而且由于之前我把项目也部署到docker容器中了,所以即使将Sentinel放到docker中,也有可能会出现端口冲突的问题。

启动后通过访问8090端口可以看到Sentinel的页面:

向需要进行微服务保护的微服务导入依赖:

XML 复制代码
        <!--sentinel-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>

配置Sentinel:

bash 复制代码
spring:
  cloud:
    sentinel:
      transport:
        dashboard: localhost:8090 # sentinel的控制台地址
      http-method-specify: true # 是否设置请求方式作为资源名称

在访问一次购物车页面后就可以看得到购物车微服务的服务名称了:

2.流量限制

首先要了解请求被处理的流程:先建立连接,然后再排队,最后被处理

为什么要限制流量?

因为流量会影响请求的响应速度,导致用户使用时会很"卡",如果流量不被限制,并且变得太大时,SpringBoot默认的Tomcat线程池中的工作线程会被占满,新的请求就只能去排队了,而等待队列如果也被占满了,将会导致新请求被拒绝,这样用户就会完全无法访问网页了。

那为什么会影响响应速度呢?

因为请求超过处理能力后,会自动排队,这就导致许多请求无法被及时处理,也就是:

正常:响应时长=请求被处理的时长

过多:响应时长=请求被处理的时长+排队等待的时长

我们这里为了模拟服务响应超时,我们修改代码,让请求处理时长增加,从而尽可能让更多线程去排队:

java 复制代码
@ApiOperation("根据id批量查询商品")
    @GetMapping
    public List<ItemDTO> queryItemByIds(@RequestParam("ids") List<Long> ids){

        //TODO 模拟业务延迟
        ThreadUtil.sleep(500);
        return itemService.queryItemByIds(ids);
    }

同时我们将Tomcat的最大线程处理数降低,等待队列的线程数降低,同时将最大连接数降低。

bash 复制代码
  tomcat:
    threads:
      max: 25
    accept-count: 25
    max-connections: 100

这里限制这三个参数就是为了限制请求的处理,也就是降低Tomcat处理请求的能力,让Tomcat线程池尽快饱和,从而模拟大用户量的场景。

接下来,正式开始流量限制,首先先设置流量限制,我们这里配置访问购物车的流量限制:

然后我们使用测试软件jmeter模拟大用户量的情况:

开始测试,我们可以看到,异常率接近百分之四十,这是因为我们限制的流量是每秒6个,而测试案例是每秒十个(1000/100),所以平均每秒会有4个请求被抛出异常429。

在测试过程中,我们自己也访问购物车来测试,发现延迟在522ms,这说明我们访问购物车的时间是没变的,所以流量限制的好处:通过限制请求处理量,来保障被处理请求的质量。

3.线程隔离

线程隔离是指:在线程池中分配指定量线程给某个微服务,如果这个微服务异常,只会影响被分配的几个线程,其他线程不被影响,从而让其他和本微服务无耦合的微服务(不远程调用异常微服务的其他微服务)正常运行。

购物车中有一个功能是增加商品数量,这个是通过远程调用实现的,和购物车查询无关,如果我们将购物车查询功能线程隔离,那按道理说,即使查询异常,也不会影响增加商品的功能。

接下来尝试模拟这个场景:

一样的设置步骤,对于查询购物车的GET请求,在流控中选择并发线程数,我们这里设置为5:

这里我们设置测试的配置:

开始测试后,我们尝试增加商品到购物车,我们会发现,购物车是查不出来了,但是我们增加商品数量是不被影响的。

4.Fallback

我们先前的购物车微服务会远程调用ItemClient,如果远程调用失败就会抛异常,从而影响购物车的功能,Fallback指的是回退,其实就是一个备用方案,如果ItemClient远程调用异常,那么这个时候就会自动进入回退流程,重新创建一个远程调用对象,这个新对象中会重写方法,但是这个方法里面就没有逻辑了,直接返回空,并且报错误日志,这样就可以正常返回了,只是返回的是空,对于用户来讲,这个就不会过多影响其他功能,而报错的日志又对开发人员的维护起到提示作用。

先模拟商品微服务大用户量,方便后续让远程调用异常,从而逼它走fallback策略:

java 复制代码
@ApiOperation("根据id批量查询商品")
    @GetMapping
    public List<ItemDTO> queryItemByIds(@RequestParam("ids") List<Long> ids){

        //TODO 模拟业务延迟
        ThreadUtil.sleep(500);
        return itemService.queryItemByIds(ids);
    }

首先需要创建一个工厂类,用于生成降级代理对象(替代方案)。

java 复制代码
@Slf4j
public class ItemClientFallbackFactory implements FallbackFactory<ItemClient> {

    @Override
    public ItemClient create(Throwable cause) {
        return new ItemClient() {
            @Override
            public List<ItemDTO> queryItemByIds(Collection<Long> ids) {
                log.error("查询商品失败:",cause);
                return CollUtils.emptyList();
            }

            @Override
            public void deductStock(List<OrderDetailDTO> items) {
                log.error("扣减商品库存失败:",cause);
                throw new RuntimeException(cause);
            }
        };
    }
}

然后我们需要将工厂写到配置类中,在远程调用异常的时候生效。

java 复制代码
public class DefaultFeignConfig {

    /**
     * 配置日志级别
     *
     * @return
     */
    @Bean
    public Logger.Level feignLoggerLevel() {
        return Logger.Level.FULL;
    }

    @Bean
    public RequestInterceptor userInfoRequestInterceptor() {
        return new RequestInterceptor() {
            @Override
            public void apply(RequestTemplate requestTemplate) {
                Long userId = UserContext.getUser();
                if (userId != null) {
                    requestTemplate.header("user-info", userId.toString());
                }
            }
        };
    }
    @Bean
    public ItemClientFallbackFactory itemClientFallbackFactory(){
        return new ItemClientFallbackFactory();
    }
}

最后将配置类中的工厂方法配置到远程调用接口中去:

java 复制代码
@FeignClient(value = "item-service",fallbackFactory = ItemClientFallbackFactory.class)
public interface ItemClient {
    @GetMapping("/items")
    List<ItemDTO> queryItemByIds(@RequestParam("ids") Collection<Long> ids);

    @PutMapping("/items/stock/deduct")
    void deductStock(@RequestBody List<OrderDetailDTO> items);
}

这样,我们就可以看到这个远程调用的资源名了,这就意味着我们可以单独对远程调用进行线程隔离了(远程调用和微服务共享一个线程池,所以是有隔离的必要的),隔离后,这个远程调用服务异常就不会对购物车微服务本身产生影响了,同时由于远程调用服务异常,会执行fallback,于是会返回空响应,从而让购物车其他功能不受影响。

我们对购物车进行测试,发现不会出现异常了

同时将响应空值回去。

5.服务熔断

服务熔断通常会和fallback策略一起使用,服务熔断指当远程调用发生异常时,就直接拒绝远程调用的请求了,这个时候远程调用会走fallback策略**(此时熔断器处于关闭状态)** ,同时,熔断器会在指定时间后尝试放行一次请求到已经异常的远程调用服务**(半开状态)**:

如果这次请求无异常了,熔断器就会重新启用正常远程调用服务(熔断器恢复打开)。

如果这次请求依旧有异常,就继续等待指定时间,然后再次尝试。

我们对远程调用配置熔断规则,直接在Sentinel中配置:

开始测试,可以看到,这个时候商品远程调用已经异常了,所以执行的是fallback策略:

停止测试后,会发现又恢复了,因为熔断器进入半开状态后又向商品远程调用发出了少量请求,这次由于没有大流量测试了,所以直接通过了,所以熔断器恢复打开状态,远程调用也就不再执行fallback策略了,服务恢复。

相关推荐
洛阳泰山2 小时前
快速上手 MaxKB4J:开源企业级 Agentic 工作流系统在 Sealos 上的完整部署指南
java·人工智能·后端
guslegend2 小时前
SpringSecurity授权原理与实战
java
原来是好奇心2 小时前
深入Spring Boot源码(七):测试框架原理与最佳实践
java·源码·springboot
embrace992 小时前
【C语言学习】预处理详解
java·c语言·开发语言·数据结构·c++·学习·算法
山沐与山2 小时前
【Flink】Flink架构深度剖析:JobManager与TaskManager
java·架构·flink
Hello.Reader2 小时前
Flink SQL「SHOW / SHOW CREATE」元数据巡检、DDL 复刻与排障速查(含 Java 示例)
java·sql·flink
Doris_LMS2 小时前
接口、普通类和抽象类
java
重生之我是Java开发战士2 小时前
【数据结构】优先级队列(堆)
java·数据结构·算法
菜鸟233号2 小时前
力扣216 组合总和III java实现
java·数据结构·算法·leetcode