Sentinel的GitHub地址:https://github.com/alibaba/Sentinel。
它的作用和Hystrix非常类似,但是使用起来比Hystrix更加方便。Hystrix需要程序员手工搭建监控平台,而且没有一套Web界面实现更细粒度的配置,所以还是有一定局限性的。Sentinel自称是分布式系统的流量防卫兵,它是一个单独的组件,可以独立出来,提供了界面化的细粒度的统一配置。
Sentinel 具有以下特征:
下载地址:https://github.com/alibaba/Sentinel/releases
Sentinel文档:https://spring-cloud-alibaba-group.github.io/github-pages/greenwich/spring-cloud-alibaba.html#_spring_cloud_alibaba_sentinel
Sentinel由前台和后台两部分构成。后台是核心库,能够运行于所有Java运行时环境,同时对Dubbo、Spring Cloud等框架有较好的支持,前台是基于Spring Boot开发的,打包后即可直接运行,不需要额外的Tomcat容器。
在这里下载对应版本后,其实就是一个jar包,使用java -jar命令运行即可,默认使用的是8080端口,浏览器访问http://localhost:8080,输入账户密码即可看到界面,用户名和密码都是sentinel。
这里依旧采用Docker的方式,Docker里的端口是8858,浏览器访问http://192.168.0.123:8858/,输入用户名密码,都是sentinel,即可看到界面了,界面左侧有规则列表。
[root@bogon ~]# docker pull bladex/sentinel-dashboard:1.7.2
Trying to pull repository docker.io/bladex/sentinel-dashboard ...
1.7.2: Pulling from docker.io/bladex/sentinel-dashboard
169185f82c45: Pull complete
4346af5b5a4f: Pull complete
145353319704: Pull complete
a6b160c30643: Pull complete
Digest: sha256:e525dd34128508242f4ad96d96721900eba617d744af7f2164b43c720db0cbe0
Status: Downloaded newer image for docker.io/bladex/sentinel-dashboard:1.7.2
[root@bogon ~]# docker run -d -p 8858:8858 bladex/sentinel-dashboard:1.7.2
d350b3c3eef1d31d62dd0ad672cce34ce1812b3e77ee021a02b7f5e2d5e6235e
[root@bogon ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
d350b3c3eef1 bladex/sentinel-dashboard:1.7.2 "java -Djava.secur..." 5 seconds ago Up 4 seconds 8719/tcp, 0.0.0.0:8858->8858/tcp zen_knuth
新建一个cloudalibaba-sentinel-service8401模块,修改pom.xml,加入sentinel相关的坐标,加入nacos相关的坐标,将这个模块注册进Nacos,并且使用Sentinel监控这个模块。
cloud2020
com.atguigu.springcloud
1.0-SNAPSHOT
4.0.0
cloudalibaba-sentinel-service8401
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-discovery
com.alibaba.csp
sentinel-datasource-nacos
com.alibaba.cloud
spring-cloud-starter-alibaba-sentinel
org.springframework.cloud
spring-cloud-starter-openfeign
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-actuator
org.springframework.boot
spring-boot-devtools
runtime
true
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-test
test
添加application.yml。
server:
port: 8401
spring:
application:
name: cloudalibaba-sentinal-service
cloud:
nacos:
discovery:
# Nacos服务注册中心地址
server-addr: 192.168.0.123:8848
sentinel:
transport:
# 配置Sentinel dashboard地址
# dashboard: 127.0.0.1:8080 # Windows安装Sentinel,Dashboard的地址
dashboard: 192.168.0.123:8858 # Linux里Docker运行Sentinel,Dashboard的地址
# 应用程序与Sentinel进行交互的端口,用于传输数据,如果端口被占用,Sentinel会尝试采用端口+1,直到找到可用端口
port: 8719
eager: true # 取消延迟加载(默认是延迟加载的)
management:
endpoints:
web:
exposure:
include: '*'
添加主启动类(可能会有问题,下面说明),带上@EnableDiscoveryClient注解。
package com.atguigu.springcloud.alibaba;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient
public class SentinelMain8401 {
public static void main(String[] args) {
SpringApplication.run(SentinelMain8401.class, args);
}
}
添加业务类,用于测试。
package com.atguigu.springcloud.alibaba.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class FlowLimitController {
@GetMapping("/testA")
public String testA() {
System.out.println(Thread.currentThread().getName());
// 测试线程数时候,开启
// try {
// Thread.sleep(500);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
return "----testA";
}
@GetMapping("/testB")
public String testB() {
return "----testB";
}
}
把Nacos以单机的方式启动起来,Sentinel的Dashboard也启动起来,再启动cloudalibaba-sentinel-service8401模块。浏览器访问http://192.168.0.123:8858/,默认情况下,Sentinel是懒加载的,必须发送一个请求,才能在“实时监控”中监控到,当然,也可以指定spring.cloud.sentinel.eager=true取消懒加载。
不过这里碰到了坑(这里建议使用Windows安装),Sentinel识别不到cloudalibaba-sentinel-service8401的请求,在“实时监控”里,也没有请求对应的QPS图像,看了下Sentinel的日志,有一大堆如下错误:
2020-06-29 15:51:45.938 ERROR 1 --- [pool-2-thread-1] c.a.c.s.dashboard.metric.MetricFetcher : Failed to fetch metric from (ConnectionException: Connection timed out)
具体原因可以看下:https://github.com/alibaba/Sentinel/issues/1361。当主机有多个网络适配器的时候,Sentinel会识别出错。上面报错信息的ip地址是VMware Network Adapter VMnet8的IP,它的IP和Sentinel不在一个网段,所以在fetch的时候超时了。后来找到一个方法,在主启动类的main()方法里,加一个行语句,告知Sentinel服务的IP地址,再次尝试,Sentinel就可以识别到请求了,看一下效果图。
package com.atguigu.springcloud.alibaba;
import com.alibaba.csp.sentinel.transport.config.TransportConfig;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient
public class SentinelMain8401 {
public static void main(String[] args) {
// Linux下Docker模式运行Sentinel时使用下面这行代码,Windows下运行Sentinel不要使用
System.setProperty(TransportConfig.HEARTBEAT_CLIENT_IP, "192.168.0.105");
// 下面这种方式好像不行
// System.setProperty(TransportConfig.HEARTBEAT_CLIENT_IP, IPUtils.getOutIPV4());
SpringApplication.run(SentinelMain8401.class, args);
}
}
另外,注意一下,如果长时间不访问,实时监控的信息会自动消失,直到下次请求才会继续出现。
访问Sentinel控制台,点击左侧“流控规则”,再点击右上角的“新建流控规则”,可以看到一些选项,官网地址:https://github.com/alibaba/Sentinel/wiki/%E6%B5%81%E9%87%8F%E6%8E%A7%E5%88%B6。
每秒钟最多请求一次,超过1次,返回快速失败(浏览器看到Blocked by Sentinel (flow limiting)信息),通过浏览器频繁请求http://localhost:8401/testA,即可看到效果。
线程数的测试,手工模拟不出来(可能是手速慢了),那就用JMeter吧,记得开启/testA请求的Thread.sleep()方法,否则看不出来效果,用JMeter测试时候也不是完全正确,不知道为什么。
HTTP请求发给http://localhost:8401/testA,1秒钟启动10个线程,添加结果树查看返回结果。
下面的配置,意思是说当访问/testB超过阈值,对/testA进行限流,应用场景:比如下游服务每秒钟只能处理5个请求,此时不希望上游发来更快的请求。
查看效果的话,需要用到JMeter给/testB发送超过阈值的高频请求,我们浏览器访问/testA,发现此时/testA直接返回了失败信息,说明配置的关联快速失败生效了。
为了验证链路规则,我们需要添加一个资源类。
package com.atguigu.springcloud.alibaba.service;
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import org.springframework.stereotype.Service;
import java.util.UUID;
@Service
public class ResourceService {
// 这里说几个注意的点:
// Sentinel Dashboard中,资源名必须和@SentinelResource注解中的value保持一致
// value:指定资源名,blockHandler:指定降级方法(注意降级方法和目标方法的返回值要一致),方法名一致这个就不用多说了
@SentinelResource(value = "getResource", blockHandler = "blockHandler")
public String getResource() {
return UUID.randomUUID().toString();
}
// BlockException一定要带上,否则不进这个方法
public String blockHandler(BlockException e) {
e.printStackTrace();
return "getResource()不可用,请稍后再试";
}
}
假设/testA和/testB请求都会调用getResource()方法,那么这两个请求就是竞争关系,当某个请求(假设是/testA请求)调用频率高的时候,必定会影响另一个请求获取资源,此时,我们就可以对/testA添加链路模式的流控,限制/testA的高频请求,此时/testB请求是不受影响的。修改/testA和/testB,让其调用getResource()方法。
package com.atguigu.springcloud.alibaba.controller;
import com.atguigu.springcloud.alibaba.service.ResourceService;
import com.atguigu.springcloud.entities.CommonResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
public class FlowLimitController {
@Resource
private ResourceService resourceService;
@GetMapping("/testA")
public String testA() {
// 测试线程数时候,开启
// System.out.println(Thread.currentThread().getName());
// try {
// Thread.sleep(500);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// 测试链路模式时候,开启
String resource = resourceService.getResource();
return "----testA----" + resource;
}
@GetMapping("/testB")
public String testB() {
// 测试链路模式时候,开启
String resource = resourceService.getResource();
return "----testB----" + resource;
}
}
在Sentinel Dashboard中加入流控规则(入口资源/testA,别把/漏掉了,我一开始就漏掉了),使用JMeter进行测试。
根据上面的测试说明,发现测试没效果,具体原因参考这里:https://github.com/alibaba/Sentinel/issues/1213。
解决方案:修改Spring Cloud Alibaba版本号为2.1.1.RELEASE,在8401模块application.yml中配置spring.cloud.sentinel.filter.enabled: false关闭自动收敛,添加FilterContextConfig配置类。
package com.atguigu.springcloud.alibaba.config;
import com.alibaba.csp.sentinel.adapter.servlet.CommonFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FilterContextConfig {
/**
* @NOTE 在spring-cloud-alibaba v2.1.1.RELEASE及前,sentinel1.7.0及后,关闭URL PATH聚合需要通过该方式,spring-cloud-alibaba v2.1.1.RELEASE后,可以通过配置关闭:spring.cloud.sentinel.web-context-unify=false
* 手动注入Sentinel的过滤器,关闭Sentinel注入CommonFilter实例,修改配置文件中的 spring.cloud.sentinel.filter.enabled=false
* 入口资源聚合问题:https://github.com/alibaba/Sentinel/issues/1024 或 https://github.com/alibaba/Sentinel/issues/1213
* 入口资源聚合问题解决:https://github.com/alibaba/Sentinel/pull/1111
*/
@Bean
public FilterRegistrationBean sentinelFilterRegistration() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(new CommonFilter());
registration.addUrlPatterns("/*");
// 入口资源关闭聚合
registration.addInitParameter(CommonFilter.WEB_CONTEXT_UNIFY, "false");
registration.setName("sentinelFilter");
registration.setOrder(1);
return registration;
}
}
重新测试,用JMeter给/testA一个高频请求,浏览器也高频访问/testB,通过结果树可以看到,有好多/testA请求的返回值可以看到有好多请求直接走了blockHandler()方法进行返回,也就是触发了服务降级,另外,浏览器端访问/testB的时候,不受影响。
和“关联”流控模式有点相似,我感觉,链路模式比关联模式的粒度更细。
前面都是采用的快速失败的方式,当请求达到阈值后,不再继续处理,直接返回一个失败的结果。
Warm Up的意思是预热,比如某个资源在初期时候,初始化操作比较大量的运算,不能及时响应高并发的请求,那么在启动初期,所接受的阈值就比较低,经过“预热时长”后,初始化完成,便可以接收高并发请求了。
上图所示,每秒最多可以接收100个请求,在初期,阈值为100/3=33,这个3叫coldFactor(冷加载因子,默认值是3)。
使用JMeter,开启12个线程,添加一个“固定定时器”,线程延迟100毫秒,也就是1秒钟1个线程会发出10个请求,12个线程就是120个请求,效果图如下所示,绿色的线在刚开始的时候,呈现出一个预热的效果,所能处理的请求数逐渐提升。
所有请求进行排队,每秒钟能处理的请求数就是阈值,处理不过来的就排队等待,如果请求达到超时时间则退出队伍。
根据效果图可以看出,每秒处理的请求数基本是恒定的,那些拒绝的,也就是到达超时时间,还没有处理的请求。
熔断降级的官网介绍:https://github.com/alibaba/Sentinel/wiki/%E7%86%94%E6%96%AD%E9%99%8D%E7%BA%A7
点击“降级规则”-“新增降级规则”,可以看到,降级策略有:RT(平均响应时间)、异常比例、异常数。
时间窗口:表示触发降级后,持续的时间,超过时间窗口后,关闭降级,并重新统计。
Sentinel的熔断降级会在调用链路中某个资源不稳定(调用超时或异常比例升高)时候触发,对这个资源进行限制,让它快速失败,避免影响其他服务导致级联错误。
当资源被降级后,在接下来的降级时间窗内,对该资源的调用会自动熔断(默认抛出DegradeException)。
添加一个testC用于测试。
@GetMapping("/testC")
public String testC() throws InterruptedException {
Thread.sleep(1000);
return "----testC----";
}
给/testC添加降级规则,当平均响应时间超过500ms,触发降级,降级5s后,关闭降级重新统计。
以每秒钟12个请求发送给/testC查看效果。
修改testC()方法,手动加入一个1/0的异常。
上图表示,当异常比例达到50%的时候,触发降级,窗口期是5秒,5秒后,重新统计。因为我们加入的是1/0,必定每次都会抛异常,使用浏览器频繁请求http://localhost:8401/testC,初期会看到1/0的异常,再之后,就是Blocked by Sentinel (flow limiting)了,表明Sentinel的异常比例起作用了,继续刷新,过了窗口期后,又会抛出1/0的异常,继续刷新,异常比例达到0.5,再次进入降级。
在官网上,有关于异常数的说明:注意由于统计时间窗口是分钟级别的,若timeWindow小于60s,则结束熔断状态后仍可能再进入熔断状态。所以,我们需要把时间窗口设置为大于60的数字。
访问10次http://localhost:8401/testC后,触发降级,再次访问,只能返回Blocked by Sentinel (flow limiting),在这70秒内,再次访问,都会返回降级的结果。
点击“热点规则”-“新增热点规则”,可以看到一些参数,这里只支持QPS模式,根据请求地址里的参数做限流。官网地址:https://github.com/alibaba/Sentinel/wiki/%E7%83%AD%E7%82%B9%E5%8F%82%E6%95%B0%E9%99%90%E6%B5%81。
添加一个testHotKey()方法和降级方法。
@GetMapping("/testHotKey")
// value对应资源名,blockHandler表示服务降级自定义方法(注意降级方法和目标方法的返回值要一致)
@SentinelResource(value = "testHotKey", blockHandler = "dealWith")
// @RequestParam中,required=false表示当前请求可以不带该参数
public String testHotKey(@RequestParam(value = "param1", required = false) String param1, @RequestParam(value = "param2", required = false) String param2) {
return "----testHotKey success----";
}
// 参数列表要和目标方法一样,BlockException一定要带上,否则不进这个方法
public String dealWith(String param1, String param2, BlockException blockException) {
blockException.printStackTrace();
return "----testHotKey fail----";
}
先看一个简单的情况。
当请求http://localhost:8401/testHotKey的时候,只要带了param1参数(param1在controller方法中参数索引是0),就要受到Hystrix的监控,当QPS大于1时,就会触发服务降级。
再来看这个复杂点的。这个的定义就是说,对param1(param1在controller方法中参数索引是0)做QPS=1的限流控制,但是param1=vip是一个例外项,当param1=vip的时候,QPS设置为100。
我们访问http://localhost:8401/testHotKey?param1=vip测试一下,通过刷新浏览器可以发现,不会再走到服务降级方法里了。
注意,Sentinel只处理BlockException,目标请求里,如果出现了其他异常,是不会走服务降级方法的,@SentinelResource还有一个fallback参数,可以用来处理这个情况。
点击“系统规则”-“新增系统规则”,可以看到阈值类型和阈值参数,具体的含义可以去官网看下。官网地址:https://github.com/alibaba/Sentinel/wiki/%E7%B3%BB%E7%BB%9F%E8%87%AA%E9%80%82%E5%BA%94%E9%99%90%E6%B5%81。
系统规则是用来设置全局的规则的,处于全局入口的地位。
@SentinelResource和Hystrix里的@HystrixCommand非常类似。8401模块中添加cloud-api-common坐标。
com.atguigu.springcloud
cloud-api-commons
${project.version}
添加/byResource方法和handleException方法。
@GetMapping("/byResource")
@SentinelResource(value = "byResource", blockHandler = "handleException")
public CommonResult byResource() {
return new CommonResult(200, "按照资源名称限流测试", new Payment(2020L, "serial001"));
}
public CommonResult handleException(BlockException exception) {
return new CommonResult(500, exception.getClass().getCanonicalName() + "\t 服务不可用");
}
新增流控规则,添加@SentinelResource注解value值的内容,对这个resource进行流控。通过浏览器访问,当QPS大于1时,就可以看到直接进入限流方法了。
添加byURL方法,这里没有带blockHandler指定降级方法,会走默认的服务降级方法。
@GetMapping("/byURL")
@SentinelResource(value = "byURL")
public CommonResult byUrl() {
return new CommonResult(200, "按照byURL限流测试", new Payment(2020L, "serial002"));
}
新增流控规则,浏览器访问http://localhost:8401/byURL,当QPS大于1时,返回了系统自带的服务降级响应。注意按照URL和按照Resource的区别,按照URL的时候,不要忘记最前面的/,如果不带/,那么就去查找@SentinelResource注解的value了。
添加一个customerBlockHandler方法,用于处理/customerBlockHandler请求,注意注解上的blockHandlerClass和blockHandler参数,通过这两个参数,指定服务降级需要执行的方法。
@GetMapping("/customerBlockHandler")
@SentinelResource(value = "customerBlockHandler",
blockHandlerClass = CustomerBlockHandler.class, blockHandler = "handlerException2")
public CommonResult customerBlockHandler() {
return new CommonResult(200, "按照客户自定义限流测试", new Payment(2020L, "serial003"));
}
我们还需要加一个CustomerBlockHandler.java用于处理自定义的限流逻辑。这里的方法名以及参数,有一定的要求,否则不生效,参考这里的blockHandler/blockHandlerClass结点的描述。
package com.atguigu.springcloud.alibaba.handler;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.atguigu.springcloud.entities.CommonResult;
import com.atguigu.springcloud.entities.Payment;
public class CustomerBlockHandler {
public static CommonResult handlerException(BlockException exception) {
return new CommonResult(444, "按照客户自定义的Glogal 全局异常处理 ---- 1", new Payment(2020L, "serial003"));
}
public static CommonResult handlerException2(BlockException exception) {
return new CommonResult(444, "按照客户自定义的Glogal 全局异常处理 ---- 2", new Payment(2020L, "serial003"));
}
}
因为我们指定的降级方法是在@SentinelResource注解上的,所以在Sentinel里配置的时候,要以@SentinelResource的value为标准配置资源名,如果以@GetMapping里的URL来配置,不能走到自定义的处理方法里。
添加上流控规则后,访问http://localhost:8401/customerBlockHandler查看效果,确实走到了自定义的方法里。
参考官网介绍:https://github.com/alibaba/Sentinel/wiki/%E6%B3%A8%E8%A7%A3%E6%94%AF%E6%8C%81。
Sentinel监控的服务,都会放到try-catch里,在try里面会检测是否触发服务限流,如果触发,就抛出BlockException异常。
Sentinel有3个核心API:SphU(定义资源)、Tracer(定义统计)、ContextUtil(定义上下文)。
新建cloudalibaba-provider9003、cloudalibaba-provider9004、cloudalibaba-consumer-nacos-order84模块。这三个模块都会注册进Nacos,9003和9004作为服务提供者,消费者整合Ribbon实现负载均衡,84作为消费者,整合Feign实现服务接口调用。
cloudalibaba-provider9003、cloudalibaba-provider9004、cloudalibaba-consumer-nacos-order84的pom.xml完全一致,所以这里就只放dependency坐标了。
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-discovery
com.alibaba.cloud
spring-cloud-starter-alibaba-sentinel
org.springframework.cloud
spring-cloud-starter-openfeign
com.atguigu.springcloud
cloud-api-commons
${project.version}
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-actuator
org.springframework.boot
spring-boot-devtools
runtime
true
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-test
test
cloudalibaba-provider9003和cloudalibaba-provider9004添加application.yml配置文件,注意修改server.port。
server:
port: 9003或9004
spring:
application:
name: nacos-payment-provider
cloud:
nacos:
discovery:
server-addr: 192.168.0.123:8848
management:
endpoints:
web:
exposure:
include: '*'
cloudalibaba-provider9003和cloudalibaba-provider9004添加主启动类,需要注意的是,添加@EnableDiscoveryClient注解用于Nacos的服务发现,其他的正常写即可。添加业务类,这里使用HashMap构造假数据取值。
package com.atguigu.springcloud.alibaba.controller;
import com.atguigu.springcloud.entities.CommonResult;
import com.atguigu.springcloud.entities.Payment;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
@RestController
public class PaymentController {
@Value("${server.port}")
private String serverPort;
public static HashMap map = new HashMap<>();
static {
map.put(1L, new Payment(1L, "1111"));
map.put(1L, new Payment(2L, "2222"));
map.put(1L, new Payment(3L, "3333"));
}
@GetMapping(value = "/paymentSQL/{id}")
public CommonResult paymentSQL(@PathVariable("id") Long id) {
Payment payment = map.get(id);
return new CommonResult<>(200, "from mysql,serverPort: " + serverPort, payment);
}
}
cloudalibaba-provider9003和cloudalibaba-provider9004就搭建完了,下面继续搭建cloudalibaba-consumer-nacos84模块,添加application.yml。
server:
port: 84
spring:
application:
name: nacos-order-consumer
cloud:
nacos:
discovery:
server-addr: 192.168.0.123:8848
sentinel:
transport:
dashboard: 192.168.0.123:8858
port: 8719
# 消费者将去访问的微服务名称,这里采用服务名称查找服务(成功注册进nacos的微服务提供者)
server-url:
nacos-user-service: http://nacos-payment-provider
添加主启动类,也要加上@EnableDiscoveryClient注解用于服务发现,因为使用的docker搭建的Sentinel,所以需要额外指定TransportConfig.HEARTBEAT_CLIENT_IP。
package com.atguigu.springcloud.alibaba;
import com.alibaba.csp.sentinel.transport.config.TransportConfig;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient
public class OrderMain84 {
public static void main(String[] args) {
// Linux下Docker模式运行Sentinel时使用下面这行代码,Windows下运行Sentinel不要使用
System.setProperty(TransportConfig.HEARTBEAT_CLIENT_IP, "192.168.0.105");
SpringApplication.run(OrderMain84.class, args);
}
}
添加配置类,向容器中注册一个带有负载均衡的RestTemplate。
package com.atguigu.springcloud.alibaba.config;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class ApplicationContextConfig {
@Bean
@LoadBalanced
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
}
添加业务类CircleBreakerController.java,主要是用来演示服务熔断的,后面会在这个类上做一些修改。
package com.atguigu.springcloud.alibaba.controller;
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.atguigu.springcloud.entities.CommonResult;
import com.atguigu.springcloud.entities.Payment;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import javax.annotation.Resource;
@RestController
public class CircleBreakerController {
// nacos-payment-provider就是9003和9004微服务的spring.application.name的值
public static final String SERVICE_URL = "http://nacos-payment-provider";
@Resource
private RestTemplate restTemplate;
@RequestMapping("/consumer/fallback/{id}")
@SentinelResource(value = "fallback")
public CommonResult fallback(@PathVariable Long id) {
CommonResult result = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/" + id, CommonResult.class, id);
if (id == 4) {
throw new IllegalArgumentException("IllegalArgument,非法参数异常...");
} else if (result.getData() == null) {
throw new NullPointerException("NullPointerException,该ID没有对应记录,空指针异常");
}
return result;
}
}
通过浏览器访问http://localhost:84/consumer/fallback/1,多次刷新,观察响应信息可以判断负载均衡是否生效,查看Nacos注册中心,Sentinel监控中心,确保当前3个模块的环境搭建是成功的。
访问http://localhost:84/consumer/fallback/4和http://localhost:84/consumer/fallback/5都会抛出我们自定义的异常信息。下面我们对这块进行改造,当程序抛出异常时候,走SentinelResource指定自定义的fallback方法。
将@SentinelResource(value = "fallback")换成@SentinelResource(value = "fallback", fallback = "handlerFallback"),并添加handlerFallback()方法,再次访问刚才报错的两个地址,这次走了自定义的fallback方法。
public CommonResult handlerFallback(@PathVariable Long id, Throwable throwable) {
Payment payment = new Payment(id, null);
return new CommonResult(444, "异常fallback方法,异常内容是:" + throwable.getMessage(), payment);
}
将@SentinelResource(value = "fallback", fallback = "handlerFallback")换成@SentinelResource(value = "fallback", blockHandler = "blockHandler"),并添加blockHandler()方法。
public CommonResult blockHandler(@PathVariable Long id, BlockException e) {
Payment payment = new Payment(id, "null");
return new CommonResult(444, "blockHandler-sentinel限流,BlockException:" + e.getMessage(), payment);
}
如果此时不设置降级规则,访问http://localhost:84/consumer/fallback/5直接抛出异常。我们添加一个降级规则(假设添加异常数>2触发降级),再次访问http://localhost:84/consumer/fallback/5,第三次访问的时候,就走到了我们自定义的blockHandler()方法里。
将@SentinelResource(value = "fallback", blockHandler = "blockHandler")换成@SentinelResource(value = "fallback", fallback = "handlerFallback", blockHandler = "blockHandler")。
在流控规则里,加一个fallback资源的直接流控限制,当QPS>1时,触发blockException。通过浏览器访问http://localhost:84/consumer/fallback/5,在低频(QPS<1)的时候,由fallback来接管,在高频(QPS>1)的时候,由blockHandler来接管。
所以,我们得出结论,当fallback和blockHandler都配置后,被限流降级抛出BlockException只会进入blockHandler指定的方法。
exceptionsToIgnore也是@SentinelResource的参数,接收一个Throwable的数组,表示哪些异常不需要走fallback指定的方法。比方说,这里把NullPointException加进去。再次访问http://localhost:84/consumer/fallback/5,就不会进fallback指定的方法了,而是直接将异常信息抛出。
pom.xml添加OpenFeign的坐标。
org.springframework.cloud
spring-cloud-starter-openfeign
修改application.yml,开启Sentinel对Feign的支持,即添加feign.sentinel.enable: true。修改主启动类,添加@EnableFeignClients注解。编写PaymentService.java接口,带上@FeignClient注解。
package com.atguigu.springcloud.alibaba.service;
import com.atguigu.springcloud.entities.CommonResult;
import com.atguigu.springcloud.entities.Payment;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
// 指定fallback表示Feign的服务降级处理类,PaymentFallbackService实现这个接口,在对应的方法里,编写服务降级代码
@FeignClient(value = "nacos-payment-provider", fallback = PaymentFallbackService.class)
public interface PaymentService {
@GetMapping(value = "/paymentSQL/{id}")
CommonResult paymentSQL(@PathVariable("id") Long id);
}
这里还需要PaymentFallbackService.java,用于处理服务调用中的异常处理,别忘了@Component注解。
package com.atguigu.springcloud.alibaba.service;
import com.atguigu.springcloud.entities.CommonResult;
import com.atguigu.springcloud.entities.Payment;
import org.springframework.stereotype.Component;
@Component
public class PaymentFallbackService implements PaymentService {
@Override
public CommonResult paymentSQL(Long id) {
return new CommonResult<>(444, "服务降级返回-PaymentFallbackService", new Payment(id, "errorSerial"));
}
}
在CircleBreakerController中加入一个请求,测试通过Feign发送请求,还要用@Resource加入PaymentService对象。
@Resource
private PaymentService paymentService;
@GetMapping("/consumer/paymentSQL/{id}")
public CommonResult paymentSQL(@PathVariable("id") Long id) {
return paymentService.paymentSQL(id);
}
通过浏览器请求http://localhost:84/consumer/paymentSQL/1可以获取到数据,为了让Feign的服务降级方法起作用,我们关掉9003和9004模块,再次访问,因为Feign默认连接超时为1秒,默认读取资源超时是1秒,2s过后Feign超时,程序自动进入了Feign的自定义fallback方法里面。
Sentinel | Hystrix | resilience4j | |
隔离策略 | 信号量隔离(并发线程数限流) | 线程池隔离/信号量隔离 | 信号量隔离 |
熔断降级策略 | 基于响应时间、异常比率、异常数 | 基于异常比率 | 基于异常比率、响应时间 |
实时统计实现 | 滑动窗口(LeapArray) | 滑动窗口(基于RxJava) | Ring Bit Buffer |
动态规则配置 | 支持多种数据源 | 支持多种数据源 | 有限支持 |
扩展性 | 多个扩展点 | 插件的形式 | 接口的形式 |
基于注解的支持 | 支持 | 支持 | 支持 |
限流 | 基于QPS,支持基于调用关系的限流 | 有限的支持 | Rate Limiter |
流量整形 | 支持预热模式、匀速器模式、预热排队模式 | 不支持 | 简单的Rate Limiter模式 |
系统自适应保护 | 支持 | 不支持 | 不支持 |
控制台 | 提供开箱即用控制台,可配置规则、查看秒级监控、机器发现 | 简单的监控查看 | 不提供控制台,可对接其他监控系统 |
之前的时候,只要重启Sentinel相关的模块,Sentinel里的配置信息都会丢失,因为默认情况下,Sentinel的规则都是保存在内存里的,我们需要结合Nacos把Sentinel里的配置持久化。只要在Nacos里做一些配置,Sentinel的流控规则,就可以持久化了。这里使用cloudalibaba-sentinel-service8401做修改。
pom.xml中添加sentinel-datasource-nacos坐标。
com.alibaba.csp
sentinel-datasource-nacos
在application.yml里添加内容,指定配置文件存储在Nacos的基本信息。
spring:
application:
name: cloudalibaba-sentinal-service
cloud:
sentinel:
datasource:
ds1:
nacos:
server-addr: 192.168.0.123:8848
dataId: ${spring.application.name}
groupId: DEFAULT_GROUP
data-type: json
rule-type: flow
回到Nacos里,点击“配置管理”-“配置列表”-“新建配置”,写上dataId为cloudalibaba-sentinal-service,选择json,填充以下内容后点击发布。启动8401模块,查看Sentinel里的流控配置。
[
{
"resource": "/byURL",
"limitApp": "default",
"grade": 1,
"count": 1,
"strategy": 0,
"controlBehavior": 0,
"clusterMode": false
}
]
# 以下是解释
# resource:资源名称
# limitApp:来源应用
# grade:阈值类型:0-线程数;1-QPS
# count:单机阈值
# strategy:流控模式:0-直接;1-关联;2-链路
# controlBehavior:流控效果:0-快速失败;1-Warm Up;2-排队等待
# clusterMode:是否集群
因为Sentinel是懒加载,所以先请求若干次http://localhost:8401/byURL,再点进去查看流控规则,即可看到从Nacos里读取到的规则了。如果看不到,把Nacos、Sentinel、8401模块都重启一下,我第一次就是配置都是正确的,但是通过8401模块的启动日志来看,报了一个WARN:converter can not convert rules because source is empty,也就是从Nacos里读取到的配置是empty的。非常迷惑,重启Nacos、Sentinel、8401模块后,就可以正常读取到了。
不过,这个方法非常鸡肋,在Nacos里手写配置文件,很容易出错,而且我试了下,在Nacos里,把count的值改掉,Sentinel里并不能感知到,还是按照1来做流控。
这个后序再研究下。