之前我们对微服务进行监控,速率控制,服务熔断,服务降级等等都是通过Hystrix来进行的,而Hystrix需要我们程序员自己手工搭建监控平台没有一套Web界面来给我们进行更加细粒度化的配置流控速率控制,服务熔断,服务降级。。。。。现在Sentinel的出现解决了这些问题,它提供了直接界面化的细粒度统一配置。
到他的官网github上下载,https://github.com/alibaba/Sentinel/releases/tag/1.7.0,找到那个jar包,下载到本地,然后在命令行中输入java -jar sentinel-dashboard-1.7.0.jar,运行
然后再浏览器中输入http://localhost:8080
看到这个说明安装运行成功。输入用户名密码(sentinel,sentinel)即可登录
首先我们新建一个Module,新开一个微服务,并且把这个微服务注册到Nacos中,然后用Sentinel监控他。
在pom中必须导入这三个依赖
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cspgroupId>
<artifactId>sentinel-datasource-nacosartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-sentinelartifactId>
dependency>
新建yml
server:
port: 8401
spring:
application:
name: cloudalibaba-sentinel-service
cloud:
nacos:
discovery:
server-addr: localhost:8848 #Nacos服务注册中心地址
sentinel:
transport:
dashboard: localhost:8080 #配置Sentinel dashboard地址
port: 8719
datasource:
ds1:
nacos:
server-addr: localhost:8848
dataId: ${spring.application.name}
groupId: DEFAULT_GROUP
data-type: json
rule-type: flow
management:
endpoints:
web:
exposure:
include: '*'
这个微服务的端口号是8401,然后我要把他注册进Nacos中,所以要配一个nacos,然后将它用Sentinel进行监控,所以要配一个sentinel的配置。最后那段management是暴露监控端点的。
然后新建主启动类
@EnableDiscoveryClient
@SpringBootApplication
public class CloudalibabaSentinelService8401Application {
public static void main(String[] args) {
SpringApplication.run(CloudalibabaSentinelService8401Application.class, args);
System.out.println("启动成功");
}
}
controller
@RestController
@Slf4j
public class FlowLimitController {
@GetMapping("/testA")
public String testA() {
return "------testA";
}
@GetMapping("/testB")
public String testB() {
return "------testB";
}
}
然后启动这个微服务,启动之后登录Nacos查看服务管理能看到这个服务已经被注册到Nacos中
然后看看他能不能被Sentinel监控到登录Sentinel
发现空空如也,这是因为Sentinel采用了懒加载,你访问一下接口他就会出来
然后我们就能在Web界面上看到这个接口的监控效果。
这个流控模式有三种
1.直接:api达到限流条件直接限流
2.关联:当关联的阈值达到阈值时就限流自己
3.链路:只记录指定链路上的流量(指定资源从入口资源进来的流量,如果达到阈值,就进行限流)。
资源名就是那个接口的地址,QPS是每秒点击的次数,我配的是1说明我每秒钟最多访问1次,超过了这个阈值就会被限流(如下图所示)。
我们在流控配置界面里选择的阈值类型改成线程数,然后阈值改为1,当调用api的线程数达到阈值的时候,进行限流。为了测试我们可以在那个接口中加一些延迟。
@GetMapping("/testA")
public String testA() {
// 测试阈值类型:线程数
try {
TimeUnit.MILLISECONDS.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "------testA";
}
我们先点一下这个接口然后他会阻塞,在这个过程中我们另起一个访问该接口这就意味着有两个线程同时访问了这个接口,我们配置的阈值数是1现在有两个线程所以就是有一个能访问一个被限流。
首先一点何为关联当流控达到阈值的时候就限流自己, 这个就相当于连坐,比如说你和你同桌打架,明明这事儿不赖你,其实是赖他,但是呢老师在认定责任的时候会认定为这是一起互殴的“刑事案件”,你也得跟着挨说,就相当于B惹事A挂了。这个主要应用在这样一种情形,有一个支付接口它挂了,下订单接口跟着一块挂。
然后我们在sentinel的web界面上配置一下关联模式的流控。
这个说明当B资源每秒钟点击数大于1A就会被限流。一会我们密集访问B,最后你会发现A居然挂了。我们用postman来模拟这个过程,
首先新建一个collection输上url,然后配置一下具体几个线程多长时间访问一次。
这里我们设置成20个线程每隔0.3秒访问一次。然后我们看一下结果
B满了A就挂了。
Warm up方式,即预热/冷启动方式。当系统长期处于低水位的情况下,当流量突然增加时,直接把系统拉升到高水位可能把整个系统压垮。通过冷启动,让通过的流量缓慢增加,在一定时间内增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮。来看一个例子
Warm up有一个默认的coldFactor为3,即请求QPS从(threshold/3)开始,这个案例就是阈值为10预热时长设置为5s,系统初始化的阈值就是3,等5s后阈值慢慢恢复为10。也就是说你一开始1s之内你连点3次或3次以上他就限流等5s之后他的阈值恢复为10,1s之内连点3次就没问题了。
排队即让请求以均匀的速度通过,阈值类型必须设成QPS,否则无效。来看下面的一个例子。
这个的意思就是/testB每秒1次请求,超过的话就必须排队等待,等待的时间为20000毫秒。这个实际意义就是请求太多别让他一次性打过来,匀速来隔一段时间来一个避免太忙或者太闲。我们继续用之前的postman做多线程测试,10个线程每个1s请求一次。
@GetMapping("/testB")
public String testB() {
log.info(Thread.currentThread().getName() + "\t" + "...testB");
return "------testB";
}
我们把线程访问的日志打出来。
能看到他是1s一个。
Sentinel熔断降级会在调用链路中某个资源出现不稳定状态时(例如调用超时或异常比例升高),对这个资源的调用进行限制,让请求快速失败,避免影响到其他资源而导致级联错误。当资源被降级后,在接下来的降级时间窗口内,对该资源的调用都自动熔断(默认行为是抛出DegradeException)Sentinel熔断器没有半开状态。(系统自动去检测是否有请求异常,没有异常就关闭断路器恢复使用,有异常则继续打开断路器不可用)。
平均响应时间(RT)当1s内持续进入5个请求,对应的时刻的平均响应时间均超过阈值,那么在接下来得时间窗口之内对这个方法的调用都会自动地熔断。为了测试这个降级规则我们要在controller里面加上一个testD。
@GetMapping("/testD")
public String testD() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("testD 测试RT");
return "------testD";
}
首先我们在这个接口上配上降级规则
这个的意思是每个请求给你200毫秒的时间处理,如果这个时间段内处理不完在1s之内会跳闸断电保护我这个系统。
然后我们用JMeter(之前写过略过)测一下我们1s内发送10个请求大于他要求的5了,然后我们要求的时间是200毫秒内处理完,可是我们的接口D中阻塞时间是1s一定处理不完所以在1s内我要让微服务不可用也就是跳闸断电。
停掉JMeter之后一段时间之后再访问就会恢复正常。
异常比例:当资源的每秒请求量>=5,并且每秒异常总数占通过量的比值超过阈值之后,资源进入降级状态,即在接下的时间窗口之内对这个方法的调用都会自动的返回,异常比率的阈值范围是[0.0,1.0],代表0%~100%。
将之前的testD接口方法的阻塞1s改成异常。
@GetMapping("/testD")
public String testD() {
log.info("testD 异常比例");
int age = 10/0;
return "------testD";
}
这个的意思就是正确的比例必须在0.8以上不然的话会熔断。然后我们还是通过JMeter访问testD接口进行测试testD接口一定会抛异常,那这样的话异常率就是%100所以这个testD会被降级。
当我停掉JMeter时我再次访问这次我只发一个请求小于所要求的5个请求所以不会走熔断,会直接报异常。
当资源近1分钟的异常数目超过阈值之后进行熔断,(这次的统计窗口一定是分钟级别的因为如果小于60s,结束熔断后会再次熔断)。
我们配置这个降级规则就是当异常数超过5他才会降级,如果小于5会直接抛异常,降级之后的70s后再次访问会重新报错。为了演示这个现象我们新增一个接口testE这个会抛出异常。
@GetMapping("/testE")
public String testE() {
log.info("testE 测试异常数");
int age = 10 / 0;
return "------testE 测试异常数";
}
我们连点5次。。。服务降级。
等一会(过了时间窗口)再次访问。
这个就是对于经常访问的数据为了不让他崩,很多时候我们希望统计某个热点数据中访问频次最高的TopK数据,并对其访问进行限制,比如说某一个商品ID经常被访问到你就要对这个进行限流。热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源进行限流。 限流之后的降级需要一个兜底方法,这个兜底方法Sentinel默认的都是返回一个字符串Blocked by Sentinel (flow limiting)其实这个兜底方法也可以我们自定义类似于hystrix,某个方法出问题了就找对应的兜底方法,hystrix有@HystrixCommand,Sentinel有@SentinelResource通过这个注解我们可以指定方法让他发生降级的时候用指定的方法处理。
@GetMapping("/testHotKey")
@SentinelResource(value = "testHotKey", blockHandler = "deal_testHotKey")
public String testHotKey(@RequestParam(value = "p1", required = false) String p1,
@RequestParam(value = "p2", required = false) String p2) {
return "------testHotKey";
}
public String deal_testHotKey(String p1, String p2, BlockException exception) {
//sentinel系统默认的提示:Blocked by Sentinel (flow limiting)
return "------deal_testHotKey,o(╥﹏╥)o";
}
@SentinelResoure这个注解value你写啥都行只要他唯一就行,blockHandler里面写上兜底方法的方法名,然后当发生降级的时候就会调用这个兜底方法。
这个是配置规则资源名的意思就是@SentinelResource这个注解的value值,然后限流模式它只支持QPS,参数索引你一会访问的时候你可以选择两个参数(根据之前的testHotKey方法),按顺序下标即索引,这里配置成0说明,0这个是热点key不是别的,只有0号参数可能被限流,单机阈值就是当1s超过访问1次就限流。超过统计窗口时长后恢复正常。
假如说我把访问参数换了换成p2那么他永远不会限流
热点key限流控制的特例,有些时候你既想限流又不想限流 之前演示的p1限流每秒达到访问阈值时就会被限流,但是现在我想让他在p1等于一个特定值得时候他的阈值可以达到很高。我们需要在配置当中添加参数例外项。
我们添加参数例外项标记好类型,然后当这个参数为5时,1s内不超过200次访问就不会降级。
Sentinel还可以做系统级别的规则,就是在各种服务接口的基础上再包上一层,如果达到最外层的阈值那么整个系统(服务)都会被停用。
maxQps * minRt
计算得出。设定参考值一般是 CPU cores * 2.5
。之前已经介绍过关于这个注解的使用,这个注解比较常用的两个参数一个是value就是资源名用于在界面配置,另一个是blockHandler,就是兜底方法,如果你配置了兜底方法他就会使用兜底方法,如果没有配置就会返回系统默认的Blocked By Sentinel(flow limit)。
业务代码与兜底方法都写在一起严重耦合,业务类也会逐渐膨胀。
我们需要让业务代码和兜底方法解耦合防止业务逻辑膨胀,所以我们需要在我们的controller包的同级包创建一个myhandler,写上我们自己的CustomerBlockHandler。同时我们在controller这增加一个测试的方法
public class CustomerBlockHandler {
public static CommonResult handlerException(BlockException exception) {
return new CommonResult(4444, "按客戶自定义,global handlerException----1");
}
public static CommonResult handlerException2(BlockException exception) {
return new CommonResult(4444, "按客戶自定义,global handlerException----2");
}
}
@GetMapping("/rateLimit/customerBlockHandler")
@SentinelResource(value = "customerBlockHandler",
blockHandlerClass = CustomerBlockHandler.class,
blockHandler = "handlerException2")
public CommonResult customerBlockHandler() {
return new CommonResult(200, "按客戶自定义", new Payment(2020L, "serial003"));
}
然后我们给customerBlockHandler配置限流规则,在customerBlockHandler()这个方法中上面配置@SentinelResource这个注解标注资源名value,blockHandlerClass表示异常处理的Handler,blockHandler代表的是降级的兜底方法。当触发降级条件的时候@SentinelResource就会起作用,就会找到blockHandlerClass中配置的类,再找到里面的blockHandler所指向的方法。
新增之后我们访问这个接口,狂点
发现果然是找的blockHandlerClass所对应的类和blockHandler所对应的方法。我们可以通过这种方式实现业务方法与兜底方法的解耦。
我们需要一个消费者84,两个生产者9003,9004,把这三个都注册到Nacos服务中心里,通过消费者以负载均衡的方式轮询访问9003,9004。这个配置跟之前nacos的差不多不细说了,看一下业务代码。
9003,9004
@RestController
public class PaymentController {
@Value("${server.port}")
private String serverPort;
public static HashMap<Long, Payment> hashMap = new HashMap<>();
static {
hashMap.put(1L, new Payment(1L, "28a8c1e3bc2742d8848569891fb42181"));
hashMap.put(2L, new Payment(2L, "bba8c1e3bc2742d8848569891ac32182"));
hashMap.put(3L, new Payment(3L, "6ua8c1e3bc2742d8848569891xt92183"));
}
@GetMapping(value = "/paymentSQL/{id}")
public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id) {
Payment payment = hashMap.get(id);
CommonResult<Payment> result = new CommonResult(200, "from mysql,serverPort: " + serverPort, payment);
return result;
}
}
9003,9004都一个样就是输出的端口号不一样,然后咱就是演示一下就不用数据库了,拿那个static代码块里头的东西当数据库就完了。
然后是消费者84端
controller层
@RestController
@Slf4j
public class CircleBreakerController {
public static final String SERVICE_URL = "http://nacos-payment-provider";
@Resource
private RestTemplate restTemplate;
@RequestMapping("/consumer/fallback/{id}")
public CommonResult<Payment> fallback(@PathVariable Long id) {
CommonResult<Payment> result = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/" + id, CommonResult.class, id);
if (id == 4) {
throw new IllegalArgumentException("IllegalArgumentException,非法参数异常....");
} else if (result.getData() == null) {
throw new NullPointerException("NullPointerException,该ID没有对应记录,空指针异常");
}
return result;
}
}
service层
@FeignClient(value = "nacos-payment-provider", fallback = PaymentFallbackService.class)
public interface PaymentService {
@GetMapping(value = "/paymentSQL/{id}")
CommonResult<Payment> paymentSQL(@PathVariable("id") Long id);
}
@Component
public class PaymentFallbackService implements PaymentService {
@Override
public CommonResult<Payment> paymentSQL(Long id) {
return new CommonResult<>(44444, "服务降级返回,---PaymentFallbackService", new Payment(id, "errorSerial"));
}
}
config层
@Configuration
public class ApplicationContextConfig {
@Bean
@LoadBalanced
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
}
然后看看效果能不能实现
一次9003一次9004实现了最初的设想。
然后我们再访问接口4和5它会在页面上返回报错信息比如4这个
但是我们并不希望给用户看到这些,所以我们要实现我们自己的服务熔断的规则,自己实现兜底方法。
fallback管的是java的异常。
@RestController
@Slf4j
public class CircleBreakerController {
public static final String SERVICE_URL = "http://nacos-payment-provider";
@Resource
private RestTemplate restTemplate;
@RequestMapping("/consumer/fallback/{id}")
@SentinelResource(value = "fallback", fallback = "handlerFallback") //fallback只负责业务异常
public CommonResult<Payment> fallback(@PathVariable Long id) {
CommonResult<Payment> result = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/" + id, CommonResult.class, id);
if (id == 4) {
throw new IllegalArgumentException("IllegalArgumentException,非法参数异常....");
} else if (result.getData() == null) {
throw new NullPointerException("NullPointerException,该ID没有对应记录,空指针异常");
}
return result;
}
//本例是fallback
public CommonResult handlerFallback(@PathVariable Long id, Throwable e) {
Payment payment = new Payment(id, "null");
return new CommonResult<>(444, "兜底异常handlerFallback,exception内容 " + e.getMessage(), payment);
}
}
我们再去访问这个接口你会发现舒服多了。。。。
我们再看一下只配置blockHandler的,这个管的是sentinel异常需要在sentinel里面配置。
@RestController
@Slf4j
public class CircleBreakerController {
public static final String SERVICE_URL = "http://nacos-payment-provider";
@Resource
private RestTemplate restTemplate;
@RequestMapping("/consumer/fallback/{id}")
@SentinelResource(value = "fallback",blockHandler = "blockHandler") //blockHandler只负责sentinel控制台配置违规
public CommonResult<Payment> fallback(@PathVariable Long id) {
CommonResult<Payment> result = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/" + id, CommonResult.class, id);
if (id == 4) {
throw new IllegalArgumentException("IllegalArgumentException,非法参数异常....");
} else if (result.getData() == null) {
throw new NullPointerException("NullPointerException,该ID没有对应记录,空指针异常");
}
return result;
}
//本例是blockHandler
public CommonResult blockHandler(@PathVariable Long id, BlockException blockException) {
Payment payment = new Payment(id, "null");
return new CommonResult<>(445, "blockHandler-sentinel限流,无此流水: blockException " + blockException.getMessage(), payment);
}
}
我们新增一个降级规则有一个异常就走兜底方法。
既然handler管的是java异常,blockHandler管的是sentinel配置异常,那么这两个都配置的情况下,到时候该听谁的。
我们在fallback这个资源上配置一个流控规则,如果QPS超过1那就降级。然后我们正常访问5这个接口按理说应该是java业务上的异常果然被fallback捕获到了,然后我们加快速度,让他超过流控的阈值,再看一下。
可以看出超过阈值的时候走的是sentinel相关的降级规则blockHandler这个兜底方法。
我们在@SourceResoure这个注解加上这个配置项里面规定配置的异常,那么这个异常不会走blockHandler的兜底方法。
@SentinelResource(value = "fallback", fallback = "handlerFallback", blockHandler = "blockHandler",
exceptionsToIgnore = {IllegalArgumentException.class})
public CommonResult<Payment> fallback(@PathVariable Long id) {
CommonResult<Payment> result = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/" + id, CommonResult.class, id);
if (id == 4) {
throw new IllegalArgumentException("IllegalArgumentException,非法参数异常....");
} else if (result.getData() == null) {
throw new NullPointerException("NullPointerException,该ID没有对应记录,空指针异常");
}
return result;
}
当我规定exceptionsToIgnore为IllegalArgumentException.class时说明IllegalArgumentException异常不会被兜底方法处理,会直接抛出异常给用户。
我们在消费端的yml文件中激活Sentinel对openFeign的支持
feign:
sentinel:
enabled: true
然后service中加上@FeignClient注解,再加一个处理降级的Service类。
@FeignClient(value = "nacos-payment-provider", fallback = PaymentFallbackService.class)
public interface PaymentService {
@GetMapping(value = "/paymentSQL/{id}")
CommonResult<Payment> paymentSQL(@PathVariable("id") Long id);
}
@Component
public class PaymentFallbackService implements PaymentService {
@Override
public CommonResult<Payment> paymentSQL(Long id) {
return new CommonResult<>(44444, "服务降级返回,---PaymentFallbackService", new Payment(id, "errorSerial"));
}
}
controller中的相应的方法
@Resource
private PaymentService paymentService;
@GetMapping(value = "/consumer/paymentSQL/{id}")
public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id) {
return paymentService.paymentSQL(id);
}
如果没有服务提供者宕机,等一些异常情况他会寻找value服务的方法,如果出现宕机或者其他异常将会走fallback配置的类里面的方法。
比如说我开启消费者84和服务提供者9003然后我访问localhost:84/consumer/paymentSQL/1他就会到nacos-payment-provider服务里找到相应的方法去调用
当我把9003服务提供者宕机之后再看看结果
他会走fallback的类中的降级方法返回给用户一个友好提示,而不是直接报错。这个跟hystrix差不多,hystrix消费侧导入的是hystrix的相关依赖,sentinel消费侧导入的是sentinel的相关依赖。
@RestController
public class RateLimitController {
@GetMapping("/byResource")
@SentinelResource(value = "byResource", blockHandler = "handleException")
public CommonResult byResource() {
return new CommonResult(200, "按资源名称限流测试OK", new Payment(2020L, "serial001"));
}
public CommonResult handleException(BlockException exception) {
return new CommonResult(444, exception.getClass().getCanonicalName() + "\t 服务不可用");
}
}
看这段代码然后在Sentinel界面中配置他的限流规则。
1s钟访问阈值超过1就会执行自定义的handleException方法。
但是当我把服务暂停之后再次查看Sentinel web界面。
发现空空如也,说明一旦我们重启应用,sentinel配置的规则将会消失,生产环境需要将配置规则进行持久化。
我们可以将限流规则持久化进Nacos进行保存,只要刷新8401某个rest地址,sentinel控制台的流控规则就能看到,只要Nacos里面的配置不删除,针对8401上的sentinel上的流控规则持续有效。
我们需要修改8401的pom文件,添加将sentinel一些配置持久化进nacos的依赖
<dependency>
<groupId>com.alibaba.cspgroupId>
<artifactId>sentinel-datasource-nacosartifactId>
dependency>
然后修改8401的yml文件
添加一下红框的部分
然后我们在nacos配置列表中配置sentinel的流控规则,DataID就是服务名称,然后底下的选择json把这段粘上去
然后发布,然后我们重启一下8401,访问一下对应的资源,快点几次,超过他的阈值触发他的限流
然后我们把8401关了再看看sentinel控制台。
再重启多次访问/rateLimit/byUrl,然后再看看。它神奇的回来了
同时仍然能对这个资源进行限流,所以我们可以将限流规则写进nacos这样能实现持久化。