关于服务治理
总体而言,限流和降级作为微服务架构中的重要机制,尽管在实现上可能有多种方式,但它们都着眼于保护服务提供者和消费者,在面对异常情况时确保系统稳定运行。限流关注于保护服务提供者,控制请求流量;而降级则关注于服务消费者,确保在服务不可用或异常情况下提供基本的功能。
限流
是一种针对服务提供者的策略,用于控制对特定服务接口或服务实例的访问量。其目的在于保护服务提供者免受过大请求流量的影响,确保服务稳定性。限流措施可以在服务提供者或服务消费者两端实现,通过设定流量阈值并采取排队、拒绝请求或返回错误信息等方式来控制流量,从而保护服务。降级
是针对服务消费者的应对策略,在服务出现异常或限流时,通过对服务调用进行降级处理,确保消费者端能够在异常情况下正常工作。降级的目的在于转变为弱依赖状态,使系统能够在服务不可用时提供基本的功能或数据。这种策略可以在服务消费者端实施,通过返回默认值、提供备用数据或简化功能等方式来保证系统的可用性。断路器
:一个自动中断,恢复的功能,实际场景中用的不多。
1、Sentinel.dashbod 图形化界面安装
-
下载地址:https://github.com/alibaba/Sentinel/releases
-
启动:下面命令中的端口号可以根据需要修改
java -Dserver.port=9999 -Dcsp.sentinel.dashboard.server=localhost:9999 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard-1.8.7.jar
- 通过 http://localhost:9999 访问图形化界面,密码默认为sentinel。
2、alibaba-sentinel-server
服务端主要是限流配置
pom.xml
在本示例中,主要用到了三个主要组件,nacos服务发现,sentinel,以及nacos配置中心(做为sentinel规则的存储,否则当服务重启后规则就会丢失)
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-sentinel-datasource</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
application.properties配置
这里配置比较多,可参考上面jar包引用说明查看,主要是配置了Nacos和Sentinel控制台的地址。
spring.profiles.active = dev
spring.application.name=AlibabaSpringbootSentinelServer
server.port=19504
management.endpoints.web.exposure.include=*
management.endpoint.health.show-details=always
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
spring.cloud.nacos.discovery.fail-fast=true
spring.cloud.nacos.username=nacos
spring.cloud.nacos.password=nacos
spring.cloud.sentinel.transport.dashboard=localhost:9999
spring.cloud.sentinel.transport.port=8719 #与sentinel通信的转用端口
spring.cloud.sentinel.eager=true
spring.cloud.sentinel.web-context-unify=true
spring.cloud.sentinel.datasource.ds2.nacos.server-addr=127.0.0.1:8848
spring.cloud.sentinel.datasource.ds2.nacos.username=nacos
spring.cloud.sentinel.datasource.ds2.nacos.password=nacos
#需要nocas中配合一个名为 AlibabaSpringbootSentinelServer-sentinel 的文件,这里不需要加-dev啥的,因为这处指定死了名称
spring.cloud.sentinel.datasource.ds2.nacos.dataId=${spring.application.name}-sentinel
spring.cloud.sentinel.datasource.ds2.nacos.groupId=DEFAULT_GROUP
spring.cloud.sentinel.datasource.ds2.nacos.data-type=json
#type=flow 以 JSON 格式返回现有的限流规则,degrade 返回现有生效的降级规则列表,system 则返回系统保护规则。
spring.cloud.sentinel.datasource.ds2.nacos.rule-type=flow
SpringbootApplication启动类
主要是添加了@EnableDiscoveryClient
注解,如果不需要服务发现可以去掉。
@Slf4j
@SpringBootApplication(scanBasePackages = {"com.korgs", "cn.hutool.extra.spring"})
@Configuration
@EnableConfigurationProperties
@ServletComponentScan
@RestController
@EnableDiscoveryClient
public class AlibabaSpringbootSentinelServerApplication {
@Value("${server.port}")
private String serverPort;
public static void main(String[] args) {
SpringApplication.run(AlibabaSpringbootSentinelServerApplication.class, args);
}
@GetMapping("/helloworld/{uuid}")
public BaseResponse<String> helloWorld(@PathVariable String uuid){
String str = LogGenerator.trackLog()
+ " uuid=" + uuid + " I am busy to handle this request."
+ " serverPort=" + serverPort;
log.info( str );
return BaseResponse.success(str);
}
}
Controller测试类实现
给普通的uri上添加@SentinelResource(value = "userInfo", blockHandler = "exceptionHandler")
,这个注解在文章最后会详细解释。
@Slf4j
@RestController
@RequestMapping("/api/user")
public class UserController {
@Value("${server.port}")
private String serverPort;
@SentinelResource(value = "userInfo", blockHandler = "exceptionHandler")
@GetMapping("/userinfo/{uuid}")
public BaseResponse<String> userInfo(@PathVariable String uuid){
String str = LogGenerator.trackLog()
+ " uuid=" + uuid + " I am busy to handle userInfo."
+ " serverPort=" + serverPort;
log.info( str );
return BaseResponse.success(str);
}
@SentinelResource(value = "userOrder")
@GetMapping("/userorderlist/{uuid}")
public ListResponse<List<String>> userOrder(@PathVariable String uuid){
String str = LogGenerator.trackLog()
+ " uuid=" + uuid + " I am busy to handle userInfo."
+ " serverPort=" + serverPort;
log.info( str );
List<String> userOrders = new ArrayList<>();
userOrders.add("order1");
userOrders.add("order2");
userOrders.add("order3");
userOrders.add("order4");
return ListResponse.success(userOrders);
}
@SentinelResource(value = "userStatus")
@GetMapping("/userstatus/{uuid}")
public BaseResponse<String> userStatus(@PathVariable String uuid){
String str = LogGenerator.trackLog()
+ " uuid=" + uuid + " I am busy to handle userStatus."
+ " serverPort=" + serverPort;
return BaseResponse.success(str);
}
public BaseResponse<String> exceptionHandler(long s, BlockException ex) {
ex.printStackTrace();
return BaseResponse.error(s+"");
}
}
3、Sentinel配置限流规则
限流规则配置
- 配置之前要先启动 alibaba-sentinel-server 服务,然后登陆sentinel客户端,就可以发现此服务,然后在流控规则中新增规则即可。
- 配置规则,规则可以注解的value或uri来配置。
比如下面这个配置
@SentinelResource(value = "userInfo", blockHandler = "exceptionHandler")
@GetMapping("/userinfo/{uuid}")
public BaseResponse<String> userInfo(@PathVariable String uuid){
}
资源名可以配置成userInfo,也可以配置成/userinfo/{uuid},一般建议配置成@SentinelResource
注解的值,这样也起到了一个解耦的作用。
Controller限流功能测试
因为上面规则的阈值配置成了1,所以我们快速访问上述的userinfo地址。会发现uri返回时好时坏。在浏览器中打开http://localhost:19504/swagger-ui/index.html,如下:
上述红框就是限流后的返回结果。
4、alibaba-sentinel-client
客户端主要是降级配置,这个示例中添加了ribbon和RestTemplate支持,前者用于负载,后者用于远程访问。
pom.xml
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
</dependencies>
application.properties配置
spring.profiles.active = dev
spring.application.name=AlibabaSpringbootSentinelClient
server.port=19505
management.endpoints.web.exposure.include=*
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
spring.cloud.nacos.discovery.fail-fast=true
spring.cloud.nacos.username=nacos
spring.cloud.nacos.password=nacos
#自定义配置服务端
service-url.nacos-service = http://AlibabaSpringbootSentinelServer
SpringbootApplication启动类
主要是添加了@EnableDiscoveryClient
注解,如果不需要服务发现可以去掉。
@Slf4j
@SpringBootApplication(scanBasePackages = {"com.korgs", "cn.hutool.extra.spring"})
@Configuration
@EnableConfigurationProperties
@ServletComponentScan
@RestController
@EnableDiscoveryClient
public class AlibabaSpringbootSentinelClientApplication {
public static void main(String[] args) {
SpringApplication.run(AlibabaSpringbootSentinelClientApplication.class, args);
}
@GetMapping("/helloworld")
public String helloWorld(){
log.info( LogGenerator.trackLog()
+ "msg=" + "I am busy to handle this request.");
return "hello world";
}
}
配置RestTemplate
添加@SentinelRestTemplate
,配置全局降级响应
@Configuration
public class RibbonConfig {
@Bean
@LoadBalanced
@SentinelRestTemplate
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
Controller测试类实现
配置自定义降级响应,即被服务端限流后的响应
@Slf4j
@RestController
@RequestMapping("/api/load")
public class LoadUserController {
@Autowired
private RestTemplate restTemplate;
@Value("${service-url.nacos-service}")
private String ribbonServiceUrl;
/*熔断,降级,这里主要用到的是fallback标签*/
@GetMapping("/fusing")
@SentinelResource(value = "fusing", fallback = "handleFallback")
public BaseResponse<String> fusing(String uuid){
String result = restTemplate.getForObject(ribbonServiceUrl + "/api/user/userinfo/{1}", String.class, uuid);
return BaseResponse.success(result);
}
public BaseResponse<String> handleFallback(String uuid, Throwable e) {
log.error("handleFallback2 id:{},throwable class:{}", uuid, e.getClass());
return BaseResponse.error("服务降级返回");
}
}
Controller被限流后的降级测试
- 先启动 alibaba-sentinel-server
- 再启动 alibaba-sentinel-client
- 打开 http://localhost:19505/swagger-ui
当快速访问时就会发现,返回结果变成了自定义的描述了。
5、Sentinel详细使用说明
Sentinel注解
@SentinelRestTemplate注解
此注解的属性支持限流(blockHandler, blockHandlerClass)和降级(fallback, fallbackClass)的处理。下面的示例是演示调用服务时时支持的限流。
即在消费者一端实现的限流配置,但目的也是为了保护服务提供者,一般不太常用,还是在提供者端配置为好。在消费端限流(也称降级)是指在服务提供者发生异常后,客户端的的处理逻辑
//配置全局限流异常
@Bean
@SentinelRestTemplate(blockHandler = "handleException", blockHandlerClass = ExceptionUtil.class)
public RestTemplate restTemplate() {
return new RestTemplate();
}
@SentinelRestTemplate注解 中 blockHandler 或 fallback 属性对应的方法必须是对应 blockHandlerClass 或 fallbackClass 属性中的静态方法。
该方法的参数跟返回值跟 org.springframework.http.client.ClientHttpRequestInterceptor#interceptor 方法一致,其中参数多出了一个 BlockException 参数用于获取 Sentinel 捕获的异常。
比如上述 @SentinelRestTemplate 注解中 ExceptionUtil 的 handleException 属性对应的方法声明如下:
public class ExceptionUtil {
public static ClientHttpResponse handleException(HttpRequest request, byte[] body, ClientHttpRequestExecution execution, BlockException exception) {
}
public static SentinelClientHttpResponse handleException(HttpRequest request,
byte[] body, ClientHttpRequestExecution execution, BlockException ex) {
System.out.println("Oops: " + ex.getClass().getCanonicalName());
return new SentinelClientHttpResponse("custom block info");
}
}
当使用 RestTemplate 调用被 Sentinel 熔断后,会返回 RestTemplate request block by sentinel 信息,或者也可以编写对应的方法自行处理返回信息。这里提供了 SentinelClientHttpResponse 用于构造返回信息。
@SentinelResource 注解
public class TestService {
// 原函数
@SentinelResource(value = "hello", blockHandler = "exceptionHandler", fallback = "helloFallback")
public String hello(long s) {
return String.format("Hello at %d", s);
}
// Fallback 函数,函数签名与原函数一致或加一个 Throwable 类型的参数.
public String helloFallback(long s) {
return String.format("Halooooo %d", s);
}
// Block 异常处理函数,参数最后多一个 BlockException,其余与原函数一致.
public String exceptionHandler(long s, BlockException ex) {
ex.printStackTrace();
return "Oops, error occurred at " + s;
}
// 这里单独演示 blockHandlerClass 的配置.
// 对应的 `handleException` 函数需要位于 `ExceptionUtil` 类中,并且必须为 public static 函数.
@SentinelResource(value = "test", blockHandler = "handleException", blockHandlerClass = {ExceptionUtil.class})
public void test() {
System.out.println("Test");
}
}
value
:资源名称,必需项(不能为空)entryType
:entry 类型,可选项(默认为 EntryType.OUT)blockHandler / blockHandlerClass
(限流处理): blockHandler 对应处理 BlockException 的函数名称,可选项。blockHandler 函数访问范围需要是 public,返回类型需要与原方法相匹配,参数类型需要和原方法相匹配并且最后加一个额外的参数,类型为 BlockException。blockHandler 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 blockHandlerClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。fallback / fallbackClass
(异常处理):fallback 函数名称,可选项,用于在抛出异常的时候提供 fallback 处理逻辑。fallback 函数可以针对所有类型的异常(除了 exceptionsToIgnore 里面排除掉的异常类型)进行处理。fallback 函数签名和位置要求:- 返回值类型必须与原函数返回值类型一致;
- 方法参数列表需要和原函数一致,或者可以额外多一个 Throwable 类型的参数用于接收对应的异常。
- fallback 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 fallbackClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。
defaultFallback
(since 1.6.0):默认的 fallback 函数名称,可选项,通常用于通用的 fallback 逻辑(即可以用于很多服务或方法)。默认 fallback 函数可以针对所有类型的异常(除了 exceptionsToIgnore 里面排除掉的异常类型)进行处理。若同时配置了 fallback 和 defaultFallback,则只有 fallback 会生效。defaultFallback 函数签名要求:- 返回值类型必须与原函数返回值类型一致;
- 方法参数列表需要为空,或者可以额外多一个 Throwable 类型的参数用于接收对应的异常。
- defaultFallback 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 fallbackClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。
exceptionsToIgnore
(since 1.6.0):用于指定哪些异常被排除掉,不会计入异常统计中,也不会进入 fallback 逻辑中,而是会原样抛出。
配置全局异常处理
Sentinel内置的 URL 限流触发后默认处理逻辑是,直接返回 “Blocked by Sentinel (flow limiting)”。 如果需要自定义处理逻辑,实现的方式如下:
public class CustomUrlBlockHandler implements UrlBlockHandler {
@Override
public void blocked(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws IOException {
// todo add your logic
}
}
WebCallbackManager.setUrlBlockHandler(new CustomUrlBlockHandler());
Sentinel中配置限流规则
可以sentinel图形化界面中配置限流规则,可以用资源名,也可以添加相对的url路径,比如下面例子,在资源名
对话框中可以配置userInfo 或 /api/user/userinfo
@RestController("/api/user")
public class UserController{
@SentinelResource(value = "userInfo", blockHandler = "exceptionHandler")
@GetMapping("/userinfo")
public BaseResponse<String> userInfo(String uuid){
String str = LogGenerator.trackLog()
+ " uuid=" + uuid + " I am busy to handle userInfo."
+ " serverPort=" + serverPort;
log.info( str );
return BaseResponse.success(str);
}
}
给sentinel提供 ReadableDataSource 存储支持
默认情况下,当我们在Sentinel控制台中配置规则时,控制台推送规则方式是通过API将规则推送至客户端并直接更新到内存中。一旦我们重启应用,规则将消失。
Sentinel starter 整合了目前存在的几类 ReadableDataSource。只需要在配置文件中进行相关配置,即可在 Spring 容器中自动注册 DataSource。 下面我们介绍下如何将配置规则进行持久化,以存储到Nacos为例。
spring.cloud.sentinel.datasource.ds2.nacos.server-addr=127.0.0.1:8848
spring.cloud.sentinel.datasource.ds2.nacos.username=nacos
spring.cloud.sentinel.datasource.ds2.nacos.password=nacos
#需要nocas中配合一个名为 AlibabaSpringbootSentinelServer-sentinel 的文件,这里不需要加-dev啥的,因为这处指定死了名称
spring.cloud.sentinel.datasource.ds2.nacos.dataId=${spring.application.name}-sentinel
spring.cloud.sentinel.datasource.ds2.nacos.groupId=DEFAULT_GROUP
spring.cloud.sentinel.datasource.ds2.nacos.data-type=json
#type=flow 以 JSON 格式返回现有的限流规则,degrade 返回现有生效的降级规则列表,system 则返回系统保护规则。
spring.cloud.sentinel.datasource.ds2.nacos.rule-type=flow
然后在nacos中配置一外名为AlibabaSpringbootSentinelServer-sentinel的文件,这样就会在sentinel中存储了。文件内容如下:
[
{
"resource": "userInfo", //资源名称
"limitApp": "default", //来源应用
"grade": 1, //阈值类型,0表示线程数,1表示QPS
"count": 1, //单机阈值
"strategy": 0, //流控模式,0表示直接,1表示关联,2表示链路
"controlBehavior": 0, //流控效果,0表示快速失败,1表示Warm Up,2表示排队等待
"clusterMode": false //是否集群
}
]
6、与openFeign配合实现降级
- alibaba-sentinel-openFeign-client:19508
此服务和上面的是alibaba-sentinel-client功能一样,只不过把ribbon换成了openFiegn实现,测试方法也完全一样,不详细说了。
但在业务实现中建议使用openFiegn。
源码下载
涉及模块:
- alibaba-sentinel-server:19501,服务端
- alibaba-sentinel-client : 19506, ribbon实现的客户端
- alibaba-sentinel-openFeign-client: 19508,openFeign实现的客户端
源码下载:
- 基础框架源码下载
- Alibaba SpringCloud集成Nacos、Sentinel实现服务治理
源码运行方法:
- 模块详细功能说明和运行测试方法