**Sentinel:**随着微服务的流行,服务和服务之间的稳定性变得越来越重要。
Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。
是用来管理和配置服务的流控,降级,熔断等等的。
Sentinel下载
首先我们自然要下载sentinel
2020年9月25日目前的最新版本是1.8.0
下载地址:https://github.com/alibaba/Sentinel
启动事项
而sentinel启动的默认端口为8080,这和Tomcat的端口重合,所以当端口重合的时候启动就会报错,
我们要修改Sentinel的启动端口
使用下面这段代码来进行启动
java -Dserver.port=8858 -jar sentinel-dashboard-1.8.0.jar
这样就把启动的端口改为8858而不会和Tomcat的8080重合报错
运行界面
cmd运行成功后我们打开浏览器,输入localhost:8858进入到下面页面就是运行成功
账户密码初始都是sentinel
一开始应该是一片空白,我这里是已经运行过的。
那么我们先来配置一个服务端来登录sentinel来进行调试
配置服务端
pom
<dependency><!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
<groupId>com.qwf</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<!--SpringCloud ailibaba nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--SpringCloud ailibaba sentinel-datasource-nacos 后续做持久化用到-->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
<!--SpringCloud ailibaba sentinel -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!--openfeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- SpringBoot整合Web组件+actuator -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--日常通用jar包配置-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>4.6.3</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
这里有些配置后续会用到,我先导入,spring-cloud-starter-openfeign和sentinel-datasource-nacos暂时用不到,后面我再慢慢讲
application.yml
server:
port: 8401
spring:
application:
name: cloudalibaba-sentinel-service
cloud:
nacos:
discovery:
server-addr: localhost:8848
sentinel:
transport:
dashboard: localhost:8858
port: 8719
management:
endpoints:
web:
exposure:
include: "*"
这里的spring.cloud.nacos.discovery.server-addr是链接到nacos的路径
下面的spring.cloud.sentinel.transport.dashboard是链接的sentinel的路径,
port是在应用对应的机器上启动一个 Http Server,该 Server 会与 Sentinel 控制台做交互。比如 Sentinel 控制台添加了1个限流规则,会把规则数据 push 给这个 Http Server 接收,Http Server 再将规则注册到 Sentinel 中。
默认这个server端口是从8719开始,如果被占就+1从8720一直到找到空闲的端口为止。
management.endpoints.web.exposure.include是把包含的节点全部暴露给监控
main方法
@SpringBootApplication
@EnableDiscoveryClient
public class SentinelServiceMain8401 {
public static void main(String[] args) {
SpringApplication.run(SentinelServiceMain8401.class,args);
}
}
下面我们先写两个方法来测试
FlowLimitController
@RestController
@Slf4j
public class FlowLimitController {
@GetMapping(value = "/testA")
public String getTestA(){
log.info(Thread.currentThread().getName()+"\t"+"........textA");
return "************TestA";
}
@GetMapping(value = "/testB")
public String getTestB(){
return "************TestB";
}
简单的配置完成,我们运行查看
运行调试
发现可以直接通过
接下来我们观察Sentinel发生了什么变化
原本空白的界面出现了我们配置的服务,sentinel是服务运行通过之后才会获取服务信息。
我们再来看一下服务监控,点击簇点链路
可以发现我们刚刚点的路径名testA,而上面的context是服务一步步获取的路径。
我们先点击流控
在这里开始解释:
针对来源是特指对调用这个服务的人进行流控,default是指没有
阈值类型QPS是每秒钟请求的次数,线程数是指电脑处理对testA的请求的线程数,
是否集群不用解释
流控模式:
直接:就是直接对testA资源进行流控,
关联:比如说链接testB,单机阈值为1,textB每秒访问量超过1了,textA就不能用了,就像支付系统 要瘫痪了,下订单接口就先不能用,缓解支付系统压力。
链路:我们访问testA不是直接访问,是先访问sentinel_web_servlet_context再访问testA,可是sentinel_web_servlet_context当中还有其他方法,当这个服务的访问其他方法过多比如访问testB服务过多,为了缓解压力就不让testA访问了,把资源都给其他方法。
说了这么多不如来动手看一下
第一种:QPS直接快速失败,阈值为1
在我们连续点击刷新testA后
sentinel发现一秒访问数超过一次,在这一秒内访问都被拒绝
第二种:QPS直接warm up,阈值为3,预热时长为3
预热时长是指一开始点击并不是到了阈值才流控,而是阈值除以预热时长然后均匀上升直到阈值,这样防止平常无人访问突然来了很大量的访问,系统承受不住直接崩溃,一开始不让你达到阈值慢慢放开,可以防止系统崩溃。
第三种::QPS直接排队,阈值为1,超时时间为20秒
如果短时间内大量的请求进来,我就一秒钟处理一个一秒钟处理一个,等待超过20秒的请求就报超时错误。
下面我用postman连续300毫秒请求30次,观察后台的日志消息时间。
可以观察到一秒钟后台信息出现一次,说明后台一秒钟只处理一次信息
第四种::QPS关联快速,阈值为1
我们用postman用200ms调用testB40次,调用期间我们来调用testA。
可以看到testA运行失败已经被限流
第五种::QPS链路快速,阈值为1
此时我们对testB做相同操作
得出和上面相同的结果,但是过程不同,上方是对testB控制,这个是对入口限制。
降级
降级有三种模式
慢调用比例:指在一秒内发生的最小请求数(默认是5)当中的RT(请求响应的最久时间)是否超出设定阈值并且超过了全部请求的设定比例。就会熔断。
比如说如下图
这个图的意思是当我我一秒钟的请求大于等于5个,而且其中有一半以上的请求数的响应时间都大于200毫秒,我就停止访问5秒。
我们修改一下testA方法,使响应时间大于200毫秒
@GetMapping(value = "/testA")
public String getTestA(){
try {
TimeUnit.MILLISECONDS.sleep(800);
}
catch (InterruptedException e){
e.printStackTrace();
}
log.info(Thread.currentThread().getName()+"\t"+"........textA");
return "************TestA";
}
修改之后可以看到运行5次之后TestA开始报错误信息,因为每次响应都超过200毫秒
异常比例就是一秒钟报错的占全部的百分之多少就熔断。
异常数是以分钟计算,超过就熔断。
热点
为了测试热点我们先创建一个getHotKey方法,可以传入p1和p2两个字符串参数而且不用必须要传。
这里热点限流会直接报500,我们使用@SentinelResource来做服务降级
@SentinelResource很像@HystrixCommand,
当发生错误时选择一个方法来作为服务降级,
但是发生的Sentinel配置错误使用的是blockHandler ,不要选错。
方法运行报错使用的是fallback。而使用热点必须配置@SentinelResource。否则无法使用。
这里热点限流明显是sentinel配置错误,使用blockHandler 。
@GetMapping(value = "/hotkey")
@SentinelResource(value = "hotkey",blockHandler = "hotkey_Fallback")
public String getHotKey(@RequestParam(value = "p1",required = false) String p1,
@RequestParam(value = "p2",required = false) String p2){
return "************hotkey";
}
public String hotkey_Fallback(String p1, String p2, BlockException exception){
return "************hotkey_Fallback,o(╥﹏╥)o";
}
热点是只能用QPS模式,参数索引是指你传入的第几个参数,从0开始,比如说getHotKey的p1参数就是0,p2就是1。
阈值和时长就不解释了。
我们设定hotkey的热点,设定p1一秒钟只能调用一次
当我们不传p1时可以快速刷新而hotkey不会限流
而传入p1参数后刷新2次就被限流了
@SentinelResource讲解
首先我们创建一个新的controller来查看@SentinelResource的配置
@GetMapping("/byResource")
@SentinelResource(value = "byResource",blockHandler = "byResource_Fallback")
public ConmonResult byResource(){
return new ConmonResult(200,"按资源名称限流测试",new Payment(2020L,"serial0001"));
}
public ConmonResult byResource_Fallback(BlockException e){
return new ConmonResult(444,e.getClass().getCanonicalName()+"\t"+"服务不可用");
}
@SentinelResource当中的value为sentinel当中看到的资源名,可以随意命名,但为了规范最好和方法名或者路径名相同。
blockHandler 是指当发生限流时,该使用那个服务降级方法。默认的就是sentinel limited
我们可以测试一下发生降级时调用了哪个方法名
发生降级会调用com.alibaba.csp.sentinel.slots.block.flow.FlowException这个方法来进行服务降级
sentinel配置持久化
但是会发现一个问题,当我们重启服务时,我这边的服务是8401
我们再看一下sentinel
可以看到我们之前配置的流控,热点,降级方法都没有了。如果这样每次我们更新都要重新配置一次。那是多么大而且麻烦的工作。
我们可以在application.yml中修改spring,在里面修改成下面这段代码
spring:
application:
name: cloudalibaba-sentinel-service
cloud:
nacos:
discovery:
server-addr: localhost:8848
sentinel:
transport:
dashboard: localhost:8858
port: 8719
datasource:
ds1:
nacos:
server-addr: localhost:8848
dataId: cloudalibaba-sentinel-service
groupId: DEFAULT_GROUP
data-type: json
rule-type: flow
我们链接到nacos,创建下面这个配置cloudalibaba-sentinel-service,名称就是spring.cloud.sentinel.datasource.ds1.nacos.dataId的内容,格式为json
resource是资源名
limitApp是来源应用
grade是阈值类型,0是线程,1是QPS
count是阈值
strategy是流控模式,0是直接,1是关联,2是链接
controlBehavior是流控效果,0是直接,1是warm up,2是排队
clusterMode是集群模式
这样就配置成功每次运行时都会使byGlobalRescource按照QPS快速直接,阈值为1非集群环境下限流,实现了持久化。
全局服务降级方法
但是@ServiceResource每次都要编写一个服务降级方法,既增加了代码量,耦合还更高了。自然可以写一个方法全局调用
我们写一个全局服务降级方法的handler
public class ConsumerBlockHandler {
public static ConmonResult handlerException1(BlockException e){
return new ConmonResult(4444,"handlerException1的fallback");
}
public static ConmonResult handlerException2(BlockException e){
return new ConmonResult(4444,"handlerException2的fallback");
}
}
再在controller中添加一个应用全局配置的方法
@GetMapping("/byGlobalResource")
@SentinelResource(value = "byGlobalResource",blockHandlerClass=
ConsumerBlockHandler.class,blockHandler = "handlerException1")
public ConmonResult byGlobalResource(){
return new ConmonResult(200,"按资源名称限流测试",new Payment(2020L,"serial0001"));
}
}
可以看到调用服务降级方法成功,这样减少了代码量还减少了耦合,看起来舒服了很多。
我们再来修改一下byGlobalResource()方法
@GetMapping("/byGlobalResource")
@SentinelResource(value = "byGlobalResource",blockHandlerClass=
ConsumerBlockHandler.class,blockHandler = "handlerException1",fallback="byGlobalResourceFallback" )
public ConmonResult byGlobalResource(){
Integer a=10/0;
return new ConmonResult(200,"按资源名称限流测试",new Payment(2020L,"serial0001"));
}
public ConmonResult byGlobalResourceFallback(){
return new ConmonResult(445,"运行错误,调用byGlobalResourceFallback",new Payment(2020L,"error"));
}
当blockHandler 和fallback同时存在时,会调用哪一个呢
我们设置了一个除0错误。运行时出现
发现了是调用fallback当中的服务降级方法,但是如果我们也设置限流然后多运行几次会发生什么呢?
发现一开始几次是fallback错误,后面限流开始调用blockHandler 错误。由此我们也明白了sentinel的优先级。
sentinel和openfeign结合
首先我们要在父工程pom中的依赖最上方添加
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-openfeign-dependencies</artifactId>
<version>2.2.0.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
用最新的openfeign会报错
看了一下代码,问题的表现是从Sentinel抛出,本质是由于feign核心接口方法命名纠正拼写错误导致
Hoxton.SR7 中,fegin.context接口方法的定义为parseAndValidateMetadata
很明显是为了纠正拼写错误。我们先降低openfeign版本到2.2.0.RELEASE
我们先创建一个服务端
pom
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
<!--SpringCloud ailibaba sentinel -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!--openfeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>com.qwf</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<!--基础配置-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
application.yml
server:
port: 9001
spring:
application:
name: nacos-payment-provider
cloud:
nacos:
discovery:
server-addr: localhost:8848
sentinel:
transport:
dashboard: localhost:8858
port: 8719
management:
endpoints:
web:
exposure:
include: "*"
main
@SpringBootApplication
@EnableDiscoveryClient
public class AlibabaProviderMain9002 {
public static void main(String[] args) {
SpringApplication.run(AlibabaProviderMain9002.class,args);
}
}
controller
@RestController
public class VirtualDatabaseController {
@Value("${server.port}")
private String port;
public static HashMap<Long, Payment> hashMap=new HashMap<>();
static {
hashMap.put(1L,new Payment(1L,"6eb391a3-345f-43e5-a940-d2ca5001e589"));
hashMap.put(2L,new Payment(2L,"63cf224d-79af-41a7-b0c6-3a992da6dd46"));
hashMap.put(3L,new Payment(3L,"f5d8d70b-f15f-47fd-9251-b809939955e5"));
}
@GetMapping("/paymentSQL/{id}")
public ConmonResult<Payment> paymentSQL(@PathVariable(value = "id") Long id){
Payment payment=hashMap.get(id);
ConmonResult<Payment> result=new ConmonResult(200,"from mysql, porr="+port,payment);
return result;
}
}
用户端
pom
<dependencies>
<!--SpringCloud openfeign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--SpringCloud ailibaba nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--SpringCloud ailibaba sentinel -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
<dependency>
<groupId>com.qwf</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<!-- SpringBoot整合Web组件 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--日常通用jar包配置-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
application.yml
server:
port: 84
spring:
application:
name: nacos-order-consumer
cloud:
nacos:
discovery:
server-addr: localhost:8848
sentinel:
transport:
dashboard: localhost:8858
port: 8719
service-url:
nacos-user-service: http://nacos-payment-provider
feign:
sentinel:
enabled: true
相比于服务端多了一个feign.sentinel.enabled=true
main方法
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class SentinelConsumerMain84 {
public static void main(String[] args) {
SpringApplication.run(SentinelConsumerMain84.class,args);
}
}
service接口
@Component("PaymentService")
@FeignClient(value = "nacos-payment-provider",fallback = PaymentServiceImpl.class)
public interface PaymentService
{
@GetMapping(value = "/paymentSQL/{id}")
public ConmonResult<Payment> paymentSQL(@PathVariable("id") Long id);
}
service当中使用了@FeignClient,其中value是链接服务的服务名,fallback是发生服务降级调用方法的类名
PaymentServiceImpl
@Component
public class PaymentServiceImpl implements PaymentService {
@Override
public ConmonResult<Payment> paymentSQL(Long id) {
return new ConmonResult<>(44444,"发生服务降级,--PaymentFallbackService",new Payment(id,"errorSerial"));
}
}
controller
@Resource
private PaymentService paymentService;
@GetMapping("/consumer/openFeign/paymentSQL/{id}")
public ConmonResult<Payment> paymentSQL(@PathVariable(value = "id") Long id){
ConmonResult<Payment> result= paymentService.paymentSQL(id);
if(id==4){
throw new IllegalArgumentException("IllegalArgumentException,非法参数异常");
}
else if(result.getData()==null){
throw new NullPointerException("空指针异常");
}
return result;
}