Hystrix:断路器

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wizweiQx-1662386586487)(images/logo.png)]

Spring Cloud Netflix Hystrix:断路器

一、什么是灾难性的雪崩效应

1. 什么是灾难性雪崩效应?

1.1 平稳状态

​ 在微服务架构的项目中,尤其是中大型项目,肯定会出现一个服务调用其他的服务,其他服务又调用别的服务,服务和服务之间形成了一种链式的调用关系。

​ 当少量请求时,对于整个服务链条是没有过多的影响的。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4yQCuKBf-1662386586503)(images/示例1.png)]

1.2 极端状态出现

​ 虽然每个服务的请求都是少量的,但是最终都访问服务T。所以对于服务T来说请2的。所在的服务器CPU压力比较高。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-L2Mczf2L-1662386586506)(images/示例2.png)]

1.3 雪崩的开始

​ 当其中某一个服务突然遇到大量请求时。整个链条上所有服务负载骤增。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iYdhfH8k-1662386586522)(images/示例3.png)]

1.4 灾难到来

​ 导致服务U和服务T的负载过高。运行性能下降。会导致其他调用服务U和服务T的链条出现问题。从而所有的项目可能都出现的问题。

​ 这种情况就称为灾难性的雪崩效应。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bf7DD8vE-1662386586526)(images/示例4.png)]

2. 灾难性雪崩效应发生的原因

​ 造成灾难性雪崩效应的原因,可以简单归结为下述三种:

  1. 服务提供者(Application Service)不可用。如:硬件故障、程序BUG、缓存击穿、并发请求量过大等。

  2. 重试加大流量。如:用户重试、代码重试逻辑等。

  3. 服务调用者(Application Client)不可用。如:同步请求阻塞造成的资源耗尽等。

雪崩效应最终的结果就是:服务链条中的某一个服务不可用,导致一系列的服务不可用,最终造成服务逻辑崩溃。这种问题造成的后果,往往是无法预料的。

二、如何防止灾难性雪崩效应的发生

1. 降级

​ 超时降级、资源不足时(线程或信号量)降级,降级后可以配合降级接口返回托底数据。实现一个fallback方法, 当请求后端服务出现异常的时候, 可以使用fallback方法返回的值.
​ 此方案保证的是:即使某服务出现了问题,整个项目还可以继续运行。

2. 熔断

​ 当失败率(如因网络故障/超时造成的失败率高)达到阀值自动触发降级,熔断器触发的快速失败会进行快速恢复。
​ 通俗理解:熔断就是具有特定条件的降级,当出现熔断时在设定的时间内容就不在请求Application Service了。所以在代码上熔断和降级都是一个注解
​ 此方案保证的是:即使某服务出现了问题,整个项目还可以继续运行,且一段时间内,出现问题的服务不会被访问。

3. 请求缓存

​ 提供了请求缓存。服务A调用服务B,如果在A中添加请求缓存,第一次请求后走缓存了,就不在访问服务B了,即使出现大量请求时,也不会对B产生高负载。
​ 请求缓存可以使用Spring Cache实现。
​ 此方案保证的是:减少对Application Service的调用。

4. 请求合并

​ 提供请求合并。当服务A调用服务B时,设定在5毫秒内所有请求合并到一起,对于服务B的负载就会大大减少,解决了对于服务B负载激增的问题。
​ 此方案保证的是:减少对Application Service的调用。

5. 隔离

​ 隔离分为线程池隔离和信号量隔离。通过判断线程池或信号量是否已满,超出容量的请求直接降级,从而达到限流的作用。
​ 未隔离时,资源公用,所以混乱:
​ 隔离后,资源独享,秩序井然:

三、Hystrix简介

1. Hystrix简介

​ 在Spring Cloud中解决灾难性雪崩效应就是通过Spring Cloud Netflix Hystrix实现的。
​ Hystrix [hɪst’rɪks],中文含义是豪猪,因其背上长满棘刺,从而拥有了自我保护的能力。本文所说的Hystrix(中文:断路器)是Netflix开源的一款容错框架,同样具有自我保护能力。

​ 通俗解释:Hystrix就是保证在高并发下即使出现问题也可以保证程序继续运行的一系列方案。作用包含两点:容错和限流。

​ 在Spring cloud中处理服务雪崩效应,都是在服务调用方(Application Client)实现,需要依赖hystrix组件。

四、搭建基础环境

​ 此工程作为微服务体系中,可能发生问题的节点。

1. 创建父工程

1.1 创建工程

​ 创建工程parent

1.2 POM依赖
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.3.12.RELEASE</version>
</parent>
<dependencyManagement>
    <dependencies>
        <!-- Spring Cloud框架版本集中管理 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>Hoxton.SR12</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

2. 创建POJO工程

2.1 创建工程

​ 创建工程pojo

2.2 POM依赖
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>
2.3 编写实体类型
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User implements Serializable {
    private Integer id;
    private String name;
    private int age;
}

3. 创建Application Service工程

3.1 创建工程

​ 创建工程 app_service

3.2 POM依赖
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    <dependency>
        <groupId>com.bjsxt</groupId>
        <artifactId>pojo</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
</dependencies>
3.3 编写配置文件
server:
  port: 8080
spring:
  application:
    name: app-service
3.4 编写控制器
@RestController
public class ServiceController {
    @Value("${server.port}")
    private int port;

    /**
     * 模拟根据主键批量查询用户。
     */
    @RequestMapping("/batch")
    public List<User> getUsersByIds(@RequestBody List<Integer> ids){
        List<User> result = new ArrayList<>();
        for(Integer id : ids){
            result.add(new User(id, "姓名-" + id, 20));
        }
        return result;
    }
    
    @GetMapping("/test")
    public String test(){
        return "服务器AppService返回结果";
    }
}
3.5 编写启动类型
@SpringBootApplication
public class AppServiceApp {
    public static void main(String[] args) {
        SpringApplication.run(AppServiceApp.class, args);
    }
}

五、降级

​ 降级是指,当请求超时、资源不足等情况发生时进行服务降级处理,不调用真实服务逻辑,而是使用快速失败(fallback)方式直接返回一个托底数据,保证服务链条的完整,避免服务雪崩。具体解决方案如下:

1. 创建工程

​ 创建工程app_client

2. POM依赖

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    <!-- 容灾处理依赖。 -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
    </dependency>
    <dependency>
        <groupId>com.bjsxt</groupId>
        <artifactId>pojo</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
</dependencies>

3. 编写配置文件

server:
  port: 8000
spring:
  application:
    name: hystrix-ribbon-resttemplate

4. 编写服务接口与实现

4.1 编写服务接口
public interface HystrixRestTemplateService {
    String test();
}
4.2 编写服务实现
@Service
public class HystrixRestTemplateServiceImpl implements HystrixRestTemplateService {
    @Autowired
    private RestTemplate restTemplate;
    private final String baseUrl = "http://app-service";

    /**
     * 增加注解 HystrixCommand
     *  注解属性 fallbackMethod - 降级方法的名称
     * @return
     */
    @Override
    @HystrixCommand(fallbackMethod = "downgrade")
    public String test() {
        String url = baseUrl + "/test";
        System.out.println("准备访问远程服务 : /test");
        String result = restTemplate.getForObject(url, String.class);
        System.out.println("远程服务返回:" + result);
        return result;
    }

    /**
     * 降级方法。除方法名称外,其他和具体的服务方法签名一致
     * 降级方法的返回结果,就是托底数据
     */
    public String downgrade(){
        System.out.println("降级方法运行。");
        return "服务器忙,请稍后重试";
    }
}

5. 编写控制器

@RestController
public class HystrixRestTemplateController {
    @Autowired
    private HystrixRestTemplateService service;
	
    @RequestMapping("/test")
    public String getNoParams(){
        System.out.println("控制器执行 - test()");
        return service.test();
    }
}

6. 编写启动类型

/**
 * EnableHystrix - 开启Hystrix功能。netflix hystrix包提供
 * EnableCircuitBreaker - 开启熔断器|断路由|断路器。spring circuitbreaker提供
 *
 * 这两个注解都是让Spring可以识别Hystrix的注解。
 */
@SpringBootApplication
@EnableHystrix
@EnableCircuitBreaker
public class HystrixRestTemplateApp {
    public static void main(String[] args) {
        SpringApplication.run(HystrixRestTemplateApp.class, args);
    }
}

7. 测试

​ 访问 http://localhost:8000/test 得到正常返回结果:
在这里插入图片描述

​ 关闭app_service工程后,再次访问,得到降级结果:
在这里插入图片描述

六、熔断

​ 当一定时间内,异常请求比例(请求超时、网络故障、服务异常等)达到阀值时,启动熔断器,熔断器一旦启动,则会停止调用具体服务逻辑,通过fallback快速返回托底数据,保证服务链的完整。
​ 熔断有自动恢复机制,如:当熔断器启动后,每隔5秒,尝试将新的请求发送给Application Service,如果服务可正常执行并返回结果,则关闭熔断器,服务恢复。如果仍旧调用失败,则继续返回托底数据,熔断器持续开启状态。
​ 降级是出错了返回托底数据,而熔断是出错后如果开启了熔断将会一定时间不在访问application service。
在这里插入图片描述

1. 修改服务接口和实现

1.1 服务接口增加方法
String circuitBreaker();
1.2 服务实现增加方法
/**
 * 测试熔断,强化降级。
 * 注解属性 commandProperties - 具体的容灾配置参数。
 *  类型是HystrixProperty[], HystrixProperty类型是名值对。
 *  名 - 是具体的配置参数名,字符串类型,可以从HystrixPropertiesManager中查看,也可以
 *       使用其中的静态常量。
 *  值 - 参数值,字符串类型。
 */
@Override
@HystrixCommand(fallbackMethod = "circuitBreakerDowngrade", commandProperties = {
    @HystrixProperty(name = HystrixPropertiesManager
                     .EXECUTION_ISOLATION_THREAD_TIMEOUT_IN_MILLISECONDS,
                     value = "5000"), // 统计周期,默认10秒
    @HystrixProperty(name = HystrixPropertiesManager
                     .CIRCUIT_BREAKER_ENABLED,
                     value = "true"), // 是否开启熔断,默认true
    @HystrixProperty(name = HystrixPropertiesManager
                     .CIRCUIT_BREAKER_REQUEST_VOLUME_THRESHOLD,
                     value = "2"), // 统计周期内,错误几次,开启熔断, 默认20
    @HystrixProperty(name = HystrixPropertiesManager
                     .CIRCUIT_BREAKER_ERROR_THRESHOLD_PERCENTAGE,
                     value = "50"), // 统计周期内,错误百分比达到多少,开启熔断, 默认50
    @HystrixProperty(name = HystrixPropertiesManager
                     .CIRCUIT_BREAKER_SLEEP_WINDOW_IN_MILLISECONDS,
                     value = "3000"), // 开启熔断后,多少毫秒不访问远程服务,默认5000毫秒
    @HystrixProperty(name = HystrixPropertiesManager
                     .CIRCUIT_BREAKER_FORCE_OPEN,
                     value = "false"), // 是否强制开启熔断器, 默认false
    @HystrixProperty(name = HystrixPropertiesManager
                     .CIRCUIT_BREAKER_FORCE_CLOSED,
                     value = "false") // 是否强制关闭熔断器, 默认false
})
public String circuitBreaker() {
    String url = baseUrl + "/test";
    System.out.println("准备访问远程服务,地址是:" + url);
    String result = restTemplate.getForObject(url, String.class);
    System.out.println("远程返回结果是:" + result);
    return result;
}

/**
 * 熔断降级方法
 */
public String circuitBreakerDowngrade(){
    System.out.println("熔断降级触发");
    return "网站建设中";
}
1.2.1 注解属性含义解释
  1. CIRCUIT_BREAKER_ENABLED
    “circuitBreaker.enabled”;
    是否开启熔断策略。默认值为true。
  2. CIRCUIT_BREAKER_REQUEST_VOLUME_THRESHOLD
    “circuitBreaker.requestVolumeThreshold”;
    单位时间内(默认10s内),请求超时数超出则触发熔断策略。默认值为20次请求数。通俗说明:单位时间内容要判断多少次请求。
  3. EXECUTION_ISOLATION_THREAD_TIMEOUT_IN_MILLISECONDS
    execution.isolation.thread.timeoutInMilliseconds
    设置单位时间,判断circuitBreaker.requestVolumeThreshold的时间单位,默认10秒。单位毫秒。
  4. CIRCUIT_BREAKER_SLEEP_WINDOW_IN_MILLISECONDS
    “circuitBreaker.sleepWindowInMilliseconds”;
    当熔断策略开启后,延迟多久尝试再次请求远程服务。默认为5秒。单位毫秒。这5秒直接执行fallback方法,不在请求远程application service。
  5. CIRCUIT_BREAKER_ERROR_THRESHOLD_PERCENTAGE
    “circuitBreaker.errorThresholdPercentage”;
    单位时间内,出现错误的请求百分比达到限制,则触发熔断策略。默认为50%。
  6. CIRCUIT_BREAKER_FORCE_OPEN
    “circuitBreaker.forceOpen”;
    是否强制开启熔断策略。即所有请求都返回fallback托底数据。默认为false。
  7. CIRCUIT_BREAKER_FORCE_CLOSED
    “circuitBreaker.forceClosed”;
    是否强制关闭熔断策略。即所有请求一定调用远程服务。默认为false。

2. 控制器增加方法

@RequestMapping("/circuitBreaker")
public String circuitBreaker(){
	System.out.println("控制器执行 - circuitBreaker()");
	return service.circuitBreaker();
}

七、请求缓存

​ Hystrix为了降低访问服务的频率,支持将一个请求与返回结果做缓存处理。如果再次请求的URL没有变化,那么Hystrix不会请求服务,而是直接从缓存中将结果返回。这样可以大大降低访问服务的压力。
​ Hystrix自带缓存。有两个缺点:

  1. 是一个本地缓存。在集群情况下缓存是不能同步的。
  2. 不支持第三方缓存容器。Redis,memcached不支持的。

所以可以利用spring cache。实现请求缓存。
​在降级处理的代码基础上完成下面变化。

1. POM依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

2. 编写配置文件

​ 在application.yml配置文件中增加下述配置:(如果是redis集群使用spring.redis.cluster.nodes进行配置)

spring:
  redis:
    host: 192.168.192.128

3. 编辑服务实现

​ 修改服务实现类中的服务降级方法逻辑,具体如下:

/**
 * 测试请求缓存。
 * 使用Spring Cache技术实现,访问Redis,做缓存处理。
 * Spring Cache是spring-context.jar提供的技术。可以访问多种缓存服务器。
 * 包括redis。
 * 想使用Spring Cache技术,访问缓存服务,需要提供以下依赖:
 *  1. spring-context.jar,直接或间接。
 *  2. 要访问的缓存服务器客户端依赖。如:访问Redis需要Spring Data Redis依赖
 * 使用Spring Cache技术后,查询逻辑的流程是:
 *  1. 访问缓存,查看是否有缓存的结果。如果有直接返回,不执行当前方法。
 *  2. 如果缓存中没有结果,则执行方法。
 *  3. 方法返回结果,会被Spring Cache技术自动保存到缓存服务器中。
 *  4. 方法结束,返回给调用者。
 *  注解: Cacheable
 *  属性:
 *    cacheNames - 缓存中key的前缀
 *    key - 缓存中key的后缀。可以使用表达式赋值。字面值用单引号标记。方法参数变量
 *          使用#参数名标记,可以使用字符串拼接符号 +
 *    完整的缓存key是 前缀 + :: + 后缀
 */
@Override
@HystrixCommand(fallbackMethod = "downgrade")
@Cacheable(key = "'client'",cacheNames = "com:bjsxt")
public String test() {
    String url = baseUrl + "/test";
    System.out.println("准备访问远程服务 : /test");
    String result = restTemplate.getForObject(url, String.class);
    System.out.println("远程服务返回:" + result);
    return result;
}

/**
     * 降级方法。除方法名称外,其他和具体的服务方法签名一致
     * 降级方法的返回结果,就是托底数据
     */
public String downgrade(){
    System.out.println("降级方法运行。");
    return "服务器忙,请稍后重试";
}

4. 测试

​ 通过访问 http://localhost:8000/test 测试请求缓存逻辑,第一次访问时,有远程服务调用,后续请求则不会访问远程服务。并在Redis中,通过命令

keys com:bjsxt*

​ 查看缓存中的数据。

八、请求合并

​ 当没有请求合并处理时:Application Service 负载是Application Client发送请求的总数量。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2PyLMTR7-1662386586619)(images/请求合并-1.png)]

​ 增加请求合并处理后:一段时间范围内的所有请求合并为一个请求。大大的降低了Application Service 负载。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Zo68kuf4-1662386586620)(images/请求合并-2.png)]

1. 什么情况下使用请求合并?

​ 在微服务架构中,我们将一个项目拆分成很多个独立的项目,这些独立的项目通过远程调用来互相配合工作,但是,在高并发情况下,通信次数的增加会导致总的通信时间增加,同时,线程池的资源也是有限的,高并发环境会导致有大量的线程处于等待状态,进而导致响应延迟,为了解决这些问题,我们需要来了解Hystrix的请求合并。

2. 使用请求合并后的缺点

​ 设置请求合并之后,本来一个请求可能5ms就搞定了,但是现在必须再等10ms看看还有没有其他的请求一起的,这样一个请求的耗时就从5ms增加到15ms了,不过,如果我们要发起的命令本身就是一个高延迟的命令,那么这个时候就可以使用请求合并了,因为这个时候时间窗的时间消耗就显得微不足道了,另外高并发也是请求合并的一个非常重要的场景。

3. 代码实现

实现请求合并时,不仅仅需要修改Application Client代码,还需要Application Service的代码配合,因为Application Service必须支持把所有参数捆绑到一起的方式,同时还支持把多个值一起返回。此处理逻辑,对应Application Service工程中的/batch控制处理访问。

3.1 服务接口增加方法
Future<User> getUserById(Integer id);
3.2 服务实现增加方法
    /**
     * 是一个批处理逻辑。是做请求合并处理的方法。
     * 注意,Hystrix中,请求合并处理,具体方法,不是单处理方法。
     * 当前方法,不会执行。由Hystrix通过代理封装后执行。
     * 增加一个额外的批处理方法逻辑。
     *
     * 注解 HystrixCollapser - 代表当前的方法,是一个要合并的方法。
     * 属性:
     *  batchMethod - 批处理方法名称。
     *  scope - 有效范围。
     *   可选值:
     *     com.netflix.hystrix.HystrixCollapser.Scope.GLOBAL
     *     全局有效。可以合并多个客户端的请求。
     *     com.netflix.hystrix.HystrixCollapser.Scope.REQUEST 默认值
     *     请求内有效,只能合并一个请求中的多次远程调用。必须配合指定的框架才能生效。
     *     在当前方法中,会抛出异常。
     *  collapserProperties - 合并约束,类似HystrixCommand注解中的commandProperties
     *   类型是HystrixProperty[]
     * @param id
     * @return
     */
    @Override
    @HystrixCollapser(batchMethod = "getUsersByIds",
            scope = com.netflix.hystrix.HystrixCollapser.Scope.GLOBAL,
            collapserProperties = {
                    @HystrixProperty(name =
                            HystrixPropertiesManager.MAX_REQUESTS_IN_BATCH,
                            value = "5"), // 最多合并多少个请求
                    @HystrixProperty(name =
                            HystrixPropertiesManager.TIMER_DELAY_IN_MILLISECONDS,
                            value = "500") // 最多等待多少毫秒
            }
    )
    public Future<User> getUserById(Integer id) {
        System.out.println("根据主键查询用户方法,Service实现类型中的Override实现");
        return null;
    }

    /**
     * 批处理方法。编写访问远程批处理服务的逻辑。
     * 使用注解HystrixCommand修饰。
     *
     * 问题:
     *  使用RestTemplate访问远程服务的时候,如果服务返回结果是集合。
     *  集合的泛型是自定义类型。因为SpringMVC的@ResponseBody是把Java对象转换成JSON返回,
     *  RestTemplate不知道返回的JSON对应的具体Java类型是什么,
     *  所以使用最通用的类型,Map转换。
     *  远程服务返回的是List<User>,SpringMVC注解ResponseBody处理后,返回的是字符串
     *  [{"id":1,"name":"姓名1","age":20}, {...}]
     *  RestTemplate转换上述JSON格式字符串,[]使用List集合处理。
     *  {"id":1,"name":"姓名1","age":20}是什么类型?使用通用类型Map处理。
     *  JSON对象是属性名id、name、age是map的key。属性值1、姓名1、20是map的value。
     *
     *  手工使用Jackson实现转换处理。
     *
     * 定义要求:
     *  1. 访问修饰符是public
     *  2. 返回值的类型是远程服务的返回类型。
     *  3. 方法命名,随意。不重复即可。
     *  4. 参数表,是List集合类型,泛型是要合并的方法参数类型。名称和要合并的方法参数名一致
     *  5. 抛出异常,不能抛出范围超过要合并的方法的异常类型。
     * @param id
     * @return
     */
    @HystrixCommand
    public List<User> getUsersByIds(List<Integer> id){
        ObjectMapper mapper = new ObjectMapper();

        String url = baseUrl + "/batch";
        System.out.println("准备访问远程服务,地址是:" + url + " , 参数是:" + id);
        List<LinkedHashMap> result =
                restTemplate.postForObject(url, id, List.class);
        List<User> users = new ArrayList<>(result.size());
        for(LinkedHashMap userMap : result){
            try {
                // 把Map转换成JSON格式字符串
                String userJson = mapper.writeValueAsString(userMap);
                // 把JSON格式字符串转换成User类型对象
                User user = mapper.readValue(userJson, User.class);
                // 把处理后的User类型对象,保存到返回结果集合中
                users.add(user);
            }catch (Exception e){
                e.printStackTrace();
            }
        }
        System.out.println("查询结果数量是:" + result.size());
        System.out.println("查询的用户集合是:" + result);
        return users;
    }
3.2.1 注解属性含义

​ @HystrixCollapser 进行请求合并
batchMethod:处理请求合并的方法
scope:合并请求的请求作用域。可选值有global和request。
global:代表所有的请求线程都可以等待可合并。 常用
request:代表一个请求线程中的多次远程服务调用可合
timerDelayInMilliseconds:等待时长,默认10毫秒。
maxRequestInBatch:最大请求合并数量。

3.3 控制器增加方法
/**
 * 根据主键查询用户。
 * 具体实现逻辑,调用远程服务eureka-client-app-service批处理查询实现。
 * 合并当前的请求。把多次请求参数主键,合并成一个集合参数List<Integer>。
 * 一次性访问远程服务,返回的批处理查询结果,拆分后,返回给客户端。
 * @param id
 * @return
 */
@RequestMapping("/getUserById")
public User getUserById(Integer id){
	Future<User> future = service.getUserById(id);
	System.out.println("控制器执行 - getUserById()");
	try {
		return future.get();
	} catch (InterruptedException e) {
		e.printStackTrace();
	} catch (ExecutionException e) {
		e.printStackTrace();
	}
	// 发生了异常。返回null。
	return null;
}

九、隔离

1. 什么是限流

​ 在高并发的系统中,往往需要在系统中做限流,一方面是为了防止大量的请求使服务器过载,导致服务不可用,另一方面是为了防止网络攻击。
​ 通过Hystrix的线程池隔离和信号量隔离控制了线程数量也就实现了限流效果。

2. 线程池隔离

2.1 为什么使用线程池隔离?

​ 比如我们现在有3个业务调用分别是查询订单、查询商品、查询用户,且这三个业务请求都是依赖第三方服务-订单服务、商品服务、用户服务。三个服务均是通过RPC调用。当查询订单服务,假如线程阻塞了,这个时候后续有大量的查询订单请求过来,那么容器中的线程数量则会持续增加直致资源耗尽,导致整个服务对外不可用,这就是灾难性雪崩。如下图:

2.1.1 订单Application Service不可用:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qFkXGBoS-1662386586636)(images/线程池隔离1.png)]

2.1.2 Application Client不可用:

​ 没有线程池隔离的时候可能因为某个接口的高并发导致其他接口也可用。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Bpr4sbNV-1662386586637)(images/线程池隔离2.png)]

2.1.3 增加线程池隔离后:

​ 当使用线程池隔离。不同接口有着自己独立的线程池,即使某个线程池都被占用,也不影响其他线程。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7HekUuj4-1662386586638)(images/线程池隔离3.png)]

2.2 Hystrix中的线程池隔离

​ Hystrix采用Bulkhead Partition舱壁隔离技术2。

​ 舱壁隔离指的的是讲船体内部分为多个隔舱,一旦其中某几个隔舱发生破损进水,水流不会在其他舱壁中流动,从而保证船舱依然具有足够的浮力和稳定性,降低沉船危险。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2AhG7vfJ-1662386586639)(images/线程池隔离4.png)]

2.2.1 线程池隔离的优点
  1. 任何一个服务都会被隔离在自己的线程池内,即使自己的线程池资源填满也不会影响其他服务。
  2. 当依赖的服务重新恢复时,可通过清理线程池,瞬间恢复服务的调用。但是如果是tomcat线程池被填满,再恢复就会很麻烦。
  3. 每个都是独立线程池。一定程度上解决了高并发问题。
  4. 由于线程池中线程个数是有限制,所以也解决了限流问题。
2.2.2 线程池隔离的缺点
  1. 增加了CPU开销。因为不仅仅有Tomcat的线程池,还需要有Hystrix线程池。
  2. 每个操作都是独立的线程,就有排队、调度和上下文切换等问题。
  3. 因为线程切换,在不增加额外代码处理的前提下,请求头丢失。
2.3 代码演示
2.3.1 服务接口增加方法
String thread();
2.3.2 服务实现增加方法
/**
 * 测试线程池隔离。 也可以提供降级、熔断等处理逻辑。
 * 访问远程服务/getNoParams测试。
 * 当前方法,使用独立的线程池。和其他方法隔离。
 * 提供注解HystrixCommand
 *  groupKey - 分组唯一值。默认使用当前类型的类名。代表一个独立接口的唯一命名。
 *   一般都使用类型的名称定义。是使用 字母 ,数字 ,_ 组成的字符串。
 *  commandKey - 命令唯一值。默认使用当前方法名称。代表一个独立接口中的命令唯一命名。
 *   一般使用方法名称定义。是使用 字母 ,数字 ,_ 组成的字符串
 *  threadPoolKey - 隔离的线程池命名中缀。默认使用groupKey的值。
 *   定义后,线程池命名是 "hystrix-" + threadPoolKey。
 *   池中的线程命名是 "hystrix-" + threadPoolKey + 数字编号(从1开始,自然数递增)
 *  threadPoolProperties - 定义隔离线程池的配置信息。如:线程池容量,线程存活时间等。
 *   类型是HystrixProperty[]
 * @return
 */
@Override
@HystrixCommand(groupKey = "MyFirstThread",
		commandKey = "thread",
		threadPoolKey = "pool-name",
		threadPoolProperties = {
		@HystrixProperty(name = HystrixPropertiesManager.CORE_SIZE,
				value = "3"), // 线程池容量
		@HystrixProperty(name = HystrixPropertiesManager.KEEP_ALIVE_TIME_MINUTES,
				value = "5"), // 线程空闲时,最大存活时间是多少分钟
		@HystrixProperty(name = HystrixPropertiesManager.MAX_QUEUE_SIZE,
				value = "5"), // 线程池占满时,最多由多少个请求阻塞等待
		@HystrixProperty(name = HystrixPropertiesManager
				.QUEUE_SIZE_REJECTION_THRESHOLD,
				value = "5") // 当阻塞队列MAX_QUEUE_SIZE占满时,可以有多少个
							 // 请求同时阻塞等待后续处理。
		}
)
public String thread() {
	String url = baseUrl + "/test";
	System.out.println("当前方法使用的线程名称是:" +
			Thread.currentThread().getName());
	String result =
			restTemplate.getForObject(url, String.class);
	System.out.println("远程返回:" + result);
	return result;
}
2.3.3 注解属性说明

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-F4Htzsbr-1662386586640)(images/线程池隔离5.png)]

2.3.4 控制器增加方法
/**
 * 线程池隔离测试。
 * @return
 */
@RequestMapping("/thread")
public String thread(){
	System.out.println(Thread.currentThread().getName() +
			"控制器执行 - thread()");
	return service.thread();
}

3. 信号量隔离

3.1 什么是信号量

​ java.util.concurrent.Semaphore用来控制可同时并发的线程数。通过构造方法指定内部虚拟许可的数量。每次线程执行操作时先通过acquire方法获得许可,执行完毕再通过release方法释放许可。如果无可用许可,那么acquire方法将一直阻塞,直到其它线程释放许可。
​ 如果采用信号量隔离技术,每接收一个请求,都是服务自身线程去直接调用依赖服务,信号量就相当于一道关卡,每个线程通过关卡后,信号量数量减1,当为0时不再允许线程通过,而是直接执行fallback逻辑并返回,说白了仅仅做了一个限流。

3.2 信号量隔离的优点

​ 没有线程切换,性能好。

3.3 信号量隔离的缺点

​ 所有请求处理共用一个线程池。所有接口相互影响更大。容易因为一个接口问题,导致其他接口同时出错。

3.4 代码演示
3.4.1 服务接口增加方法
String semaphore();
3.4.2 服务实现增加方法
/**
 * 测试信号量隔离
 * 就是定义一个阈值,设定同时处理的请求上限。当处理的请求达到阈值时,
 * 后续请求,降级处理。
 *
 * 使用注解 HystrixCommand 修饰
 *  commandProperties - 描述隔离方案和信号量隔离阈值
 *
 * @return
 */
@Override
@HystrixCommand(fallbackMethod = "downgrade",
		commandProperties = {
		@HystrixProperty(name = HystrixPropertiesManager
				.EXECUTION_ISOLATION_STRATEGY,
				value = "SEMAPHORE"), // 隔离方案。默认线程池隔离。 THREAD | SEMAPHORE
		@HystrixProperty(name = HystrixPropertiesManager
				.EXECUTION_ISOLATION_SEMAPHORE_MAX_CONCURRENT_REQUESTS,
				value = "2"), // 最大信号量
		@HystrixProperty(name = HystrixPropertiesManager
				.EXECUTION_ISOLATION_THREAD_TIMEOUT_IN_MILLISECONDS,
				value = "1000") // 超时时间。信号量隔离中无效,线程池隔离中有效。默认1秒
		}
)
public String semaphore() {
	String url = baseUrl + "/test";
	System.out.println("远程地址是:" + url + " , 线程名称是:" +
			Thread.currentThread().getName());
	String result = restTemplate.getForObject(url, String.class);

	try {
		Thread.sleep(800);
	} catch (InterruptedException e) {
		e.printStackTrace();
	}

	System.out.println("远程返回:" + result);
	return result;
}
3.4.3 注解属性说明

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-B0nZlS9m-1662386586642)(images/信号量隔离1.png)]

3.4.4 控制器增加方法
/**
 * 信号量隔离测试。
 * @return
 */
@RequestMapping("/semaphore")
public String semaphore(){
	System.out.println(Thread.currentThread().getName() +
			"控制器执行 - semaphore()");
	return service.semaphore();
}

4. 线程池隔离和信号量隔离分析

4.1 线程池隔离和信号量隔离的区别
线程池隔离信号量隔离
线程请求线程和调用远程服务的线程不是同一个线程请求线程和调用远程服务的线程是同一个线程
开销较大(包括排队、调度、上下文等开销)很低(无线程切换)
异步支持不支持
并发支持支持(线程池容量上限)支持(信号量阈值上限)
传递Header无法传递Http Header可以传递Http Header
支持超时支持超时不支持超时
4.2 线程池隔离和信号量隔离的选择
4.2.1 什么时候选择线程池隔离

请求并发量大,并且耗时较长(如计算量级大、访问其他服务、访问数据库等),使用线程池隔离可以保证容器线程池(Tomcat线程池)利用率更好,不会因为远程服务调用的原因,导致线程处于等待、阻塞等状态,可以实现快速失败返回。

4.2.2 什么时候选择信号量隔离

请求并发量大,并且耗时较短(如计算量级小、访问缓存等),使用信号量隔离可以保证快速返回,不会因为线程切换而导致不必要的损耗。且因为这种服务返回快速,并不会长时间占用容器线程(Tomcat线程),提高了服务的整体性能。

十、 Openfeign的容灾处理

​ 当使用OpenFeign调用远程服务超时会出现500错误。可使用Hystrix来实现容灾处理。

​ 在OpenFeign启动器依赖中,默认包含Hystrix核心类库,但不包含Hystrix启动器中的全部资源,所以可实现Hystrix容灾处理方案,但不能实现Hystrix其他扩展处理(如@EnableHystrix注解就不包含在默认资源中)。

1. POM依赖

​ 在Application Client工程中增加下述依赖:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

2. 编写配置文件

​ 在application.yml配置中增加下述配置:默认情况下OpenFeign的hystrix是不开启的,需要手动开启。

feign:
  hystrix:
    enabled: true

3. 编写服务接口

​ 编写FeignHystricService服务接口。使用OpenFeign声明式调用。fallback属性表示降级后处理类型。也可使用内部类实现。

@FeignClient(name = "app-service", fallback = FeignHystricServiceFallback.class)
public interface FeignHystricService {
    @PostMapping("/test")
    String test();
}

4. 编写服务实现

@Component
public class FeignHystricServiceFallback implements FeignHystricService {
    @Override
    public String test() {
        System.out.println("执行方法,出现服务降级,返回托底数据");
        String result = "因为Provider连接不上了,返回托底数据";
        return map;
    }
}

5. 修改启动类型

@SpringBootApplication
@EnableHystrix
@EnableFeignClients
@EnableCircuitBreaker
public class HystrixRestTemplateApp {
    public static void main(String[] args) {
        SpringApplication.run(HystrixRestTemplateApp.class, args);
    }
}

6. Openfeign中的熔断配置

​ 在Openfeign技术中,如果需要开启熔断策略,则只需要在application.yml配置文件中增加下述配置即可。

hystrix: # hystrix 容灾配置
  command: # hystrix 命令配置,就是具体的容灾方案
    default: # 默认环境,相当于全局范围,后续的配置,参考HystrixPropertiesManager中的常量配置
      circuitBreaker: # 熔断配置, 常用。 其他配置不常用。
        enabled: true
        requestVolumeThreshold: 2
        sleepWindowInMilliseconds: 2000
        errorThresholdPercentage: 50
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值