• SpringCloudAlibaba笔记06


    介绍

    Spring Cloud Alibaba Sentinel 服务限流/熔断实战

    实验环境准备

    需要提前下载并启动sentinel的dashboard,下载路径及wiki,本次下载的是sentinel-dashboard-1.8.1

    启动服务端

    启动本地nacos(略)
    启动sentinel的dashboard

    java -Dserver.port=8849 -Dcsp.sentinel.dashboard.server=localhost:8849 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard-1.8.1.jar
    

    tips: sentinel 控制台的默认账号和密码都是 “sentinel”

    搭建微服务系统

    这里在上一个dubbo的例子中修改,主要结构如下:

    • dubbo-sample-api: 服务接口定义,供 consumer/provider 引用
    • dubbo-provider-sample : Dubbo 服务端,对外提供一些服务
    • dubbo-consumer-sample: Spring Boot Web 应用,其中的一些 API 会作为 consumer 来调用 dubbo-provider 获取结果。里面一共定义了三个 API path:
      • /demo/hello: 接受一个 name 参数,会 RPC 调用后端的 FooService:sayHello(name) 方法。
      • /demo/time:调用后端的 FooService:getCurrentTime 方法获取当前时间;里面可以通过 slow 请求参数模拟慢调用。
      • /demo/bonjour/{name}: 直接调用本地 DemoService 服务。

    配置sentinel

    添加依赖

    在 dubbo-provider-sample和dubbo-consumer-sample中的build.gradle文件中加入sentinel的依赖包,配置如下:

    dependencies {
        implementation 'com.alibaba.cloud:spring-cloud-starter-alibaba-sentinel'
        implementation 'com.alibaba.csp:sentinel-apache-dubbo-adapter'
    }
    

    修改代码和配置

    修改dubbo-provider-sample和dubbo-consumer-sample中application.properties文件,添加sentinel的配置,如下:

    # Sentinel 控制台地址
    spring.cloud.sentinel.transport.dashboard=127.0.0.1:8849
    # 取消Sentinel控制台懒加载
    # 默认情况下 Sentinel 会在客户端首次调用的时候进行初始化,开始向控制台发送心跳包
    # 配置 sentinel.eager=true 时,取消Sentinel控制台懒加载功能
    spring.cloud.sentinel.eager=true
    

    在dubbo-consumer-sample中,增加sentinel的配置类,以此来处理限流异常

    package com.example.dubboconsumersample.demos.sentinel;
    
    import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.BlockExceptionHandler;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import java.io.PrintWriter;
    
    @Configuration
    public class SentinelWebConfig {
    
    	@Bean
    	public BlockExceptionHandler sentinelBlockExceptionHandler() {
    		return (request, response, e) -> {
    			// 429 Too Many Requests
    			response.setStatus(429);
    
    			PrintWriter out = response.getWriter();
    			out.print("Oops, blocked by Sentinel: " + e.getClass().getSimpleName());
    			out.flush();
    			out.close();
    		};
    	}
    }
    
    

    在dubbo-consumer-sample中,新建DemoService类,增加sentinel的限流配置,增加后代码如下

    package com.example.dubboconsumersample.demos.sentinel;
    
    import com.alibaba.csp.sentinel.annotation.SentinelResource;
    import com.alibaba.csp.sentinel.slots.block.BlockException;
    import org.springframework.stereotype.Service;
    
    @Service
    public class DemoService {
    
    	@SentinelResource(value = "DemoService#bonjour", defaultFallback = "bonjourFallback")
    	public String bonjour(String name) {
    		return "Bonjour, " + name;
    	}
    
    	public String bonjourFallback(Throwable t) {
    		if (BlockException.isBlockException(t)) {
    			return "Blocked by Sentinel: " + t.getClass().getSimpleName();
    		}
    		return "Oops, failed: " + t.getClass().getCanonicalName();
    	}
    }
    
    

    在dubbo-sample-api添加dubbo的接口类,如下

    package com.example.dubbo.api;
    
    /**
     * 该接口为 Dubbo 的服务端、消费端公用的接口定义。
     * 当前案例中,通过复制代码的方式实现接口发布,这不是最优雅的使用方法。更好的建议是通过maven坐标的方式独立维护api。
     */
    public interface FooService {
    
    	String sayHello(String name);
    
    	long getCurrentTime(boolean slow);
    }
    
    

    在dubbo-provider-sample中新建FooServiceImpl类,来实现接口功能

    package com.example.dubboprovidersample.service;
    
    import com.example.dubbo.api.FooService;
    import org.apache.dubbo.config.annotation.DubboService;
    
    /**
     * demo dubbo sentinel
     */
    @DubboService
    public class FooServiceImpl implements FooService {
    	@Override
    	public String sayHello(String name) {
    		return "Hello, " + name;
    	}
    
    	@Override
    	public long getCurrentTime(boolean slow) {
    		// Simulate slow invocations randomly.
    		if (slow) {
    			try {
    				Thread.sleep(100);
    			} catch (InterruptedException ignored) {
    			}
    		}
    		return System.currentTimeMillis();
    	}
    }
    
    

    编译并启动dubbo-provider-sample和dubbo-consumer-sample两个应用

    测试访问接口:

    流控规则

    流量控制配置

    (略)根据文档操作

    熔断降级规则

    熔断降级通常用于自动切断不稳定的服务,防止调用方被拖垮导致级联故障。熔断降级规则通常在调用端,针对弱依赖调用进行配置,在熔断时返回预定义好的 fallback 值,这样可以保证核心链路不被不稳定的旁路影响。

    Sentinel 提供以下几种熔断策略:

    • 慢调用比例 (SLOWREQUESTRATIO):选择以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大的响应时间),请求的响应时间大于该值则统计为慢调用。当单位统计时长(statIntervalMs,默认为 1s)内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断。
    • 异常比例 (ERROR_RATIO):当单位统计时长内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。异常比率的阈值范围是 [0.0, 1.0],代表 0% - 100%。
    • 异常数 (ERROR_COUNT):当单位统计时长内的异常数目超过阈值之后会自动进行熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。

    下面我们来在 Web 应用中针对 Dubbo consumer 来配置慢调用熔断规则,并模拟慢调用来观察效果。我们在 web-api-demo 中针对 com.alibaba.csp.sentinel.demo.dubbo.FooService 服务调用配置熔断降级规则。

    控制台配置的统计时长默认为 1s。在上面的这条规则中,我们设定慢调用临界值为 50ms,响应时间超出 50ms 即记为慢调用。当统计时长内的请求数 >=5 且慢调用的比例超出我们配置的阈值(80%)就会触发熔断,熔断时长为 5s,经过熔断时长后会允许一个请求探测通过,若请求正常则恢复,否则继续熔断。

    我们的实例中 /demo/time API 可以通过 slow 请求参数模拟慢调用,当 slow=true 时该请求耗时会超过 100ms。我们可以用 ab 等压测工具或脚本,批量请求 localhost:8090/demo/time?slow=true,可以观察到熔断的返回

    2021-02-19 10:26:46.415 ERROR 13588 --- [io-62000-exec-6] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.RuntimeException: SentinelBlockException: DegradeException] with root cause
    
    java.lang.RuntimeException: SentinelBlockException: DegradeException
    	at com.alibaba.csp.sentinel.slots.block.BlockException.block(BlockException:0) ~[sentinel-core-1.8.0.jar:1.8.0]
    021
    

    如果我们一直模拟慢调用,我们可以观察到熔断后每 5s 会允许通过一个请求,但该请求仍然是慢调用,会重新打回熔断,无法恢复。我们可以在触发熔断后,等待一段时间后手动发一个不带 slow=true 的正常请求,然后再进行请求,可以观察到熔断恢复。

    需要注意的是,即使服务调用方引入了熔断降级机制,我们还是需要在 HTTP 或 RPC 客户端配置请求超时时间,来做一个兜底的防护。

    注解方式自定义埋点

    刚才我们看到的埋点都是 Sentinel 适配模块提供的自动埋点。有的时候自动埋点可能没法满足我们的需求,我们希望在某个业务逻辑的位置进行限流,能不能做到呢?当然可以!Sentinel 提供两种方式进行自定义埋点:SphU API 和 @SentinelResource 注解,前者最为通用但是代码比较繁杂,耦合度较高;注解方式侵入性较低,但有使用场景的限制。这里我们来动手在 Web 应用的 DemoService 上添加注解,来达到针对本地服务埋点的目标。

    在 DemoService 中我们实现了一个简单的打招呼的服务:

    @Service
    public class DemoService {
    
    	public String bonjour(String name) {
    		return "Bonjour, " + name;
    	}
    
    }
    

    下面我们给 bonjour 这个函数添加 @SentinelResource 注解,注解的 value 代表这个埋点的名称(resourceName),会显示在簇点链路/监控页面。

    
    	@SentinelResource(value = "DemoService#bonjour", defaultFallback = "bonjourFallback")
    	public String bonjour(String name) {
    		return "Bonjour, " + name;
    	}
    

    加上该注解后,再通过网关访问 /demo/bonjour/{name} 这个 API 的时候,我们就可以在簇点链路页面看到我们自定义的 DemoService#bonjour 埋点了。
    添加注解埋点只是第一步。一般在生产环境中,我们希望在这些自定义埋点发生限流的时候,有一些 fallback 逻辑,而不是直接对外抛出异常。这里我们可以写一个 fallback 函数:

    	public String bonjourFallback(Throwable t) {
    		if (BlockException.isBlockException(t)) {
    			return "Blocked by Sentinel: " + t.getClass().getSimpleName();
    		}
    		return "Oops, failed: " + t.getClass().getCanonicalName();
    	}
    

    完整代码如下:

    
    import com.alibaba.csp.sentinel.annotation.SentinelResource;
    import com.alibaba.csp.sentinel.slots.block.BlockException;
    import org.springframework.stereotype.Service;
    
    @Service
    public class DemoService {
    
    	@SentinelResource(value = "DemoService#bonjour", defaultFallback = "bonjourFallback")
    	public String bonjour(String name) {
    		return "Bonjour, " + name;
    	}
    
    	public String bonjourFallback(Throwable t) {
    		if (BlockException.isBlockException(t)) {
    			return "Blocked by Sentinel: " + t.getClass().getSimpleName();
    		}
    		return "Oops, failed: " + t.getClass().getCanonicalName();
    	}
    }
    
    

    这样当我们自定义的 DemoService#bonjour 资源被限流或熔断的时候,请求会走到 fallback 的逻辑中,返回 fallback 结果,而不会直接抛出异常。我们可以配一个 QPS=1 的限流规则,然后快速请求后观察返回值:
    通过页面刷新访问http://127.0.0.1:62000/demo/bonjour/theonefx 返回:

    Blocked by Sentinel: FlowException
    

    注意:使用 @SentinelResource 注解要求对应的类必须由 Spring 托管(即为 Spring bean),并且不能是内部调用(没法走到代理),不能是 private 方法。Sentinel 注解生效依赖 Spring AOP 动态代理机制。

    配置自定义的流控处理逻辑

    Sentinel 的各种适配方式均支持自定义的流控处理逻辑。以 Spring Web 适配为例,我们只需要提供自定义的 BlockExceptionHandler 实现并注册为 bean 即可为 Web 埋点提供自定义处理逻辑。其中 BlockExceptionHandler 的定义如下:

    
    import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.BlockExceptionHandler;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    import java.io.PrintWriter;
    
    @Configuration
    public class SentinelWebConfig {
    
    	@Bean
    	public BlockExceptionHandler sentinelBlockExceptionHandler() {
    		return (request, response, e) -> {
    			// 429 Too Many Requests
    			response.setStatus(429);
    
    			PrintWriter out = response.getWriter();
    			out.print("Oops, blocked by Sentinel WebConfig: " + e.getClass().getSimpleName());
    			out.flush();
    			out.close();
    		};
    	}
    }
    

    在sentinel的链路簇点资源/demo/bonjour/{name}处增加流控规则,测试访问http://127.0.0.1:62000/demo/bonjour/theonefx ,返回结果如下:

    Oops, blocked by Sentinel WebConfig: FlowException
    

    该 handler 会获取流控类型并打印返回信息,返回状态码为 429。我们可以根据实际的业务需求,配置跳转或自定义的返回信息。

    对于注解方式,我们上一节已经提到,可以指定 fallback 函数来处理流控异常和业务异常,这里不再展开讲解;对于 Dubbo 适配,我们可以通过 DubboAdapterGlobalConfig 注册 provider/consumer fallback 来提供自定义的流控处理逻辑;对于 Spring Cloud Gateway 适配,我们可以注册自定义的 BlockRequestHandler 实现类来为网关流控注册自定义的处理逻辑。

    对 Spring Cloud 其他组件的支持
    Spring Cloud Alibaba Sentinel 还提供对 Spring Cloud 其它常用组件的支持,包括 RestTemplate、Feign 等。篇幅所限,我们不展开实践。大家可以参考 Spring Cloud Alibaba 文档来进行接入和配置。

    总结

    通过本教程,我们了解了流控降级作为高可用防护手段的重要性,了解了 Sentinel 的核心特性和原理,并通过动手实践学习了如何快速接入 SCA Sentinel 来为微服务进行流控降级。Sentinel 还有着非常多的高级特性等着大家去发掘,如热点防护、集群流控等,大家可以参考 Sentinel 官方文档来了解更多的特性和场景。

    那么是不是服务的量级很小就不用进行限流防护了呢?是不是微服务的架构比较简单就不用引入熔断保护机制了呢?其实,这与请求的量级、架构的复杂程度无关。很多时候,可能正是一个非常边缘的服务出现故障而导致整体业务受影响,造成巨大损失。我们需要具有面向失败设计的意识,在平时就做好容量规划和强弱依赖的梳理,合理地配置流控降级规则,做好事前防护,而不是在线上出现问题以后再进行补救。

  • 相关阅读:
    Ajax实现文件下载
    jquery easyui 插件开发
    Chrome谷歌浏览器首页被改为Hao123导航怎么办|附各类解决方法【转】
    查看mysql版本的四种方法
    IntelliJ IDEA 快捷键大全
    Java中判断字符串是否为数字的五种方法
    比数据分析更要命的是:数据质量
    Python绘制六种可视化图表详解,三维图最炫酷!你觉得呢?
    大数据需要好设计
    Python模块学习filecmp文件比较
  • 原文地址:https://www.cnblogs.com/GYoungBean/p/14415745.html
Copyright © 2020-2023  润新知