Sentinel 开发随笔

Sentinel 控制台使用

添加流控规则失败

错误描述

Sentinel 控制台部署到云服务器上面,当添加流控规则或者熔断规则时,出现 null 或者 undefine 的错误提示,如下图所示:

错误分析

Sentinel 控制台是需要和微服务应用进行双向通信的,本地的微服务访问接口会注册到 Sentinel 控制台,同时会在本地起一个 HTTP 服务,默认监听端口是 8719,如果端口被占用会自动从 8719 开始依次递增扫描,直至找到未被占用的端口。管理页面如下图所示:

可以看到,Sentinel 控制台显示的 IP 是内网的,云服务器(公网)是无法访问本地服务的,所以就会出错。上面红框中的 IP 和端口,是可以在本地微服务应用中设置的,YML 配置参数如下:

1
2
3
4
5
6
spring:
cloud:
sentinel:
transport:
client-ip: 192.168.63.147
port: 8719

所以,当 Sentinel 控制台和微服务应用部署在同一台服务器上就不会有任何问题,如果二者是分别部署在两台公网服务器上,这里的 client-ip 就需要指定为微服务应用所在服务器的公网 IP 地址。

提示

上面的 spring.cloud.sentinel.transport.port 端口配置会在业务应用对应的机器上启动一个 Http Server,该 Server 会与 Sentinel 控制台互相通信。比如,Sentinel 控制台添加了一个限流规则,会把规则数据 Push 给这个 Http Server 接收,Http Server 再将规则注册到 Sentinel 中。

错误解决

综上所述,需要让 Sentinel 控制台和微服务可以双向通信,那么方法有三个:

  • (1) 如果是在本地运行的微服务应用,那么就直接在本地运行 Sentinel 控制台,将 client-ip 直接配置成 127.0.0.1
  • (2) 如果 Sentinel 控制台部署在云服务器上,那么就将本地的微服务应用也部署到同一台云服务器上运行。
  • (3) 如果 Sentinel 控制台和微服务部署在不同的公有云服务器上,则需要指定 client-ip 为微服务应用所在服务器的公网 IP 地址。

Sentinel 整合主流框架

整合 Gateway 出错

错误描述

为了实现网关限流,使用以下组件版本整合 Sentinel 和 Gateway

组件版本组合一版本组合二说明
Spring Boot3.0.93.2.9
Spring Cloud2022.0.22023.0.1
Spring Cloud Alibaba2022.0.0.02023.0.1.3
Nacos Server2.4.32.4.3

在 Gateway 应用启动后,一旦 Gateway 触发限流就会抛出异常 NoSuchMethodError,如下所示:

1
2
3
4
5
6
7
8
9
10
11
java.lang.NoSuchMethodError: 'org.springframework.web.reactive.function.server.ServerResponse$BodyBuilder org.springframework.web.reactive.function.server.ServerResponse.status(org.springframework.http.HttpStatus)'
at com.alibaba.csp.sentinel.adapter.gateway.sc.callback.DefaultBlockRequestHandler.htmlErrorResponse(DefaultBlockRequestHandler.java:51)
at com.alibaba.csp.sentinel.adapter.gateway.sc.callback.DefaultBlockRequestHandler.handleRequest(DefaultBlockRequestHandler.java:42)
at com.alibaba.csp.sentinel.adapter.gateway.sc.exception.SentinelGatewayBlockExceptionHandler.handleBlockedRequest(SentinelGatewayBlockExceptionHandler.java:64)
at com.alibaba.csp.sentinel.adapter.gateway.sc.exception.SentinelGatewayBlockExceptionHandler.handle(SentinelGatewayBlockExceptionHandler.java:59)
at org.springframework.web.server.handler.ExceptionHandlingWebHandler.lambda$handle$1(ExceptionHandlingWebHandler.java:85)
at reactor.core.publisher.FluxOnErrorResume$ResumeSubscriber.onError(FluxOnErrorResume.java:94)
at reactor.core.publisher.MonoPeekTerminal$MonoTerminalPeekSubscriber.onError(MonoPeekTerminal.java:258)
at reactor.core.publisher.FluxOnErrorResume$ResumeSubscriber.onError(FluxOnErrorResume.java:106)
at reactor.core.publisher.Operators.error(Operators.java:198)
at reactor.core.publisher.MonoError.subscribe(MonoError.java:53)

错误分析

这是因为 DefaultBlockRequestHandler 使用到的 ServerResponse.status(HttpStatus.TOO_MANY_REQUESTS) 在 Spring-Webflux 不同版本中参数类型不一样导致的。在 Spring-Webflux 的 6.x 版本中 ServerResponse.status(HttpStatusCode status) 方法需要传入 HttpStatusCode 参数,而在 DefaultBlockRequestHandler 中传入的是 HttpStatus,所以在 Spring-Webflux 的 6.x 版本中 ServerResponse 没有找到对应的方法。

第一种方案

耐心等待 Spring Cloud Alibaba Sentinel 官方修复对应的 Bug

第二种方案

通过自定义逻辑来处理被限流的请求(即不使用默认的 DefaultBlockRequestHandler)来解决此问题,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.BlockRequestHandler;
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager;
import jakarta.annotation.PostConstruct;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.HashMap;
import java.util.Map;

@Configuration
public class GatewayConfiguration {

@PostConstruct
public void doInit() {
initGatewayBlockHandler();
}

/**
* 自定义触发流控规则(限流)后的处理逻辑
*/
public void initGatewayBlockHandler() {
GatewayCallbackManager.setBlockHandler(new BlockRequestHandler() {
@Override
public Mono<ServerResponse> handleRequest(ServerWebExchange serverWebExchange, Throwable ex) {
Map<String, String> map = new HashMap<>();
map.put("errorCode", String.valueOf(HttpStatus.TOO_MANY_REQUESTS.value()));
map.put("errorMessage", "访问过于频繁,请稍后再试!");

return ServerResponse.status(HttpStatus.TOO_MANY_REQUESTS).contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(map));
}
});
}

}