微服務實戰(八)整合Sentinel閘道器服務限流功能 SpringCloud GateWay + Sentinel + Nacos

gin90發表於2020-02-12

本章主要內容

在SpringCloud GateWay服務閘道器中整合Sentinel來實現服務的限流功能。

首先,我們談一下什麼是服務限流。

在如今網際網路的大環境下,我們後端的介面呼叫頻次(QPS/TPS)動輒上百萬,甚至達到千萬級別,這對於服務端的承受能力是一個巨大的考驗。那麼限流在這個問題中起到什麼作用呢?其實就是對於流量進行有策略的管理和限制。 比如說在一個系統中,有訂單查詢服務,商品查詢服務,積分查詢等,如果一個系統能支援的QPS(每秒查詢次數)是100,那麼這100該如何如何分配就是限流功能所考慮的事情。

根據這些服務的重要性,我們可能會把商品查詢服務的QPS設定的比較高,因為這類使用者查詢到商品是有購買意向的,而訂單查詢、積分查詢啥的管他呢!那麼在一段時間內使用者訪問量過大時,我們就能儘量保證有購買意向的使用者正常使用系統。這就是限流的作用。

什麼是Sentinel

Sentinel 由阿里巴巴研發,主要以流量為切入點,從流量控制熔斷降級系統負載保護等多個維度保護服務的穩定性。

文件:https://github.com/alibaba/Sentinel/wiki/主頁

原始碼:https://github.com/alibaba/Sentinel

其實文件還是非常全面的,我也就不做搬運工了,主要是有目的性地講述一下與SpringCloud GateWay的整合以及初步使用吧!

和SpringCloud GateWay 整合

新增依賴

先在我們之前搭建上的閘道器工程的pom.xml 中新增sentinel的閘道器依賴包

<dependency>
	<groupId>com.alibaba.csp</groupId>
	<artifactId>sentinel-spring-cloud-gateway-adapter</artifactId>
</dependency>

The managed version is 1.6.3 The artifact is managed in com.alibaba.cloud:spring-cloud-alibaba-dependencies:2.1.0.RELEASE

由於我們已經引入了 spring-cloud-alibaba-dependencies ,所以在上面就不用特意新增版本號了,預設就使用了1.6.3,與整體的SpringCloud版本對應。

 

注入Sentinel配置

我們寫一個 @Configuration 配置類(放在Boot的類路徑同級或者子級),裡面的程式碼主要作用是設定需要限流的API (根據路徑),以及限流的規則



import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.annotation.PostConstruct;

import org.springframework.beans.factory.ObjectProvider;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.web.reactive.result.view.ViewResolver;

import com.alibaba.csp.sentinel.adapter.gateway.common.SentinelGatewayConstants;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiDefinition;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiPathPredicateItem;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiPredicateItem;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.GatewayApiDefinitionManager;
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayFlowRule;
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayRuleManager;
import com.alibaba.csp.sentinel.adapter.gateway.sc.SentinelGatewayFilter;
import com.alibaba.csp.sentinel.adapter.gateway.sc.exception.SentinelGatewayBlockExceptionHandler;


@Configuration
public class GatewayConfiguration {

    private final List<ViewResolver> viewResolvers;
    private final ServerCodecConfigurer serverCodecConfigurer;

    public GatewayConfiguration(ObjectProvider<List<ViewResolver>> viewResolversProvider,
                                ServerCodecConfigurer serverCodecConfigurer) {
        this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
        this.serverCodecConfigurer = serverCodecConfigurer;
    }

    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
        // Register the block exception handler for Spring Cloud Gateway.
        return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
    }

    @Bean
    @Order(-1)
    public GlobalFilter sentinelGatewayFilter() {
        return new SentinelGatewayFilter();
    }

    @PostConstruct
    public void doInit() {
        initCustomizedApis();
        initGatewayRules();
    }

    private void initCustomizedApis() {
/*
ApiDefinition:使用者自定義的 API 定義分組,可以看做是一些 URL 匹配的組合。
比如我們可以定義一個 API 叫 my_api,請求 path 模式為 /foo/** 和 /baz/** 的都歸到 my_api 這個 API 分組下面。
限流的時候可以針對這個自定義的 API 分組維度進行限流。
*/
        Set<ApiDefinition> definitions = new HashSet<>();
        ApiDefinition api1 = new ApiDefinition("combat_gateway_api")
            .setPredicateItems(new HashSet<ApiPredicateItem>() {{
                add(new ApiPathPredicateItem().setPattern("/nacos-provider/loadBanlance/print"));
            }});
        
        definitions.add(api1);
        GatewayApiDefinitionManager.loadApiDefinitions(definitions);
    }

    private void initGatewayRules() {
/*
GatewayFlowRule:閘道器限流規則,
針對 API Gateway 的場景定製的限流規則,可以針對不同 route 或自定義的 API 分組進行限流,
支援針對請求中的引數、Header、來源 IP 等進行定製化的限流。
*/
        Set<GatewayFlowRule> rules = new HashSet<>();
       
/*設定限流規則 
count: QPS即每秒鐘允許的呼叫次數
intervalSec: 每隔多少時間統計一次彙總資料,統計時間視窗,單位是秒,預設是 1 秒。
*/
        rules.add(new GatewayFlowRule("combat_gateway_api")
            .setResourceMode(SentinelGatewayConstants.RESOURCE_MODE_CUSTOM_API_NAME)
            .setCount(5)
            .setIntervalSec(1)
            
        );
        GatewayRuleManager.loadRules(rules);
    }
}

其中閘道器限流規則 GatewayFlowRule 的欄位解釋如下:

  • resource:資源名稱,可以是閘道器中的 route 名稱或者使用者自定義的 API 分組名稱。
  • resourceMode:規則是針對 API Gateway 的 route(RESOURCE_MODE_ROUTE_ID)還是使用者在 Sentinel 中定義的 API 分組(RESOURCE_MODE_CUSTOM_API_NAME),預設是 route。
  • grade:限流指標維度,同限流規則的 grade 欄位。
  • count:限流閾值
  • intervalSec:統計時間視窗,單位是秒,預設是 1 秒。
  • controlBehavior:流量整形的控制效果,同限流規則的 controlBehavior 欄位,目前支援快速失敗和勻速排隊兩種模式,預設是快速失敗。
  • burst:應對突發請求時額外允許的請求數目。
  • maxQueueingTimeoutMs:勻速排隊模式下的最長排隊時間,單位是毫秒,僅在勻速排隊模式下生效。
  • paramItem:引數限流配置。若不提供,則代表不針對引數進行限流,該閘道器規則將會被轉換成普通流控規則;否則會轉換成熱點規則。其中的欄位:
    • parseStrategy:從請求中提取引數的策略,目前支援提取來源 IP(PARAM_PARSE_STRATEGY_CLIENT_IP)、Host(PARAM_PARSE_STRATEGY_HOST)、任意 Header(PARAM_PARSE_STRATEGY_HEADER)和任意 URL 引數(PARAM_PARSE_STRATEGY_URL_PARAM)四種模式。
    • fieldName:若提取策略選擇 Header 模式或 URL 引數模式,則需要指定對應的 header 名稱或 URL 引數名稱。
    • pattern:引數值的匹配模式,只有匹配該模式的請求屬性值會納入統計和流控;若為空則統計該請求屬性的所有值。(1.6.2 版本開始支援)
    • matchStrategy:引數值的匹配策略,目前支援精確匹配(PARAM_MATCH_STRATEGY_EXACT)、子串匹配(PARAM_MATCH_STRATEGY_CONTAINS)和正則匹配(PARAM_MATCH_STRATEGY_REGEX)。(1.6.2 版本開始支援)

使用者可以通過 GatewayRuleManager.loadRules(rules) 手動載入閘道器規則,或通過 GatewayRuleManager.register2Property(property) 註冊動態規則源動態推送(推薦方式)。

限流效果測試

把我們之前章節中搭建的那些個玩意兒都執行起來吧。

我這裡執行了 nacos、combat-gateway、combat-provider

然後通過閘道器,呼叫我們之前寫好的API介面。

http://127.0.0.1:9000/nacos-provider/loadBanlance/print

(注意呼叫的介面需要在 GatewayConfiguration  中設定好限流的路徑)

 

在瀏覽器或者PostMan中呼叫介面,連續重新整理。

然後我們檢視Sentinel的日誌(啟動時會輸出日誌地址)

下面就是限流的統計資料

passQpsblockQpssuccessQpsexceptionQpsrtoccupiedPassQpsconcurrencyclassification
536901275003
54950130003

 

其中 passQps  代表通過的請求, blockQps 代表被阻止的請求, successQps  代表成功執行完成的請求個數, exceptionQps  代表使用者自定義的異常, rt 代表平均響應時長。

由於之前設定的限流是 QPS =5 ,可以看到上面的限流日誌中,通過的請求數是 <=5/s的。

rules.add(new GatewayFlowRule("combat_gateway_api")
            .setResourceMode(SentinelGatewayConstants.RESOURCE_MODE_CUSTOM_API_NAME)
            .setCount(5)
            .setIntervalSec(1)

 

自定義限流的返回值

通過剛才的測試發現,被限流後的返回編碼是429。

Server returned HTTP response code: 429 for URL: http://127.0.0.1:9000/nacos-provider/loadBanlance/print

如何讓它按照我們業務系統的規範返回定製的錯誤資訊呢?這樣才便於我們針對閘道器限流後的處理。

在之前的 GatewayConfiguration 中註冊一個限流處理器

 GatewayCallbackManager.setBlockHandler(new MyBlockRequestHandler());

實現BlockRequestHandler

package com.zjf.combat.sentinel;

import static org.springframework.web.reactive.function.BodyInserters.fromObject;

import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.server.ServerWebExchange;

import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.BlockRequestHandler;

import reactor.core.publisher.Mono;

public class MyBlockRequestHandler implements BlockRequestHandler {

	private static final String DEFAULT_BLOCK_MSG_PREFIX = "Blocked by Sentinel: ";

	@Override
	public Mono<ServerResponse> handleRequest(ServerWebExchange exchange, Throwable ex) {
		
		// 返回http狀態碼為200
		return ServerResponse.status(200).contentType(MediaType.APPLICATION_JSON_UTF8)
				.body(fromObject(buildErrorResult(ex)));
	}

	

	private ErrorResult buildErrorResult(Throwable ex) {
		return new ErrorResult(200,
				DEFAULT_BLOCK_MSG_PREFIX + ex.getClass().getSimpleName());
	}

	

	private static class ErrorResult {
		private final int code;
		private final String message;

		ErrorResult(int code, String message) {
			this.code = code;
			this.message = message;
		}

		public int getCode() {
			return code;
		}

		public String getMessage() {
			return message;
		}
	}

}

被限流後的請求返回

相關文章