微服务调用链路中的某个服务故障,引起整个链路中的所有微服务都不可用,这就是雪崩。
解决雪崩问题的常见方式
认识Sentinel
Sentinal时阿里巴巴开源的一款微服务控制组件,它具有以下特征:
安装Sentinel控制台
Sentinal官方提供了UI控制台,方便我们对系统做限流设置。
java -jar sentinel-dashboard-2.0.0-alpha-preview.jar
账号:sentinel
密码:sentinel
配置项 | 默认值 | 说明 |
---|---|---|
server.port | 8080 | 服务端口 |
sentinel.dashboard.auth.username | sentinel | 默认用户名 |
sentinel.dashboard.auth.password | sentinel | 默认密码名 |
java -jar sentinel-dashboard-2.0.0-alpha-preview.jar -Dserver=8090
导入项目:
项目:百度网盘地址,提取码bzr4
sql文件:百度网盘地址,提取码: ycz7
安装nacos
简介:Nacos是Alibaba的产品,是SpringCloud中的一个组件,相比Eureka功能更加丰富
- 官网下载Nacos包
- 在终端进入Nacos包的bin目录下,输入
sh startup.sh -m standalone
即可启动- 进入可视化页面
http://127.0.0.1:8848/nacos/
,账号和密码都是nacos
这里在上面引入项目的order-service中整合sentinal,并且连接sentinel控制台,步骤如下:
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-sentinelartifactId>
dependency>
spring:
datasource:
url: jdbc:mysql://mysql:3306/cloud_order
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
application:
name: orderservice
spring:
cloud:
nacos:
discovery:
server-addr: localhost:8845
sentinel:
transport:
dashboard: localhost:8080
簇点链路:就是项目内的内调链路,链路中被监控的每个接口就是一个资源。默认情况下sentinel会监控SpringMVC的每一个端点(Endpoint-可以理解为Controller方法),因此SpringMVC的每一个端点(Endpoint)就是调用 链路中的一个资源
流控熔断都是针对链路中的资源来设置的,因此我们可以点击对应资源后面的按钮来设置规则:(下面以设置流控为案例)
2. 案例:给/order/{orderId}这个资源设置流控规则,QPS不能超过5,然后使用Jmeter测试
可以发现失败的请求都被Sentinel阻塞了
查看实时监控
在添加限流规则时,点击高级选项,可以选择三种流控模式:
案例:需求
- 在orderController新建两个端点:/order/query 和/order/update,无需业务实现
- 配置流控规则,当/order/update资源被访问的QPS超过5时,对/order/query请求限流
@GetMapping("/query")
public String queryOrder() {
// 根据id查询订单并返回
return "查询订单成功";
}
@GetMapping("/update")
public String updateOrder() {
// 根据id查询订单并返回
return "更新订单成功";
}
3. 对query添加限流
4. jmeter测试
(这里不断请求update100s,期间无法访问query)
流控模式-链路
案例:有查询订单和创建订单业务,两者都需要查询商品。针对从查询订单进入到查询商品的请求统计,并设置限流
(1)在OrderService中添加一个queryGoods,不用实现业务
public String queryGoods(){
return "queryGoods方法被调用";
}
(2)在OrderController中,改造/order/query端点,调用OrderService中的queryGoods方法
@GetMapping("/query")
public String queryOrder() {
// 根据id查询订单并返回
return orderService.queryGoods();
}
(3)在OrderController中添加一个/order/save断电,调用OrderService的queryGoods方法
@GetMapping("/save")
public String saveOrder() {
// 根据id查询订单并返回
return orderService.queryGoods();
}
(4)给queryGoods设置限流规则,从/order/query进入queryGoods方法现在QPS必须小于2
@SentinelResource("goods")
public String queryGoods(){
return "queryGoods方法被调用";
}
spring:
cloud:
sentinel:
web-context-unify: false
流控效果是指请求达到流控阈值时应该采取的措施,包括三种:
流控效果-warm up
warm up也叫预热模式,是应对服务冷启动的一种方案,请求阈值初始值是threshold/coldFactor,持续指定时间后每,逐渐提高threshold值,而coldeFactor的默认值是3
例如:我设置QPS的threshold为10,预热时间为5秒,那么初始阈值就是10/3,也就是3,然后在5秒后逐渐增长到10
案例:给/order/{orderId}这个资源设置限流,最大QPS为10,利用warm up 效果,预热时长为5s
qps是波动上升
流控效果-排队等待
当请求超过QPS阈值时,快速失败和warm up会拒绝新的请求并抛出异常。而排队等待是让所有请求进入一个队列中,然后按照阈值运行的时间间隔依次执行。后来的请求必须等待前面的请求执行完成,如果请求等待的预期时间超出最长时长,则会被拒绝
案例:给/order/{orderId}这个资源设置限流,最大QS为10,利用排队的流控效果,超时时长设置为5s
达到了流量整形的效果
之前的限流是统计访问某个资源的所有请求,判断是否超过QPS阈值。而热点参数限流是分别统计参数值相同的请求,是否超过QPS阈值
案例:给/order/{orderId}这个资源添加热点参数限流,规则如下:
- 默认的热点参数规则时1秒请求不超过2
- 给102这个参数设置例外:每1s的请求量不超过4
- 给103这个参数设置例外:每1s的请求量不超过10
@SentinelResource
@GetMapping("{orderId}")
public Order queryOrderByUserId(@PathVariable("orderId") Long orderId) {
// 根据id查询订单并返回
return orderService.queryOrderById(orderId);
}
上面的限流是为了预防服务故障的发生,而这一节介绍的内容是为了处理服务器已经出现故障而避免引起服务器雪崩问题
springcloud中,微服务调用都是通过Feign来实现的,因此做客户端保护必须整合Feign和Sentinel
feign:
sentinel:
enabled:true #开启feign对于sentinal的支持
@Slf4j
public class UserclientFallbackFactory implements FallbackFactory<UserClient>{
@Override
pulbic UserClient create(Throwable throwable){
//创建UserClient接口实现类,实现其中的方法,编写失败降级的处理逻辑
return new UserClient(){
@Override
public User findById(Long id){
//记录异常信息
log.error("查询用户失败",throwable);
return new User();
}
};
}
}
步骤二:在feign-api项目中的DefautFeignConfiguration类中将UserCllientFallbackFactory注册为一个bean
@Bean
public UserClientFallbackFactory userclientFallback(){
return new UserClientFallbackFactory();
}
步骤三:在feign-api中的userclient接口中使用UserClientFallbackFactory
@FeignClient(value="userservice", fallbackFactory=UserClientFallbackFactory.class)
pulbic interface Userclient{
@GetMapping("/user/{id}")
User findByid(@PathVariable("id") Long id);
}
线程隔离有两种实现方式:
当用户现场请求服务的时候,会给请求业务所依赖的每个服务都创建一个线程池,然后请求会调用线程池的线程取实现Feign的远程调用(没有使用原始的请求线程)
优点
- 支持主动超时和异步调用
缺点
- 线程的额外开销大
场景
- 适合与低扇出
此种方式不会创建新现场,而是使用原来的线程调用Feign远程调用,但它会维护一个信号量,没有一个请求过来,计数器减一,知道信号量为0不再接收新请求
优点
- 轻量级,无额外开销
缺点
- 不支持主动超时,不支持异步调用
场景
- 高频调用,高扇出
案例:给Userclient的查询用户接口设置流控规则,现场数不超过2。然后利用jmeter测试
由于我们做了fallback所以所有访问都会通过,但是会返回空User
除了线程隔离,熔断降级是解决雪崩问题的重要手段,其思路是有断路器统计服务调用的异常比例、慢请求比例,如果超出阈值则会熔断该服务。即拦截该服务的一切请求;当服务恢复时,断路器会放行访问该服务的请求。断路器有三种状态:
@GetMapping("/{id}")
public User queryById(@PathVariable("id") Long id) throws InterruptedException {
if (id==1) {
Thread.sleep(60);
}
return userService.queryById(id);
}
授权规则可以对调用方的来源做控制,有白名单和黑名单两种(对请求者身份进行判读)
Sentinel是通过RequestOriginParser这个接口的parseOriging来获取请求的来源的(但是默认sentinel该接口都会返回Default不管是网管来的请求还是浏览器来的请求,所以我们需要自己实现这个接口)
public interface RequestOriginParser{
//从请求Request对象中获取Origin的值,获取方式自定义
String parseOrigin(HttpServletRequest request);
}
@Component
public class HeaderOriginParser implements RequestOriginParser {
@Override
public String parseOrigin(HttpServletRequest httpServletRequest) {
//尝试获取请求头
String origin = httpServletRequest.getHeader("origin");
//非空判断
if (StringUtils.isEmpty(origin)) {
origin="blank";
}
return origin;
}
}
server:
port: 10010 #网关端口
spring:
application:
name: gateway #服务名称
cloud:
nacos:
server-addr: localhost:8845
gateway:
routes: #网关配置路由
- id: user-service #路由定义,自定义,只要唯一即可
uri: lb://userservice #路由的目标地址,lb就是负载均衡,后面跟服务的名称
predicates: #路由断言,也就是判断请求是否符合路由规则的条件
- Path=/user/** #这个按照路径匹配,只要以/user/开头的就符合要求
- id: order-service #路由定义,自定义,只要唯一即可
uri: lb://orderservice #路由的目标地址,lb就是负载均衡,后面跟服务的名称
predicates: #路由断言,也就是判断请求是否符合路由规则的条件
- Path=/order/** #这个按照路径匹配,只要以/user/开头的就符合要求
filters: #过滤器
- AddRequestHeader=Truth, Itcast is freaking awesome! #添加请求头
- AddRequestHeader=origin,gateway! #添加请求头
绕过网关,直接浏览器访问
网关访问
默认情况下,发生限流、降级、授权拦截时,都会抛出异常,如上面浏览器直接访问时抛出的异常结果。(所有的异常信息都是Blocked by Sentinel (flow limiting)
)。如果要自定义异常时的返回结果,需要实现BlockExceptionHandler接口
public interface BlockExceptionHandler{
//处理请求被限流、降级、授权拦截时抛出的异常:BlockException
void handler(HttpServletRequest request,HttpServletResponse response,BlockException e) throws Exception;
}
而BlockException包含很多子类,分别对应不同的场景
异常 | 说明 |
---|---|
FlowException | 限流异常 |
ParamFlowException | 热点参数限流的异常 |
DegradeException | 降级异常 |
AuthorityException | 授权规则异常 |
SystemBlockException | 系统规则异常 |
BlockExceptionHandler
@Component
public class MyBlockExceptionHandler implements BlockExceptionHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, 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;
}
httpServletResponse.setContentType("application/json;charset=utf-8");
httpServletResponse.setStatus(status);
httpServletResponse.getWriter().println("{\"msg\":"+msg+",\"status\""+status+"}");
}
}
不知道有没有发生,只要我们在idea中重启我们的微服务,之前在Sentinel中配置的各种规则就会消失,这就引出了规则持久化的问题(规则在内存里面)
Sentinel的控制台规则管理有三种模式:
修改OrderService
<dependency>
<groupId>com.alibaba.cspgroupId>
<artifactId>sentinel-datasource-nacosartifactId>
dependency>
cloud:
sentinel:
transport:
dashboard: localhost:8080
web-context-unify: false
datasource:
flow:
nacos:
server-addr: localhost:8845
data-id: orderservice-flow-rules
group-id: SENTINEL_GROUP
rule-type: flow
修改Sentinel-dashboard源码
4. 在sentinel-dashboard源码的pom文件中,nacos的依赖默认的scope是test,只能在测试时使用,这里要去除:
<dependency>
<groupId>com.alibaba.cspgroupId>
<artifactId>sentinel-datasource-nacosartifactId>
dependency>
5. 在sentinel-dashboard的test包下,已经编写了对nacos的支持,我们需要将其拷贝到main下
6. 然后,还需要修改测试代码中的NacosConfig类
7. 另外,还需要修改com.alibaba.csp.sentinel.dashboard.controller.v2包下的FlowControllerV2类: