天天看點

如何實作 SpringCloud Gateway 整合 Sentinel 流控規則,并持久化到 Nacos開始

開始

文檔目的

原來想通過整合Sentinel,對spring cloud gateway請求進行流控;在Sentinel界面中修改和增加流控規則,同步到nacos。

百度有很多文章,但是實踐下來沒有一個能夠實作我想要的結果,于是決定在前人的基礎上研究,終于初步達成了目的。由于本人水準有限,有些概念沒有深入了解,請見諒!

版本資訊

name version desc
spring boot 2.7.3
spring cloud 2021.0.3
io.springfox 3.0.0
knife4j 3.0.2
com.alibaba.cloud 2021.1
nacos 2.1.0 cluster mode
sentinel 1.8.4
mysql 5.7.22

整合

nacos 叢集模式配置(一台機器)

  • 配置3個nacos服務,目錄如下:
如何實作 SpringCloud Gateway 整合 Sentinel 流控規則,并持久化到 Nacos開始
  • 配置端口和資料庫連接配接
### nacos1
### Default web server port:
server.port=8840

### Connect URL of DB:
db.url.0=jdbc:mysql://127.0.0.1:3306/nacos_config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
db.user.0=XXXX
db.password.0=XXXXXX
           

同樣,将nacos2的

server.port

改為8850;nacos3的

server.port

改為8860

  • nacos1、nacos2、nacos3三個執行個體的

    cluster.conf

    修改
#2022-09-19T08:54:21.232
172.20.11.5:8840
172.20.11.5:8850
172.20.11.5:8860
           

注意:叢集模式下一定要配置為真實的ip加端口,不能用127.0.0.1或者localhost代替;後面為

spring cloud gateway

配置nacos時候,一定要寫真實的ip,否則會報以下的錯誤:

com.alibaba.nacos.api.exception.NacosException: failed to req API:/nacos/v1/ns/instance 
after all servers([http://127.0.0.1:8848])
           

由于機器資源限制,實際我隻啟動了nacos1一個執行個體。

spring cloud gateway網關搭建

server:
  port: 9000
spring:
  application:
    name: gateway-server
  main:
    allow-bean-definition-overriding: true
  config:
    activate:
      on-profile: ${SPRING_PROFILES_ACTIVE:dev}
  cloud:
    nacos:
      config:
        server-addr: ${NACOS_SERVER_ADDR:172.20.11.5}:8840  # 配置中心真實ip
        file-extension: yaml
        namespace: middle-${SPRING_PROFILES_ACTIVE:dev}
        group: ${SPRING_PROFILES_ACTIVE:dev}
        refresh-enabled: true
      discovery:
        server-addr: ${NACOS_SERVER_ADDR:172.20.11.5}:8840  #服務注冊和發現中心,和配置中心使用同一個變量
                                                            #叢集模式:localhost:8848,localhost:8849,localhost:8850
        namespace: middle-${SPRING_PROFILES_ACTIVE:dev}
        group: ${SPRING_PROFILES_ACTIVE:dev}
    gateway:
      globalcors:
        cors-configurations: #允許跨域請求
          '[**]':
            allowedOrigins: '*'
            allowedMethods: '*'
      discovery:
        locator:
          enabled: true
    sentinel: # sentinel 整合
      transport:
        dashboard: ${SENTINEL_DASHBOARD_ADDR:172.20.11.5}:8800 #sentinel控制台通路路徑
      filter:
        enabled: false #心跳啟動
      datasource:
        ds:
          nacos: # 整合nacos,把流控規則儲存到nacos, sentinel控制台啟動時候就能夠讀取這個配置
            server-addr: ${NACOS_SERVER_ADDR:172.20.11.5}:8840
            dataId: ${spring.application.name}-sentinel-flow # 為什麼會這樣設定,後面會講到
            namespace: middle-${SPRING_PROFILES_ACTIVE:dev}
            groupId: ${SPRING_PROFILES_ACTIVE:dev}
            rule-type: gw_flow # 流控配置
      eager: true
management:
  endpoints:
    web:
      exposure:
        include: '*' #shutdown,health,info,loggers,gateway,sentinel
  endpoint:
    shutdown:
      enabled: true
    health: # 适用于k8s  LivenessProbe(健康檢查) 和 ReadinessProbe(是否已經準備好接受請求)
      show-details: always #不顯示詳細資訊

           

上述配置要注意幾點:

  • nacos隻啟動了一個點,是以隻配置一個節點
  • nacos既作為注冊中心又作為配置中心
  • sentinel

    下面配置

    datasource

    為nacos時候,就是把sentinel下流控配置存儲到nacos, 這裡采用服務名加

    -sentinel-flow

    的字尾,對應于後面

    sentinel-dashboard

    源碼的修改
  • 在網關啟動時候,加入參數

    -Dcsp.sentinel.app.type=1

    ,告訴sentinel這是一個網關類型,

    sentinel-bashboard

    控制台如下顯示就對了:
如何實作 SpringCloud Gateway 整合 Sentinel 流控規則,并持久化到 Nacos開始
兩個微服務開發,分别為demo和demo1, 這裡就不用多講了,見源碼。

sentinel-dashboard改造

主要就是建立、修改和删除流控規則時候,調用nacos提供的接口,将相關流控資訊推送給nacos, 儲存到mysql.我參考了下面的這個文章,在他提供的源碼上面進行了修改,實作了我的目的,非常感謝!

https://blog.csdn.net/q669239799/article/details/125777745
  • 參考上面的文章,下載下傳文章中提供的

    sentinel-bashboard

    源碼。在

    application.properties

    中修改端口和增加nacos配置
server.port=8800
nacos.address=172.20.11.5:8840
nacos.namespace=middle-dev
nacos.group=dev
           
  • 通過debug發現,最後其實是調用

    GatewayFlowRuleController

    類的

    addFlowRule

    updateFlowRule

    deleteFlowRule

    對sentinel記憶體中的流控規則進行新增、修改和删除的。是以在對記憶體中的流控規則操作後,再調用nacos的接口,更改nacos裡面的流控規則,達到儲存到nacos的目的。這裡以修改流控規則為例子:
    • 修改

      NacosConfig.java

      , 增加對于

      GatewayFlowRuleEntity

      的轉換Bean
package com.alibaba.csp.sentinel.dashboard.config.nacos;

import com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway.GatewayFlowRuleEntity;
import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.DegradeRuleEntity;
import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.FlowRuleEntity;
import com.alibaba.csp.sentinel.datasource.Converter;
import com.alibaba.fastjson.JSON;
import com.alibaba.nacos.api.PropertyKeyConst;
import com.alibaba.nacos.api.config.ConfigFactory;
import com.alibaba.nacos.api.config.ConfigService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.List;
import java.util.Properties;

/**
 * @author Eric Zhao
 * @since 1.4.0
 */
@Configuration
public class NacosConfig {
    @Value("${nacos.address}")
    private String addr;   //Nacos位址,對應于上面的設定

    @Value("${nacos.namespace}")
    private String namespace;   //Nacos命名空間,對應于上面的設定

    @Bean
    public Converter<List<DegradeRuleEntity>, String> degradeRuleEntityEncoder() {
        return JSON::toJSONString;
    }

    @Bean
    public Converter<String, List<DegradeRuleEntity>> degradeRuleEntityDecoder() {
        return s -> JSON.parseArray(s, DegradeRuleEntity.class);
    }

    // 添加對于GatewayFlowRuleEntity的轉換
    @Bean
    public Converter<List<GatewayFlowRuleEntity>, String> gatewayFlowRuleEntityEncoder() {
        return JSON::toJSONString;
    }
    @Bean
    public Converter<String, List<GatewayFlowRuleEntity>> gatewayFlowRuleEntityDecoder() {
        return s -> JSON.parseArray(s, GatewayFlowRuleEntity.class);
    }
    // end
  
    @Bean
    public Converter<List<FlowRuleEntity>, String> flowRuleEntityEncoder() {
        return JSON::toJSONString;
    }

    @Bean
    public Converter<String, List<FlowRuleEntity>> flowRuleEntityDecoder() {
        return s -> JSON.parseArray(s, FlowRuleEntity.class);
    }

    @Bean
    public ConfigService nacosConfigService() throws Exception {
        Properties properties = new Properties();
        //nacos叢集位址
        properties.put(PropertyKeyConst.SERVER_ADDR,addr);
        //namespace為空即為public
        properties.put(PropertyKeyConst.NAMESPACE,namespace);
        return ConfigFactory.createConfigService(properties);
    }
}

           
  • 新增

    GatewayFlowRuleNacosPublisher.java

package com.alibaba.csp.sentinel.dashboard.config.flow;

import com.alibaba.csp.sentinel.dashboard.config.nacos.NacosConfigUtil;
import com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway.GatewayFlowRuleEntity;
import com.alibaba.csp.sentinel.dashboard.rule.DynamicRulePublisher;
import com.alibaba.csp.sentinel.datasource.Converter;
import com.alibaba.csp.sentinel.util.AssertUtil;
import com.alibaba.nacos.api.config.ConfigService;
import com.alibaba.nacos.api.config.ConfigType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.util.List;
// 注意,不一樣的名稱
@Component("gatewayFlowRuleNacosPublisher")
public class GatewayFlowRuleNacosPublisher implements DynamicRulePublisher<List<GatewayFlowRuleEntity>> {
    @Autowired
    private ConfigService configService;

    @Value("${nacos.group}") // 對應于application.properties裡面的配置
    private String group;
    // 使用到上面定義的Converter
    @Autowired
    private Converter<List<GatewayFlowRuleEntity>, String> converter;
    
    @Override
    public void publish(String app, List<GatewayFlowRuleEntity> rules) throws Exception {
        AssertUtil.notEmpty(app, "app name cannot be empty");
        if (rules == null) {
            return;
        }
        // 通過app服務名+字尾: 還記得我們前面說的,字尾是:-sentinel-flow, 這樣就能找到nacos裡面的配置資訊
        // 如果沒有設定group ,預設就是DEFAULT_GROUP
        // 這兒,實際上是調用NacosConfigService的功能對nacos進行操作
        configService.publishConfig(app + NacosConfigUtil.FLOW_DATA_ID_POSTFIX,
                group == null ? NacosConfigUtil.GROUP_ID : group, converter.convert(rules), ConfigType.JSON.getType());
    }

}

           
  • GatewayFlowRuleController

    添加注入剛才建立的類的執行個體
@Autowired
@Qualifier("gatewayFlowRuleNacosPublisher")
private DynamicRulePublisher<List<GatewayFlowRuleEntity>> rulePublisher;
           
  • GatewayFlowRuleController

    添加函數
private void publishRules(String appName) {
    List<GatewayFlowRuleEntity> rules = repository.findAllByApp(appName);
    try {
        rulePublisher.publish(appName, rules);
    } catch (Exception e) {
        logger.warn("public gateway flow rules to nacos fail");
    }
}
           
  • GatewayFlowRuleController

    updateFlowRule

    函數最後,添加

    publishRules

    調用
// 原有代碼,發送流控規則更新用戶端,比如這裡是我的網關服務
if (!publishRules(app, entity.getIp(), entity.getPort())) {
    logger.warn("publish gateway flow rules fail after update");
} else {
    // 用戶端成功,更新儲存到nacos持久化
    publishRules(entity.getApp());
}
           
  • 到這兒修改流控規則持久化到nacos就完了,新增和删除按照上面的思路改一下就行。
  • 對sentinel-bashboard進行打包,然後運作進行測試。

事情還沒有完...

實際在使用的時候,在sentinel更改流控規則進行更新時候,我的網關服務會出現

NullPointerException

的錯誤,導緻

sentinel-bashboard

更新流控不成功,通過網關程式日志發現,問題出在阿裡提供的

GatewayRuleManager

這個類的

applyToConvertedParamMap

函數裡面:(

GatewayRuleManager

在阿裡提供的sentinel适配于gateway的包中:

sentinel-api-gateway-adapter-common-1.8.0.jar

ParameterMetricStorage.getParamMetricForResource(resource).clearForRule(rule);
           

問題出在這一行上面,

ParameterMetricStorage.getParamMetricForResource(resource)

擷取為null, 是以報

NullPointerExceptin

; 大緻看了一下,是擷取網關相關名額的函數,這一塊也沒有精力研究,直接改為:

ParameterMetric parameterMetric = ParameterMetricStorage.getParamMetricForResource(resource);
if (parameterMetric != null) {
    parameterMetric.clearForRule(rule);
}
           

這兒沒有完全深入研究這樣寫的影響,反正從代碼上看是沒有錯的,以後有時間再深入評估。到此為止,代碼已經能夠順利運作了。

最後...

實際上,我的操作順序是這樣的

1、在nacos寫一個流控的json格式配置,主要他的data-id為:

gateway-server-sentinel-flow

(網關服務名+字尾)

[{
 "resourceMode": 0,
 "resource": "demo1",
 "grade": 1,
 "count": 3,
 "intervalSec": 1,
 "controlBehavior": 0,
 "burst": 0
},{
 "resourceMode": 0,
 "resource": "demo",
 "grade": 1,
 "count": 2,
 "intervalSec": 1,
 "controlBehavior": 0,
 "burst": 0
}]

           

2、啟動

sentinel-bashboard

(自己打的jar)

3、啟動網關程式,可以在

sentinel-bashboard

控制台上面能夠看見流控規則:注意,我塗掉的黑色部分一開始是沒有的。

如何實作 SpringCloud Gateway 整合 Sentinel 流控規則,并持久化到 Nacos開始

是以無論我怎麼設定,從網關調用背景服務從來不會阻塞;後來才發現是

resource

不對,應該在原來的resource前面加上

ReactiveCompositeDiscoveryClient_

[{
 "resourceMode": 0,
 "resource": "ReactiveCompositeDiscoveryClient_demo1",
 "grade": 1,
 "count": 3,
 "intervalSec": 1,
 "controlBehavior": 0,
 "burst": 0
},{
 "resourceMode": 0,
 "resource": "ReactiveCompositeDiscoveryClient_demo",
 "grade": 1,
 "count": 2,
 "intervalSec": 1,
 "controlBehavior": 0,
 "burst": 0
}]
           

這樣,網關調用時候就能準确的進行流控了。

注:我利用http://localhost:9000/actuator/gateway/routes檢視網關的路由資訊,得到:
如何實作 SpringCloud Gateway 整合 Sentinel 流控規則,并持久化到 Nacos開始

這個帶字首的

route_id

實際就是在

sentinelroute

控制台看見的請求鍊路上的

route_id

(見下圖), 是以上面的資源應該加上

ReactiveCompositeDiscoveryClient_

字首。再查,這個字首實際為

DiscoveryLocatorProperties

裡面的

routeIdPrefix

屬性,預設為

ReactiveCompositeDiscoveryClient_

, 在網關服務的配置檔案裡面可以配置(最後一行):

spring:  
  cloud:    
    gateway:
      globalcors:
        cors-configurations: #允許跨域請求
          '[**]':
            allowedOrigins: '*'
            allowedMethods: '*'
      discovery:
        locator:
          enabled: true
          route-id-prefix: sentinel  # 字首
           

4、實際上,應該先從網關調用,在sentinel控制台的請求鍊路上面顯示:

如何實作 SpringCloud Gateway 整合 Sentinel 流控規則,并持久化到 Nacos開始

應該從這兒添加流控規則最好,然後同步到網關服務和持久化到nacos,這才是正确的步驟。