2

spring-cloud-gateway是大家比较熟悉的网关了,不仅有路由,还有限流功能等。不过不能支持匀速限流,所以集成阿里sentinel来实现匀速限流功能,即超过qps请求进行排队,匀速转发到下游。

一、搭建spring-cloud-gateway环境

1.新建一个springboot项目,并引入相关的dependency。

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
            <version>2.2.3.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
            <version>2.2.3.RELEASE</version>
        </dependency>

2.配置bootstrap.yml文件

server:
  port: 8889
spring:
  application:
    name: app-gateway
  profiles:
      active: dev
  cloud:
    gateway:
      routes:
        - id: appPush-service
          uri: http://localhost:8081/
          predicates:
            - Path=/appPush/**

此时启动好下游服务和网关服务,访问http://localhost:8889/appPush/test,就会路由到 http://localhost:8081/appPush/test,到此简单的spring-cloud-gateway就搭建完毕了。

二、集成阿里sentinel

1.引入相关的dependency。

<dependency>
            <groupId>com.alibaba.csp</groupId>
            <artifactId>sentinel-spring-cloud-gateway-adapter</artifactId>
            <version>1.6.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
            <version>0.9.1.BUILD-SNAPSHOT</version>
        </dependency>

2.编写异常处理类

package last.soul.gateway.handler;

import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.csp.sentinel.util.function.Supplier;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.codec.HttpMessageWriter;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.reactive.result.view.ViewResolver;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebExceptionHandler;
import reactor.core.publisher.Mono;

import java.nio.charset.StandardCharsets;
import java.util.List;

public class JsonSentinelGatewayBlockExceptionHandler implements WebExceptionHandler {


    private List<ViewResolver> viewResolvers;
    private List<HttpMessageWriter<?>> messageWriters;
    private final Supplier<ServerResponse.Context> contextSupplier = () -> {
        return new ServerResponse.Context() {
            @Override
            public List<HttpMessageWriter<?>> messageWriters() {
                return JsonSentinelGatewayBlockExceptionHandler.this.messageWriters;
            }

            @Override
            public List<ViewResolver> viewResolvers() {
                return JsonSentinelGatewayBlockExceptionHandler.this.viewResolvers;
            }
        };
    };

    public JsonSentinelGatewayBlockExceptionHandler(List<ViewResolver> viewResolvers, ServerCodecConfigurer serverCodecConfigurer) {
        this.viewResolvers = viewResolvers;
        this.messageWriters = serverCodecConfigurer.getWriters();
    }

    private Mono<Void> writeResponse(ServerResponse response, ServerWebExchange exchange) {
        ServerHttpResponse serverHttpResponse = exchange.getResponse();
        serverHttpResponse.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
        byte[] datas = "{\"code\":403,\"msg\":\"限流!!!\"}".getBytes(StandardCharsets.UTF_8);
        DataBuffer buffer = serverHttpResponse.bufferFactory().wrap(datas);
        return serverHttpResponse.writeWith(Mono.just(buffer));
    }

    @Override
    public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
        if (exchange.getResponse().isCommitted()) {
            return Mono.error(ex);
        } else {
            return !BlockException.isBlockException(ex) ? Mono.error(ex) : this.handleBlockedRequest(exchange, ex).flatMap((response) -> {
                return this.writeResponse(response, exchange);
            });
        }
    }

    private Mono<ServerResponse> handleBlockedRequest(ServerWebExchange exchange, Throwable throwable) {
        return GatewayCallbackManager.getBlockHandler().handleRequest(exchange, throwable);
    }
}

3.配置类

package last.soul.gateway.config;

import last.soul.gateway.handler.JsonSentinelGatewayBlockExceptionHandler;
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.slots.block.RuleConstant;
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 javax.annotation.PostConstruct;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

@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;
    }


    /**
     * 配置SentinelGatewayBlockExceptionHandler,限流后异常处理
     *
     * @return
     */
    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public JsonSentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
        return new JsonSentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
    }

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

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

    /**
     * API分组,对不组可以进行不同限流规则
     */
    private void initCustomizedApis() {
        Set<ApiDefinition> definitions = new HashSet<>();
        ApiDefinition api1 = new ApiDefinition("appPush-service")
                .setPredicateItems(new HashSet<ApiPredicateItem>() {{
                    add(new ApiPathPredicateItem().setPattern("/appPush/**")
                    );
                }});
        ApiDefinition api2 = new ApiDefinition("appPush-service2")
                .setPredicateItems(new HashSet<ApiPredicateItem>() {{
                    add(new ApiPathPredicateItem().setPattern("/appPush2/**")
                    );
                }});
        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);
    }
}

在initCustomizedApis方法中,将api分为appPush-service和appPush-service2两组,在initGatewayRules方法中对appPush-service组实行了匀速的策略。
此时用jemeter模拟100个请求,会发现第一次通过10个请求,剩下的90个每秒执行10次,一共大约10完成。
image.png

三、部署sentinel控制台并关联到网关

通过上面两步基本也完成了匀速的功能,如果想要实时监控并修改相关参数可以集成控制台,步骤如下:
1.部署控制台有两种方式,一种是下载源代码,编译运行。另外一种是下载相关jar包,通过命令运行,推荐这种。命令如下:

java -Dserver.port=9999 -Dcsp.sentinel.dashboard.server=localhost:9999 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard-1.7.2.jar

控制台端口号9999,-Dcsp.sentinel.dashboard.server=localhost:9999表示把控制台本身加到监控中,该参数可以省略。
2.并关联到网关
有两种方式,方式一,在yml文件中cloud节点,gateway平级增加

    sentinel:
      eager: true
      transport:
        dashboard: localhost:9999
        port: 9999

方式二,在app的启动参数中(Vm options)增加

-Dcsp.sentinel.app.type=1是告诉控制台,我是网关类型的APP
-Dcsp.sentinel.dashboard.server=localhost:9999 -Dproject.name=app-gateway

-Dcsp.sentinel.app.type=1是告诉控制台,我是网关类型的APP。
-Dcsp.sentinel.dashboard.server=localhost:9999是控制台地址。
image.png
无论哪种方式都要在网关中添加控制台dependency

        <dependency>
            <groupId>com.alibaba.csp</groupId>
            <artifactId>sentinel-transport-simple-http</artifactId>
            <version>1.6.3</version>
        </dependency

3.访问控制台,并登录。地址为:http://localhost:9999/
默认的用户名密码:sentinel/sentinel
4.用jemeter模拟访问网关,并刷新控制台(因为实现监控不访问没有曲线,访问结束曲线也会消失),就可以看到qps的曲线图。
5.可以实时修改限流相关参数,会实现生效,但是网关重启后会重置为网关里代码写的值。
6.第5步中修改参数功能有时会有bug,时好时坏,可能后续版本会有改善。

四、其它

1.本文没有详细介绍gateway,sentinel知识,大家可以查阅相关文档,本文重点介绍二者如何集成。
2.规则在代码里,并且不能实时更新,使用控制台修改(上面说过不太好用)可以解决。即便实时更新了,gateway项目一重启,参数会重置。但是接入配置中心appollo或者nacos(这个对你项目springboot版本有要求,不能太低),能一次解决不能实时更新和参数会重置两个问题。至于如何集成相对简单,本文不详细介绍。
3.网关集群之后下游的qps是多少?比如两台网关集群,配置的qps都是10,那么下游接收到的请求是多少?答案是相加,20。所以建议每台网关配置的qps数应该是下游能接受的qps除以网关集群个数。
那么能不能让网关集群,让整个集群的qps数设置为下游的qps数呢,答案是可以,关于spring-cloud-gateway集成比较复杂,本文不做介绍。
至于是做集群,还是不做集群把每个网关设置成qps除以集群数,看实际情况权衡吧。
4.本文部分是参考网上好几个文章拼凑而成,就不一一列举原文了。

本文代码地址:https://github.com/gaoxu-last...


高旭
40 声望3 粉丝