一、Sentinel簡介
Sentinel是阿里開源的面向服務流量治理的框架,官方原文是Sentinel 是面向分散式、多語言異構化服務架構的流量治理元件,主要以流量為切入點,從流量路由、流量控制、流量整形、熔斷降級、系統自適應過載保護、熱點流量防護等多個維度來幫助開發者保障微服務的穩定性。
Sentinel有兩個重要的基本概念:
資源
資源就是需要進行流量管理的事物,可以是服務名也可以是介面地址URL等,如果你想根據某個介面進行限流,那資源就是該介面。
規則
規則就是進行流量管理的規則,比如用哪種限流演算法,是根據QPS限流還是根據執行緒數限流等。
更多可以檢視官方介紹https://sentinelguard.io/zh-cn/docs/introduction.html。
二、需求背景
微服務閘道器一般都會有限流的功能,防止後端服務被瞬時超高流量擊垮,剛好之前開源的ship-gate閘道器支援自定義外掛,那麼就可以透過給ship-gate增加限流功能來學習Sentinel。
完整需求如下:
ship-gate需要支援服務(如訂單服務)維度的限流,限流方式包括根據QPS和執行緒數這兩種,本次改造內容全部在ship-server子模組。
三、編碼實現
ship-server是基於Spring WebFlux實現的,Sentinel提供的有開源框架的適配sentinel-spring-webflux-adapter模組,但是看了下其實現與現有的設計不相容,於是決定用原生的核心庫sentinel-core。
同時ship-gate也需要Sentinel的控制檯功能監控限流情況,所以pom檔案需要引入以下依賴:
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-core</artifactId>
<version>1.8.6</version>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-transport-simple-http</artifactId>
<version>1.8.6</version>
</dependency>
- 配置類增加限流配置
/**
* Created by 2YSP on 2020/12/27
*/
@ConfigurationProperties(prefix = "ship.gate")
public class ServerConfigProperties {
/**
* 負載均衡演算法,預設輪詢
*/
private String loadBalance = LoadBalanceConstants.ROUND;
/**
* 閘道器超時時間,預設3s
*/
private Long timeOutMillis = 3000L;
/**
* 快取重新整理間隔,預設10s
*/
private Long cacheRefreshInterval = 10L;
/**
* 限流方式QPS或THREAD,預設QPS
*/
private String rateLimitType = "QPS";
/**
* 限流數量
*/
private Integer rateLimitCount;
// 省略getter setter方法
}
ShipPluginEnum增加對應列舉
public enum ShipPluginEnum {
/**
* DynamicRoute
*/
DYNAMIC_ROUTE("DynamicRoute", 2, "動態路由外掛"),
/**
* Auth
*/
AUTH("Auth", 1, "鑑權外掛"),
RATE_LIMIT("RateLimit", 0, "限流外掛");
private String name;
private Integer order;
private String desc;
ShipPluginEnum(String name, Integer order, String desc) {
this.name = name;
this.order = order;
this.desc = desc;
}
// 省略getter方法
}
注意: order越小越先執行(與Spring規則一致),限流外掛最先執行所以定義為0
2.新建RateLimitPlugin類,繼承AbstractShipPlugin
/**
* @Author: Ship
* @Description:
* @Date: Created in 2020/12/29
*/
public class RateLimitPlugin extends AbstractShipPlugin {
private final Logger logger = LoggerFactory.getLogger(RateLimitPlugin.class);
private static Map<String, Integer> rateLimitTypeMap = new HashMap<>();
static {
rateLimitTypeMap.put(ServerConstants.LIMIT_BY_QPS, RuleConstant.FLOW_GRADE_QPS);
rateLimitTypeMap.put(ServerConstants.LIMIT_BY_THREAD, RuleConstant.FLOW_GRADE_THREAD);
}
public RateLimitPlugin(ServerConfigProperties properties) {
super(properties);
}
@Override
public Integer order() {
return ShipPluginEnum.RATE_LIMIT.getOrder();
}
@Override
public String name() {
return ShipPluginEnum.RATE_LIMIT.getName();
}
@Override
public Mono<Void> execute(ServerWebExchange exchange, PluginChain pluginChain) {
String appName = pluginChain.getAppName();
initFlowRules(appName);
if (SphO.entry(appName)) {
// 務必保證finally會被執行
try {
/**
* 被保護的業務邏輯
*/
return pluginChain.execute(exchange, pluginChain);
} finally {
SphO.exit();
}
}
throw new ShipException(ShipExceptionEnum.REQUEST_LIMIT_ERROR);
}
private void initFlowRules(String resource) {
Assert.hasText(properties.getRateLimitType(), "config ship.gate.rateLimitType required!");
Assert.notNull(properties.getRateLimitCount(), "config ship.gate.rateLimitCount required!");
List<FlowRule> list = new ArrayList<>();
FlowRule flowRule = new FlowRule();
flowRule.setResource(resource);
flowRule.setGrade(rateLimitTypeMap.get(properties.getRateLimitType()));
flowRule.setCount(properties.getRateLimitCount().doubleValue());
list.add(flowRule);
FlowRuleManager.loadRules(list);
}
}
因為如果請求被限流了,RateLimitPlugin會丟擲異常,為了處理這種異常需要增加全域性異常處理配置。
類似SpringMVC框架,只需要實現WebExceptionHandler介面,然後在配置類註冊對應的bean即可。
ShipExceptionHandler
public class ShipExceptionHandler implements WebExceptionHandler {
private final Logger logger = LoggerFactory.getLogger(ShipExceptionHandler.class);
@Override
public Mono<Void> handle(ServerWebExchange serverWebExchange, Throwable throwable) {
logger.error("ship server exception msg:{}", throwable.getMessage());
if (throwable instanceof ShipException) {
ShipException shipException = (ShipException) throwable;
return ShipResponseUtil.doResponse(serverWebExchange, new ApiResult(shipException.getCode(), shipException.getErrMsg()));
}
String errorMsg = "system error";
if (throwable instanceof IllegalArgumentException) {
errorMsg = throwable.getMessage();
}
return ShipResponseUtil.doResponse(serverWebExchange, new ApiResult(5000, errorMsg));
}
}
配置類WebConfig
/**
* @Author: Ship
* @Description:
* @Date: Created in 2020/12/25
*/
@Configuration
@EnableWebFlux
@EnableConfigurationProperties(ServerConfigProperties.class)
public class WebConfig {
@Bean
public PluginFilter pluginFilter(@Autowired ServerConfigProperties properties) {
return new PluginFilter(properties);
}
/**
* set order -2 to before DefaultErrorWebExceptionHandler(-1) ResponseStatusExceptionHandler(0)
* @return
*/
@Order(-2)
@Bean
public ShipExceptionHandler shipExceptionHandler(){
return new ShipExceptionHandler();
}
}
注意: 這裡踩了一個小坑,開始發現自己寫的WebExceptionHandler不生效,請求錯誤響應還是原來的格式,後來發現需要新增@order註解指定bean注入的優先順序比預設的小。
- 外掛責任鏈註冊外掛RateLimitPlugin
/**
* @Author: Ship
* @Description:
* @Date: Created in 2020/12/25
*/
public class PluginFilter implements WebFilter {
private ServerConfigProperties properties;
public PluginFilter(ServerConfigProperties properties) {
this.properties = properties;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
String appName = parseAppName(exchange);
if (CollectionUtils.isEmpty(ServiceCache.getAllInstances(appName))) {
throw new ShipException(ShipExceptionEnum.SERVICE_NOT_FIND);
}
PluginChain pluginChain = new PluginChain(properties, appName);
pluginChain.addPlugin(new DynamicRoutePlugin(properties));
pluginChain.addPlugin(new AuthPlugin(properties));
pluginChain.addPlugin(new RateLimitPlugin(properties));
return pluginChain.execute(exchange, pluginChain);
}
private String parseAppName(ServerWebExchange exchange) {
RequestPath path = exchange.getRequest().getPath();
String appName = path.value().split("/")[1];
return appName;
}
}
最後,Sentinel控制檯支援JVM引數和sentinel.properties檔案兩種配置方式,方便起見在resource目錄增加sentinel.properties配置檔案,內容如下:
project.name=ship-server
csp.sentinel.dashboard.server=127.0.0.1:8080
至此,程式碼已經寫完了,下面進入測試環節。
四、測試總結
- 啟動nacos
- 啟動ship-admin,ship-gate-example工程
- 在t_plugin表插入限流外掛
INSERT INTO `ship`.`t_plugin`(`id`, `name`, `code`, `description`, `created_time`) VALUES (3, '限流', 'RateLimit', '限流外掛', '2023-04-16 11:06:39');
然後t_app_plugin表增加外掛配置(外掛管理這塊暫時沒有後臺功能後面考慮增加)
透過nacos控制檯檢視例項詳情,發現服務例項的外掛配置已經有了。
- 在application.yml增加限流配置後就可以啟動ship-server工程了
ship:
gate:
load-balance: round
time-out-millis: 3000
cache-refresh-interval: 10
rate-limit-type: QPS
rate-limit-count: 4
這裡配置了QPS限制在4以內,相當於每秒最多4個請求透過。
-
官網下載sentinel-dashboard.jar(下載地址:https://github.com/alibaba/Sentinel/releases),然後
開啟命令列輸入java -Dserver.port=8080 -Dcsp.sentinel.dashboard.server=localhost:8080 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard-1.8.6.jar啟動。 -
遊覽器訪問http://localhost:8080/#/login(預設賬號密碼都是sentinel),就可以看到後臺介面了。
因為ship-server還沒收到請求,所以左側選單欄開始不會顯示配置的projectName,相當於懶載入的原理。 -
使用wrk壓測工具對介面進行壓測,命令如下:
wrk -c 100 -t 20 -d 60s http://localhost:9000/order/user/test
IDEA控制檯日誌如下
第一張圖說明觸發了限流的保護機制,第二張圖說明QPS確實被限制到了4以內,看日誌列印的頻率猜測用的是固定視窗計數器演算法實現的,測試成功。
同時在Sentinel控制檯也能看到監控資料
總結:
Sentinel還具備其他很強大的功能,需要慢慢摸索,Sentinel控制檯有一個把配置推送到註冊中心,然後服務監聽流量規則配置的方案有時間看下怎麼玩,如果覺得這篇文章對您有用希望可以點個贊讓更多人看到?。
參考文件: