线程隔离和熔断降级并配置对应的服务降级
目录
隔离和降级
FeignClient整合Sentinel
通过Feign设置服务降级
1.创建类实现FallbackFactory接口,并让这个类和使用@FeignClient的接口类绑定
2.让order-service服务的feign开启sentinel
3.测试,只开启order-service服务,而不开启user-order服务
总结:
线程隔离
线程隔离的实现方式
sentinel的线程隔离
1)配置隔离规则
2)jemter测试
总结
熔断降级
慢调用
编辑 异常比例、异常数
授权规则
基本规则
如何获取origin
案例:我们需要只能从gateway网关才能访问资源接口
1.在order-service服务中编写类实现RequestOriginParser接口
2.gateway添加origin请求头
3.在sentinel中设置授权规则
4.结果
自定义异常结果
异常类型
自定义异常处理类实现BlockExceptionHandler接口
总结:
隔离和降级
虽然限流可以尽量避免因高并发而引起的服务故障,但服务还是会因为某些原因(服务自己出现问题)而故障。而要将这些故障控制在一定范围内,避免雪崩,就要靠线程隔离(舱壁模式)和熔断降级手段了
线程隔离就是:调用者在调用服务提供者时,给每个调用的请求分配独立线程池,出现故障时,最多消耗这个线程池内资源,避免把调用者的所有资源耗尽。
可以看到下图,我们已经规定了访问其他服务可以使用的最大线程数为10个,服务C出现问题,就只会有10个线程无法使用,而不会导致服务A中的所有线程都不可用,导致线程A宕机。
熔断降级:是在调用方这边加入断路器,统计对服务提供者的调用,如果调用的失败比例过高,则熔断该业务,不允许访问该服务的提供者了。
由下图可以看到,当服务A向服务D由两个请求都出现异常时,在服务A和服务D之间就会出现一道熔断墙,阻止服务A的其他请求向服务D(直接报错,抛异常)
我们发现不能处理的请求都是直接抛异常,终止请求,我们可以设置降级服务
如:如果访问queryById接口失败,就去访问queryByIdFalback接口作为备选
FeignClient整合Sentinel
SpringCloud中,微服务调用都是通过Feign来实现的,因此做客户端保护必须整合Feign和Sentinel。
通过Feign设置服务降级
在orderService微服务中会使用UserFeignClient去访问用户的信息
我们需要给UserFiegnClient的方法设置服务降级
User user = userFeignClient.queryById(order.getUserId());
1.创建类实现FallbackFactory接口,并让这个类和使用@FeignClient的接口类绑定
不要导错包 ,是feign.hystrix.FallbackFactory包下的FallbackFactory
import feign.hystrix.FallbackFactory;
//直接加@Component不能被启动类扫描到,因为与启动类不在同一个包
public class UserFeignClientFallbackFactory implements FallbackFactory<UserFeignClient> {
//cause为远程调用UserFeignClient时出现的异常
@Override
public UserFeignClient create(Throwable cause) {
cause.printStackTrace();
return new UserFeignClient() {
//重写UserFeign的所有方法,作为服务降级的备选处理方法
@Override
public User queryById(Long id) {
//设置默认用户信息
User user = new User();
user.setId(null);
user.setAddress(null);
user.setUsername("默认用户");
return user;
}
@Override
public String getUser(String username, Integer age) {
return "没有这个用户";
}
};
}
}
维护到IoC容器中
@Bean
public UserFeignClientFallbackFactory userFeignClientFallbackFactory(){
return new UserFeignClientFallbackFactory();
}
让UserFeignClient与UserFeignClientFallbackFactory绑定服务降级
//与FallbackFactory绑定降级服务
@FeignClient(value = "user-service",configuration = FeignConfig.class,fallbackFactory = UserFeignClientFallbackFactory.class)
public interface UserFeignClient {
@RequestMapping("/user/{id}")//使用Get方式发送请求,获取到的响应数据反序列化成User类型
User queryById(@PathVariable("id")Long id);//这里是把方法参数放到请求路径中,与controller相反
@RequestMapping("/user/getUser")
//使用@RequestParam注解让Feign知道这两个请求参数值是要加到url地址中的
String getUser(@RequestParam("username") String username, @RequestParam("age") Integer age);
}
2.让order-service服务的feign开启sentinel
feign:
httpclient:
enabled: true # 开启feign对HttpClient的支持
max-connections: 200 # 最大的连接数
max-connections-per-route: 50 # 每个路径的最大连接数
sentinel:
enabled: true #与sentinel整合,开启服务降级
3.测试,只开启order-service服务,而不开启user-order服务
如果我们之前没有编写服务降级,就会直接报错,查不到用户信息,但是我们已经开启的服务降级
访问接口,返回的用户信息是我们设置的默认用户信心,服务降级成功
总结:
Feign整合Sentinel的步骤:
- 在application.yml中配置:feign.sentienl.enable=true
- 给FeignClient编写FallbackFactory并注册为Bean
- 将FallbackFactory配置到FeignClient
线程隔离
线程隔离的实现方式
线程隔离有两种方式实现:
-
线程池隔离
-
信号量隔离(Sentinel默认采用)
线程池隔离:给每个服务调用业务分别分配一个线程池,利用线程池本身实现隔离效果
优点:
- 支持主动超时:服务I的请求主线程到线程池时,如果线程池没有及时给出响应,就直接报错,支持主动超时
- 支持异步调用:服务I的请求主线程,访问到访问服务A的线程池时,线程池会自己抽出一个空闲线程去访问服务A,而不用请求主线程主动访问
缺点:
线程的二外开销大
信号量隔离:不创建线程池,而是计数器模式,记录业务使用的线程数量,达到信号量上限时,禁止新的请求。
优点:
轻量级,不需要额外的线程开销,一个线程执行到底
缺点:
- 不支持主动超时
- 不支持异步调用
sentinel的线程隔离
在添加限流规则时,可以选择两种阈值类型:
-
QPS:就是每秒的请求数,在快速入门中已经演示过
-
线程数:是该资源能使用用的tomcat线程数的最大值。也就是通过限制线程数量,实现线程隔离(舱壁模式)。
案例需求:给 order-service服务中的UserClient的查询用户接口设置流控规则,线程数不能超过 2。然后利用jemeter测试。
1)配置隔离规则
因为order-service服务的feign已经整合的sentinel,也会把feign接口的方法作为资源
选择feign接口后面的流控按钮:
这里设置了访问/user/{id}接口时,同时最多使用的线程数为2个
2)jemter测试
一瞬间同时创建10个线程
一次发生10个请求,有较大概率并发线程数超过2,而超出的请求会走之前定义的失败降级逻辑。
访问/user/{id}时同时使用的线程超过2时,就是触发线程隔离,然后服务降级
总结
线程隔离的两种手段是?
-
信号量隔离
-
线程池隔离
信号量隔离的特点是?
- 基于计数器模式,简单,开销小
线程池隔离的特点是?
- 基于线程池模式,有额外开销,但隔离控制更强
熔断降级
熔断降级是解决雪崩问题的重要手段。其思路是由断路器统计服务调用的异常比例、慢请求比例,如果超出阈值则会熔断该服务。即拦截访问该服务的一切请求;而当服务恢复时,断路器会放行访问该服务的请求。 熔断这个服务之后,我们会设置一个熔断时间,一旦过了这个时间就会尝试放行一次请求,如果这个这个请求还是异常,就继续熔断,等待下一次的熔断时间结束
断路器控制熔断和放行是通过状态机来完成的:
状态机包括三个状态:
- closed:关闭状态,断路器放行所有请求,并开始统计异常比例、慢请求比例。超过阈值则切换到open状态
- open:打开状态,服务调用被熔断,访问被熔断服务的请求会被拒绝,快速失败,直接走降级逻辑。Open状态5秒后(熔断时间到)会进入half-open状态
- half-open:半开状态,放行一次请求,根据执行结果来判断接下来的操作。
- 请求成功:则切换到closed状态
- 请求失败:则切换到open状态
断路器熔断策略有三种:慢调用、异常比例、异常数
慢调用
慢调用:业务的响应时长(RT)大于指定时长的请求认定为慢调用请求。在指定时间内,如果请求数量超过设定的最小数量,慢调用比例大于设定的阈值,则触发熔断。
这个设置了10000ms时间范围内如果10个请求内有5个以上请求的响应时间大于500ms,就会触发熔断状态 ,熔断的时长为5s,过了5s才会尝试放行一次请求,如果成功就取消熔断,如果还是失败,就继续熔断
异常比例、异常数
异常比例或异常数:统计指定时间内的调用,如果调用次数超过指定请求数,并且出现异常的比例达到设定的比例阈值(或超过指定异常数),则触发熔断。
例如,一个异常比例设置:
解读:统计最近1000ms内的请求,如果请求量超过10次,并且异常比例不低于0.4,则触发熔断。
一个异常数设置:
解读:统计最近1000ms内的请求,如果请求量超过10次,并且异常比例不低于2次,则触发熔断。
授权规则
授权规则可以对请求方来源做判断和控制。
基本规则
授权规则可以对调用方的来源做控制,有白名单和黑名单两种方式。
-
白名单:来源(origin)在白名单内的调用者允许访问
-
黑名单:来源(origin)在黑名单内的调用者不允许访问
-
资源名:就是受保护的资源,例如/order/{orderId}
-
流控应用:是来源者的名单,
- 如果是勾选白名单,则名单中的来源被许可访问。
- 如果是勾选黑名单,则名单中的来源被禁止访问。
我们允许请求从gateway到order-service,不允许浏览器访问order-service,那么白名单中就要填写网关的来源名称(origin)。
如何获取origin
Sentinel是通过RequestOriginParser这个接口的parseOrigin来获取请求的来源的。
public interface RequestOriginParser {
/**
* 从请求request对象中获取origin,获取方式自定义
*/
String parseOrigin(HttpServletRequest request);
}
这个方法的作用就是从request对象中,获取请求者的origin值并返回。
默认情况下,sentinel不管请求者从哪里来,返回值永远是default,也就是说一切请求的来源都被认为是一样的值default。i
因此,我们需要自定义这个接口的实现,让不同的请求,返回不同的origin。这样一来接口资源就可以知道访问的来源(origin)是哪一个
案例:我们需要只能从gateway网关才能访问资源接口
我们可以在gateway中给每一个请求添加origin=gateway的请求头,让sentinel只能放行origin为gateway的请求去访问资源
1.在order-service服务中编写类实现RequestOriginParser接口
这个类的作用就是从请求中解析出origin(请求来源)并返回
package cn.itcast.order.sentinel;
import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.RequestOriginParser;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
/**
* 从请求中获取origin
*/
@Component
public class HeaderOriginParser implements RequestOriginParser {
//返回访问这个资源的origin来源
@Override
public String parseOrigin(HttpServletRequest request) {
//获取origin的请求头信息
String origin = request.getHeader("origin");
//浏览器访问时没有添加origin请求头
if(StringUtils.isBlank(origin)){
return "blank";
}
return origin;
}
}
2.gateway添加origin请求头
default-filters: #给所有的微服务都加上这个过滤器群
- AddRequestHeader=origin,gateway
3.在sentinel中设置授权规则
设置只有origin为gateway的请求才能访问资源
4.结果
从gateway发出的请求可以访问
直接从浏览器发送请求会失败
自定义异常结果
默认情况下,发生限流、降级(被调用的服务出问题)、授权拦截时,都会抛出异常到调用方。异常结果都是显示flow limmiting(限流)。这样不够友好,无法得知是限流还是降级还是授权拦截。
异常类型
而如果要自定义异常时的返回结果,需要实现BlockExceptionHandler接口:
public interface BlockExceptionHandler {
/**
* 处理请求被限流、降级、授权拦截时抛出的异常:BlockException
*/
void handle(HttpServletRequest request, HttpServletResponse response, BlockException e) throws Exception;
}
这个方法有三个参数:
- HttpServletRequest request:request对象
- HttpServletResponse response:response对象
- BlockException e:被sentinel拦截时抛出的异常
这里的BlockException包含多个不同的子类:
异常 | 说明 |
---|---|
FlowException | 限流异常 |
ParamFlowException | 热点参数限流的异常 |
DegradeException | 降级异常 |
AuthorityException | 授权规则异常 |
SystemBlockException | 系统规则异常 |
自定义异常处理类实现BlockExceptionHandler接口
import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.BlockExceptionHandler;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.csp.sentinel.slots.block.authority.AuthorityException;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeException;
import com.alibaba.csp.sentinel.slots.block.flow.FlowException;
import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowException;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
public class SentinelExceptionHandler implements BlockExceptionHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, BlockException e) throws Exception {
String msg = "未知异常";
int status = 429;
if (e instanceof FlowException) {
msg = "请求被限流了";
} else if (e instanceof ParamFlowException) {
msg = "请求被热点参数限流";
} else if (e instanceof DegradeException) {
msg = "请求被降级了";
} else if (e instanceof AuthorityException) {
msg = "没有权限访问";
status = 401;
}
response.setContentType("application/json;charset=utf-8");
response.setStatus(status);
response.getWriter().println("{\"msg\": " + msg + ", \"status\": " + status + "}");
}
}
重启测试,在不同场景下,会返回不同的异常消息.
总结:
限流是对一个服务进行保护,防止高并发导致服务崩溃,本质上服务没有出问题
线程隔离和熔断降级式防止一个服务出现问题,而当这个服务出现问题时,原先的请求就得不到想要的响应数据,而是一个异常,所以我们可以设置一个服务降级,不直接抛出异常,而是写一种默认的处理方法(如返回默认账号)