Sentinel實戰

煙味i發表於2023-04-18

一、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>
  1. 配置類增加限流配置
/**
 * 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注入的優先順序比預設的小

  1. 外掛責任鏈註冊外掛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

至此,程式碼已經寫完了,下面進入測試環節。

四、測試總結

  1. 啟動nacos
  2. 啟動ship-admin,ship-gate-example工程
  3. 在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控制檯檢視例項詳情,發現服務例項的外掛配置已經有了。

image

  1. 在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個請求透過。

  1. 官網下載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啟動。

  2. 遊覽器訪問http://localhost:8080/#/login(預設賬號密碼都是sentinel),就可以看到後臺介面了。
    image
    因為ship-server還沒收到請求,所以左側選單欄開始不會顯示配置的projectName,相當於懶載入的原理。

  3. 使用wrk壓測工具對介面進行壓測,命令如下:

wrk -c 100 -t 20 -d 60s http://localhost:9000/order/user/test

IDEA控制檯日誌如下

ship-server日誌

限流效果

第一張圖說明觸發了限流的保護機制,第二張圖說明QPS確實被限制到了4以內,看日誌列印的頻率猜測用的是固定視窗計數器演算法實現的,測試成功。

同時在Sentinel控制檯也能看到監控資料

監控資料

總結:

Sentinel還具備其他很強大的功能,需要慢慢摸索,Sentinel控制檯有一個把配置推送到註冊中心,然後服務監聽流量規則配置的方案有時間看下怎麼玩,如果覺得這篇文章對您有用希望可以點個贊讓更多人看到?。

參考文件:

https://sentinelguard.io/zh-cn/docs/dashboard.html

https://docs.spring.io/spring-framework/docs/5.1.7.RELEASE/spring-framework-reference/web-reactive.html#webflux-exception-handler

相關文章