**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次就被限流了
首先我们创建一个新的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这个方法来进行服务降级
但是会发现一个问题,当我们重启服务时,我这边的服务是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的内容,格式为jsonresource是资源名limitApp是来源应用grade是阈值类型,0是线程,1是QPScount是阈值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的优先级。
首先我们要在父工程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=truemain方法
@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; }