一、关于

Gateway、Nacos以及Sentinel相关的介绍以及使用参考之前的文章,本文主要讲这三者的集成使用。

本文代码仓库地址:https://github.com/lazyrabb1t/rabb-springcloud-demo

二、Gateway集成Nacos实现动态路由

2.1 创建工程

2.2 添加依赖

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-gateway</artifactId>
    </dependency>
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
    </dependency>
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>1.2.54</version>
        <scope>compile</scope>
    </dependency>

2.3 配置

bootstrap.yml添加nacos配置:

spring:
  cloud:
    # 设置Nacos服务端配置
    nacos:
      server-addr: localhost:8848
      username: nacos
      password: nacos
      config:
        group: DEFAULT_GROUP
        route-data-id: rabb-gateway-routes

application.yml添加gateway配置:

server:
  port: 20400
spring:
  application:
    name: rabb-gateway-alibaba
  cloud:
    gateway:
      routes:
        - id: test-route
          uri: http://lazyrabbit.xyz
          predicates:
            - Path=/rabb/**
          filters:
            - RewritePath=/rabb/?(?<segment>.*), /$\{segment}
      discovery:
        locator:
          enabled: true #开启从注册中心动态创建路由的功能
          lower-case-service-id: true #使用小写服务名,默认是大写

添加配置spring.cloud.gateway.discovery.locator.enabled=true后,Spring Cloud Gateway可以使用服务发现客户端接口DiscoveryClient,从服务注意中心获取服务注册信息,然后配置相应的路由。

默认情况下,通过服务发现客户端DiscoveryClient自动配置的路由信息中,只包括一个默认的Predicate和Filter:

  • 默认的Predicate是一个Path Predicate,模式是/serviceId/**,serviceId就是从服务发现客户端中获取的服务ID。
  • 默认的Filter是一个重写路径过滤器,它的正则表达式为:/serviceId/(?<remaining>.*),它的作用是将请求路径中的serviceId去掉,变为/(?<remaining>.*)

另外也支持自定义的Predicate和Filters。

2.5 测试

启动nacos、rabb-gateway-alibaba模块(本模块)以及一个服务提供者模块rabb-nacos-provider(nacos篇里的模块)。

可以通过网关加服务名直接访问服务提供者的接口,如:http://localhost:20400/rabb-nacos-provider/。

2.6 实现动态路由

利用Spring的事件机制,动态获取配置并发布路由更新事件:

@Component
@Slf4j
public class NacosDynamicRouteService implements ApplicationEventPublisherAware {

    @Value("${spring.cloud.nacos.config.route-data-id}")
    private String dataId;

    @Value("${spring.cloud.nacos.config.group}")
    private String group;

    @Value("${spring.cloud.nacos.server-addr}")
    private String serverAddr;

    @Autowired
    private RouteDefinitionWriter routeDefinitionWriter;

    private ApplicationEventPublisher applicationEventPublisher;

    private static final List<String> ROUTE_LIST = new ArrayList<>();

    @PostConstruct
    public void dynamicRouteByNacosListener() {
        try {
            ConfigService configService = NacosFactory.createConfigService(serverAddr);
            String config = configService.getConfig(dataId, group, 5000);
            this.refreshRoute(config);
            configService.addListener(dataId, group, new Listener() {
                @Override
                public void receiveConfigInfo(String configInfo) {
                    clearRoute();
                    refreshRoute(config);
                }

                @Override
                public Executor getExecutor() {
                    return null;
                }
            });
        } catch (NacosException e) {
            log.error("路由监听配置异常:{}", e.getMessage(), e);
        }
    }

    private void refreshRoute(String routeStr) {
        try {
            List<RouteDefinition> gatewayRouteDefinitions = JSONObject.parseArray(routeStr, RouteDefinition.class);
            for (RouteDefinition routeDefinition : gatewayRouteDefinitions) {
                addRoute(routeDefinition);
            }
            publish();
            log.info("路由更新:{}", routeStr);
        } catch (Exception e) {
            log.error("路由更新异常:{}", e.getMessage(), e);
        }
    }

    private void clearRoute() {
        for (String id : ROUTE_LIST) {
            this.routeDefinitionWriter.delete(Mono.just(id)).subscribe();
        }
        ROUTE_LIST.clear();
    }

    private void addRoute(RouteDefinition definition) {
        try {
            routeDefinitionWriter.save(Mono.just(definition)).subscribe();
            ROUTE_LIST.add(definition.getId());
        } catch (Exception e) {
            log.error("路由更新异常:{}", e.getMessage(), e);
        }
    }

    private void publish() {
        this.applicationEventPublisher.publishEvent(new RefreshRoutesEvent(this.routeDefinitionWriter));
    }

    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
        this.applicationEventPublisher = applicationEventPublisher;
    }
}

在nacos中添加名为rabb-gateway-routes的配置,值如下:

[
    {
        "id": "rabb-nacos-provider",
        "order": 0,
        "predicates": [
            {
                "args": {
                    "pattern": "/rabb-nacos-provider/**"
                },
                "name": "Path"
            }
        ],
        "filters": [
            {
                "name": "RewritePath",
                "args": {
                    "regexp": "/rabb-nacos-provider/?(?<segment>.*)",
                    "replacement": "/${segment}"
                }
            }
        ],
        "uri": "lb://rabb-nacos-provider"
    },
    {
        "filters": [
            {
                "name": "RewritePath",
                "args": {
                    "regexp": "/rabb/?(?<segment>.*)",
                    "replacement": "/${segment}"
                }
            }
        ],
        "id": "rabb_route",
        "order": 1,
        "predicates": [
            {
                "args": {
                    "pattern": "/rabb/**"
                },
                "name": "Path"
            }
        ],
        "uri": "http://www.lazyrabbit.xyz"
    }
]

同时修改配置spring.cloud.gateway.discovery.locator.enabled=false后,关闭自动映射服务的路由。

2.7 测试动态路由

修改nacos中路由配置,可以看到网关的日志中有更新路由的输出,同时更改后的路由生效。

三、Gateway集成Sentinel实现流量控制

从 1.6.0 版本开始,Sentinel 提供了 Spring Cloud Gateway 的适配模块,可以提供两种资源维度的限流:

  • route 维度:即在 Spring 配置文件中配置的路由条目,资源名为对应的 routeId
  • 自定义 API 维度:用户可以利用 Sentinel 提供的 API 来自定义一些 API 分组

3.1 添加依赖

    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
    </dependency>
    <!-- sentinel 集成gateway -->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
    </dependency>

3.2 配置

spring:
  cloud:
    sentinel:
      eager: true
      transport:
        dashboard: localhost:8880

3.3 代码配置异常处理器以及API分组和规则

@Configuration
public class SentinelConfig {

    @PostConstruct
    public void doInit() {
        // 代码方式添加api分组以及规则
        initCustomizedApis();
        initGatewayRules();
        // 自定义异常处理
        initBlockHandler();
    }

    /**
     * 自定义限流异常处理器
     */
    private void initBlockHandler() {
        BlockRequestHandler blockRequestHandler = new BlockRequestHandler() {
            @Override
            public Mono<ServerResponse> handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) {
                return ServerResponse.status(HttpStatus.TOO_MANY_REQUESTS)
                        .contentType(MediaType.APPLICATION_JSON)
                        .body(BodyInserters.fromValue(ResponseUtils.failure("限流")));
            }
        };

        // 加载自定义限流异常处理器
        GatewayCallbackManager.setBlockHandler(blockRequestHandler);
    }
    
    /**
     * API分组,对不组可以进行不同限流规则
     */
    private void initCustomizedApis() {
        Set<ApiDefinition> definitions = new HashSet<>();
        ApiDefinition api1 = new ApiDefinition("some_customized_api")
                .setPredicateItems(new HashSet<ApiPredicateItem>() {{
                    add(new ApiPathPredicateItem().setPattern("/rabb-nacos-provider/openfeign/user/**"));
                }});
        ApiDefinition api2 = new ApiDefinition("another_customized_api")
                .setPredicateItems(new HashSet<ApiPredicateItem>() {{
                    add(new ApiPathPredicateItem().setPattern("/rabb-nacos-provider/openfeign/test/**"));
                }});
        definitions.add(api1);
        definitions.add(api2);
        GatewayApiDefinitionManager.loadApiDefinitions(definitions);
    }

    /**
     * 配置限流规则
     */
    private void initGatewayRules() {
//        Set<GatewayFlowRule> rules = new HashSet<>();
//        rules.add(new GatewayFlowRule("appPush-service")
//                .setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER)//0 直接拒绝 ,1 Warm Up, 2 匀速排队
//                .setGrade(RuleConstant.FLOW_GRADE_QPS)//0 基于线程数,1 基于QPS
//                .setMaxQueueingTimeoutMs(5000)//和排队数量、单个任务处理时间成正比。可以设置大点防止丢弃请求。
//                .setCount(10)//qps限制为10
//                .setIntervalSec(1)//时间窗口为1s
//        );
//        GatewayRuleManager.loadRules(rules);
    }
}

3.4 启动配置

添加启动参数-Dcsp.sentinel.app.type=1启动该模块,告诉sentinel此模块为一个网关工程。

或者添加以下代码:

@SpringBootApplication
public class GatewayNacosSentinelApplication {

    public static void main(String[] args) {
        System.setProperty("csp.sentinel.app.type","1");
        SpringApplication.run(GatewayNacosSentinelApplication.class, args);
    }
}

3.5 测试

重启sentinel以及本模块,进行测试

四、Sentinel集成Naocs动态数据源

4.1 添加依赖

    <dependency>
        <groupId>com.alibaba.csp</groupId>
        <artifactId>sentinel-datasource-nacos</artifactId>
    </dependency>

4.2 配置

添加sentinel.datasource配置:

spring:
  application:
    name: rabb-sentinel
  cloud:
    nacos:
      server-addr: localhost:8848
      username: nacos
      password: nacos
    sentinel:
      transport:
        dashboard: localhost:8880
        port: 8719
      datasource:
        ds:
          nacos:
            server-addr: 127.0.0.1:8848
            data-id: rabb-gateway-sentinel-rule
            group-id: DEFAULT_GROUP
            username: nacos
            password: nacos
            ruleType: gw-flow
      eager: true

ds 是 ReadableDataSource 的名字,可随意编写。后面的 file ,zk ,nacos , apollo 就是对应具体的数据源,它们后面的配置就是这些具体数据源各自的配置。注意数据源的依赖要单独引入(比如 sentinel-datasource-nacos)。
每种数据源都有两个共同的配置项: data-type、 converter-class 以及 rule-type。

  • data-type 配置项表示 Converter 类型,Spring Cloud Alibaba Sentinel 默认提供两种内置的值,分别是 json 和 xml (不填默认是json)。
  • 如果不想使用内置的 json 或 xml 这两种 Converter,可以填写 custom 表示自定义 Converter,然后再配置 converter-class 配置项,该配置项需要写类的全路径名(比如 spring.cloud.sentinel.datasource.ds1.file.converter-class=com.alibaba.cloud.examples.JsonFlowRuleListConverter)。
  • rule-type 配置表示该数据源中的规则属于哪种类型的规则(flow, degrade, authority, system, param-flow, gw-flow, gw-api-group)。注意网关流控规则 (GatewayFlowRule) 对应 gw-flow。

4.3 在nacos添加流控规则

在nacos中添加一个名称rabb-gateway-sentinel-rule的配置,其值如下:

[
    {
        "resource": "rabb-nacos-provider",
        "limitApp": "default",
        "grade": 1,
        "count": 1,
        "strategy": 0,
        "controlBehavior": 0,
        "clusterMode": false
    }
]

对应属性:

  • resource:资源名,即限流规则的作用对象
  • limitApp:流控针对的调用来源,若为 default 则不区分调用来源
  • grade:限流阈值类型(QPS 或并发线程数);0代表根据并发数量来限流,1代表根据QPS来进行流量控制
  • count:限流阈值
  • strategy:调用关系限流策略
  • controlBehavior:流量控制效果(直接拒绝、Warm Up、匀速排队)
  • clusterMode:是否为集群模式

4.4 测试

重新启动各个工程,登录sentinel控制台,可以在流控规则页面中发现4.3中的配置规则。

修改nacos中配置,sentinel能及时同步过来。sentinel中的配置暂时无法同步到nacos中。