查看之前的博客可以点击顶部的【分类专栏】
为了解决上一篇博客的疑惑,我们开始继续实验。
服务隔离
没有线程池隔离的项目所有接口都运行在一个同一个线程池中,当某个接口压力过大出现故障时,会导致资源大量被消耗,从而影响其它接口的调用,直接导致服务雪崩效应。为防止服务雪崩,可以使用服务隔离机制(线程池方式或信号量),保证每个服务互不影响。
服务隔离分为:线程池隔离和信号量隔离。线程池隔离适用于 99% 的业务场景,而信号量隔离只适用于 1% 的场景,因为信号量隔离是同步的,会阻塞请求。
1、线程池隔离
我们在前面博客的基础上,在产品的微服务接口,增加线程名称的打印。
说明它们使用相同的线程池。
在 product-server 的 pom.xml 添加 Hystrix 依赖
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
</dependencies>
业务层 ProductServiceImpl 详细代码如下:
package com.study.service.impl;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import com.netflix.hystrix.contrib.javanica.conf.HystrixPropertiesManager;
import com.study.entity.ProductEntity;
import com.study.service.ProductService;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
/**
* @author biandan
* @description
* @signature 让天下没有难写的代码
* @create 2021-05-27 下午 4:12
*/
@Service
public class ProductServiceImpl implements ProductService {
//根据产品ID获取信息,这里先写固定
@HystrixCommand(
groupKey = "produce-server-single-pool",//服务名称,相同名称使用同一个线程池
commandKey = "getById",//接口名称,默认方法名
threadPoolKey = "produce-server-single-pool",//线程池名称,相同名称使用同一个线程池
commandProperties = {
//超时时间,默认 1000 ms
@HystrixProperty(name = HystrixPropertiesManager.EXECUTION_ISOLATION_THREAD_TIMEOUT_IN_MILLISECONDS,
value = "5000")
},
threadPoolProperties = {
@HystrixProperty(name = HystrixPropertiesManager.CORE_SIZE,value = "3"),//线程池大小
@HystrixProperty(name = HystrixPropertiesManager.MAX_QUEUE_SIZE,value = "100"),//最大队列阈值,默认-1
@HystrixProperty(name = HystrixPropertiesManager.KEEP_ALIVE_TIME_MINUTES,value = "2"),//线程存活时间,默认1分钟
@HystrixProperty(name = HystrixPropertiesManager.QUEUE_SIZE_REJECTION_THRESHOLD,value = "100")//超出队列等待阈值拒绝策略
},
fallbackMethod = "getByIdFallback" //服务容错回调函数
)
@Override
public ProductEntity getById(Integer id) {
System.out.println(Thread.currentThread().getName()+" 单个产品接口。getById");
ProductEntity entity = new ProductEntity();
entity.setId(1);
entity.setProductName("霸王防脱洗发液");
entity.setPrice(new Float("55.55"));
return entity;
}
//单个商品的托底数据,注意参数要与被调用的一致
public ProductEntity getByIdFallback(Integer id){
System.out.println(Thread.currentThread().getName()+" getByIdFallback 托底数据");
ProductEntity entity = new ProductEntity();
entity.setId(110);
entity.setProductName("单个商品托底数据");
entity.setPrice(new Float(11.11));
return entity;
}
//根据集合id查询
@HystrixCommand(
groupKey = "produce-server-list-pool",//服务名称,相同名称使用同一个线程池
commandKey = "getList",//接口名称,默认方法名
threadPoolKey = "produce-server-list-pool",//线程池名称,相同名称使用同一个线程池
commandProperties = {
//超时时间,默认 1000 ms
@HystrixProperty(name = HystrixPropertiesManager.EXECUTION_ISOLATION_THREAD_TIMEOUT_IN_MILLISECONDS,
value = "5000")
},
threadPoolProperties = {
@HystrixProperty(name = HystrixPropertiesManager.CORE_SIZE,value = "6"),//线程池大小
@HystrixProperty(name = HystrixPropertiesManager.MAX_QUEUE_SIZE,value = "100"),//最大队列阈值,默认-1
@HystrixProperty(name = HystrixPropertiesManager.KEEP_ALIVE_TIME_MINUTES,value = "2"),//线程存活时间,默认1分钟
@HystrixProperty(name = HystrixPropertiesManager.QUEUE_SIZE_REJECTION_THRESHOLD,value = "100")//超出队列等待阈值拒绝策略
},
fallbackMethod = "getListFallback" //服务容错回调函数
)
@Override
public List<ProductEntity> getList(List<Integer> ids) {
System.out.println(Thread.currentThread().getName()+" 多个产品接口。getList");
List<ProductEntity> list = new ArrayList<>();
ProductEntity entity = new ProductEntity();
entity.setId(1);
entity.setProductName("霸王防脱洗发液");
entity.setPrice(new Float("55.55"));
list.add(entity);
ProductEntity entity_2 = new ProductEntity();
entity_2.setId(2);
entity_2.setProductName("人字拖");
entity_2.setPrice(new Float("9.9"));
list.add(entity_2);
return list;
}
//集合商品的托底数据,注意参数要与被调用的一致
public List<ProductEntity> getListFallback(List<Integer> ids){
System.out.println(Thread.currentThread().getName()+" getListFallback 托底数据");
List<ProductEntity> list = new ArrayList<>();
ProductEntity entity = new ProductEntity();
entity.setId(111);
entity.setProductName("托底数据-霸王");
entity.setPrice(new Float("55.55"));
list.add(entity);
ProductEntity entity_2 = new ProductEntity();
entity_2.setId(112);
entity_2.setProductName("托底数据-人字拖");
entity_2.setPrice(new Float("9.9"));
list.add(entity_2);
return list;
}
}
@HystrixCommand 注解的说明:
1:commandKey:配置全局唯一标识服务的名称,如果不配置,则默认是@HystrixCommand注解修饰的函数的函数名。
2:groupKey:一个比较重要的注解,配置全局唯一标识服务分组的名称,Hystrix会根据组来组织和统计命令的告、仪表盘等信息。
3:threadPoolKey:对线程池进行设定,细粒度的配置,相当于对单个服务的线程池信息进行设置,也可多个服务设置同一个threadPoolKey构成线程组。
4:fallbackMethod:@HystrixCommand注解修饰的函数的回调函数,@HystrixCommand修饰的函数必须和这个回调函数定义在同一个类中,因为定义在了同一个类中,所以fackback method可以是public/private均可。
5:commandProperties:配置该命令的一些参数,如executionIsolationStrategy配置执行隔离策略,默认是使用线程隔离
6:threadPoolProperties:线程池相关参数设置
7:defaultFallback:默认的回调函数,该函数的函数体不能有入参,返回值类型与@HystrixCommand修饰的函数体的返回值一致。如果指定了fallbackMethod,则fallbackMethod优先级更高。
com.netflix.hystrix.contrib.javanica.exception.FallbackDefinitionException: fallback method wasn't found: getByIdFallback([class java.lang.Integer])
因为我们之前在 product-server 的 yml 配置文件分配了10个线程,因此我们现在分配 6 个给 list 接口,3个给单个接口。在启动类增加注解 @EnableHystrix ,开启 Hystrix。
分别请求单个、多个产品接口,可以看到我们的线程池已经分配好了。
然后我们继续用 JMeter 测试高并发的场景。30个线程50次循环的请求 getList 接口,然后订单服务去请求单个产品的接口。注意需要把订单服务的【请求缓存】功能注释掉,否则直接从缓存里获取数据。
然后重启2个服务,先使用 Jmeter 发送并发请求,然后订单服务发送请求。可以看到产品服务的控制台输出
这次并没有被超时的现象发生,因为获取单个产品、集合是分开的线程池。
线程池隔离的优缺点
优点:
- 使用线程池隔离可以安全隔离依赖的服务,减少所依赖服务发生故障时的影响范围。
- 当失败的服务再次变得可用时,线程池将清理并立即恢复,而不需要长时间的恢复
- 独立的线程池提高了并发性。
缺点:
- 请求在线程池中执行,会带来任务的调度、排队和上下文切换带来的CPU开销
- 因为涉及到跨线程,那么就会存在 ThreadLocal 数据的传递问题。比如在主线程初始化的 ThreadLocal 变量,在线程池中的线程无法获取到。
2、信号量隔离
每次调用线程,先获取到信号量,获取到信号量才可以继续往后执行。执行完毕需要归还信号量。
如果信号量已经被获取完毕,新进来的请求需要排队,会进入阻塞。或者拒绝处理请求。
因为默认是线程池隔离,因此先把所有线程池隔离注释掉(别注释掉熔断的 fallback 方法),然后在 getList 接口增加信号量隔离的代码:
//信号量隔离
@HystrixCommand(
commandProperties = {
//超时时间,默认1000毫秒
@HystrixProperty(name = HystrixPropertiesManager.EXECUTION_ISOLATION_THREAD_TIMEOUT_IN_MILLISECONDS,
value = "3000"),
//信号量隔离
@HystrixProperty(name = HystrixPropertiesManager.EXECUTION_ISOLATION_STRATEGY,
value = "SEMAPHORE"),
//信号量最大并发,调小方便测试
@HystrixProperty(name = HystrixPropertiesManager.EXECUTION_ISOLATION_SEMAPHORE_MAX_CONCURRENT_REQUESTS,
value = "5")
},
fallbackMethod = "getListFallback" //服务容错回调函数
)
然后把 getList 休眠代码去掉。重启 product-server 服务,然后使用 JMeter 测试:
说明在大量并发的情况下,信号量达到了最大值,直接返回 fallback 方法的内容。
线程池隔离和信号量隔离,分别在什么样的场景下去使用呢?
线程池:适合绝大多数的场景,99%的,线程池,对依赖服务的网络请求的调用和访问,timeout这种问题
信号量:你的访问不是对外部依赖的访问,而是对内部的一些比较复杂的业务逻辑的访问,但是像这种访问,系统内部的代码,其实不涉及任何的网络请求,那么只要做信号量的普通限流就可以了,因为不需要去捕获 timeout 类似的问题,算法+数据结构的效率不是太高,并发量突然太高,因为这里稍微耗时一些,导致很多线程卡在这里的话,不太好,所以进行一个基本的资源隔离和访问,避免内部复杂的低效率的代码,导致大量的线程被 hang 住。
本篇博客代码地址:https://pan.baidu.com/s/1KRv6n7_LDfIA6N5CcZsgKw 提取码:xxfz