sentinel-基于springcloud gateway(scg)的整合方案

提示:本整合方案基于springcloud alibaba的毕业版本推荐,即:

        springboot version:2.6.3

        springcloud version:2021.0.01

        springcloud alibaba version: 2021.0.1.0

目录

前言

二、使用步骤

1.父pom内容

2.gateway工程pom

3.相关代码 

        3.1 gateway yml配置文件

        3.2 请求交易码判断自定义断言

        3.3 请求灰度字段判断自定义断言

        3.4 请求验签过滤器

        3.5 响应加签过滤器方法

        3.6 sentinel BlockRequest实现类

 4.nacos持久化配置

        4.1限流规则

        4.2熔断规则

        4.3补充说明

总结


前言

基于springcloud gateway和sentinel整合的配置,记录为主。

简陋的画了一个时序图

sentinel-基于springcloud gateway(scg)的整合方案_第1张图片


一、sentinel是什么

官方解释:面向分布式服务架构的高可用流量控制组件

二、使用步骤

请注意:以下配置由于部分原因不能展示编译、打包等构建相关的插件配置,如果您需要参考请自行按情况添加,此文档只记录和标题相关的配置项。

1.父pom内容



    4.0.0

    com.hx
    qp-demo
    1.0-SNAPSHOT

    pom

    
        org.springframework.boot
        spring-boot-starter-parent
        2.6.3
         
    

    
        2021.0.1
        2021.0.1.0
    

    
        
            
                org.springframework.cloud
                spring-cloud-dependencies
                ${spring-cloud.version}
                pom
                import
            
            
                com.alibaba.cloud
                spring-cloud-alibaba-dependencies
                ${spring-cloud-alibaba.version}
                pom
                import
            
        
    
    

2.gateway工程pom



    
        qp-demo
        com.hx
        1.0-SNAPSHOT
    
    4.0.0
    网关

    qp-gateway


    
        
            org.springframework.cloud
            spring-cloud-starter-bootstrap
        
        
            org.springframework.cloud
            spring-cloud-starter-gateway
        
        
            com.alibaba.cloud
            spring-cloud-starter-alibaba-nacos-config
        
        
            com.alibaba.cloud
            spring-cloud-starter-alibaba-nacos-discovery
        
        
            org.springframework.cloud
            spring-cloud-starter-loadbalancer
        
        
            com.alibaba.cloud
            spring-cloud-starter-alibaba-sentinel
        
        
        
            com.alibaba.csp
            sentinel-datasource-nacos
        
        
        
            com.alibaba.cloud
            spring-cloud-alibaba-sentinel-gateway
        
        




    

3.相关代码 

        3.1 gateway yml配置文件

spring:
  application:
    name: qp-gateway
  cloud:
    #sentinel配置
    sentinel:
      #sentinel控制台配置,本地端口不配置默认从8719开始尝试监听
      transport:
        dashboard: 127.0.0.1:8080
      #nacos持久化配置
      datasource:
        ds1:
          nacos:
            server-addr: 127.0.0.1:8848
            data-id: qp-gateway-gw-flow
            group-id: qp
            data-type: json
            #网关限流规则
            rule-type: gw-flow
        ds2:
          nacos:
            server-addr: 127.0.0.1:8848
            namespace: public
            data-id: qp-gateway-degrade
            group-id: qp
            data-type: json
            #熔断规则
            rule-type: degrade
    gateway:
      #全局过滤器,此处做了交易请求的验签和签名
      default-filters:
      #验证签名
      - NuccMsg=true
      #签名
      - name: ModifyResponseBody
        args:
          rewriteFunction: '#{@nuccSign}'
          outClass: java.lang.String
          inClass: java.lang.String
      routes:
      #灰度支付交易网关
      - id: payment-gary
        order: 1
        uri: lb://qp-server-payment-gary
        predicates:
        #交易路由自定义断言,按请求中交易码字段进行判断
        - Path=/main/**
        - name: Nucc
          args: 
            msgTps: ["tradecode01","tradecode02","test"]
        #灰度发布自定义断言,按请求中某字段的正则表达式进行判断
        - NuccGary=SgnNo, ^110120119.*
        filters:
        #请求地址重写
        - RewritePath=/main/?(?.*), /payment/$\{segment}
      #正常支付交易网关
      - id: payment
        order: 2
        uri: lb://qp-server-payment
        predicates:
        - Path=/main/**
        - name: Nucc
          args: 
            msgTps: ["tradecode01","tradecode02","test"]
        filters:
        - RewritePath=/main/?(?.*), /payment/$\{segment}
        
nacos:
  discovery:
    server-addr: 127.0.0.1:8848

config:
  fileName: dev


server:
  port: 8088

        3.2 请求交易码判断自定义断言

import lombok.extern.slf4j.Slf4j;
import org.reactivestreams.Publisher;
import org.springframework.cloud.gateway.handler.AsyncPredicate;
import org.springframework.cloud.gateway.handler.predicate.AbstractRoutePredicateFactory;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.http.codec.HttpMessageReader;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.HandlerStrategies;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;

/**
 * 某机构XML接口规范实现的路由断言工厂
 */
@Component
@Slf4j
public class NuccRoutePredicateFactory extends AbstractRoutePredicateFactory {

    private final List> messageReaders;

    public NuccRoutePredicateFactory() {
        super(NuccRoutePredicateFactory.Config.class);
        this.messageReaders = HandlerStrategies.withDefaults().messageReaders();
    }

    public NuccRoutePredicateFactory(List> messageReaders) {
        super(NuccRoutePredicateFactory.Config.class);
        this.messageReaders = messageReaders;
    }

    @Override
    public AsyncPredicate applyAsync(NuccRoutePredicateFactory.Config config) {
        return new AsyncPredicate() {
            @Override
            public Publisher apply(ServerWebExchange exchange) {
                Object cachedBody = exchange.getAttribute("bodyCache");
                if (cachedBody != null) {
                    return Mono.just(checkMspTp(cachedBody, config));
                } else {
                    log.debug("解析....");
                    return ServerWebExchangeUtils.cacheRequestBodyAndRequest(exchange,
                            (serverHttpRequest) -> ServerRequest
                                    .create(exchange.mutate().request(serverHttpRequest).build(), messageReaders)
                                    .bodyToMono(String.class).doOnNext(objectValue -> exchange.getAttributes()
                                            .put(GatewayCons.CACHE_REQUEST_BODY_OBJECT_KEY, objectValue))
                                    .map(objectValue -> checkMspTp(objectValue, config)));
                }
            }

            @Override
            public Object getConfig() {
                return config;
            }
        };
    }

    /**
     * 校验报文编号
     *
     * @param requestBody 请求body正文
     * @param config      配置
     * @return
     */
    private boolean checkMspTp(Object requestBody, NuccRoutePredicateFactory.Config config) {
        String content = String.valueOf(requestBody);
        if (content == null) {
            log.warn("没有获取到请求body,请核实");
            return false;
        }
        if (!content.contains("") + 7);// xml报文域
        String sign = content.substring(content.indexOf("{S:") + 3, content.lastIndexOf("}"));// 签名域
        String msgTp = content.substring(content.indexOf(""));// 报文编号
        return Arrays.asList(config.msgTps).contains(msgTp);
    }

    @Override
    public Predicate apply(NuccRoutePredicateFactory.Config config) {
        throw new UnsupportedOperationException("NuccRoutePredicateFactory is only async.");
    }

    public static class Config {
        String[] msgTps; //报文编号

        public String[] getMsgTps() {
            return msgTps;
        }

        public NuccRoutePredicateFactory.Config setMsgTps(String[] msgTps) {
            this.msgTps = msgTps;
            return this;
        }
    }

}

        3.3 请求灰度字段判断自定义断言

package com.yld.gateway.predicate;

import lombok.extern.slf4j.Slf4j;
import org.reactivestreams.Publisher;
import org.springframework.cloud.gateway.handler.AsyncPredicate;
import org.springframework.cloud.gateway.handler.predicate.AbstractRoutePredicateFactory;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.http.codec.HttpMessageReader;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import org.springframework.web.reactive.function.server.HandlerStrategies;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;

/**
 * 灰度路由断言工厂
 */
@Component
@Slf4j
public class NuccGaryRoutePredicateFactory extends AbstractRoutePredicateFactory {

    private final List> messageReaders;

    @Override
    public List shortcutFieldOrder() {
        return Arrays.asList("xmlTag", "regexp");
    }

    public NuccGaryRoutePredicateFactory() {
        super(NuccGaryRoutePredicateFactory.Config.class);
        this.messageReaders = HandlerStrategies.withDefaults().messageReaders();
    }

    public NuccGaryRoutePredicateFactory(List> messageReaders) {
        super(NuccGaryRoutePredicateFactory.Config.class);
        this.messageReaders = messageReaders;
    }

    @Override
    public AsyncPredicate applyAsync(NuccGaryRoutePredicateFactory.Config config) {
        return new AsyncPredicate() {
            @Override
            public Publisher apply(ServerWebExchange exchange) {
                Object cachedBody = exchange.getAttribute("bodyCache");
                if (cachedBody != null) {
                    return Mono.just(checkGaryTag(cachedBody, config));
                } else {
                    log.debug("解析....");
                    return ServerWebExchangeUtils.cacheRequestBodyAndRequest(exchange,
                            (serverHttpRequest) -> ServerRequest
                                    .create(exchange.mutate().request(serverHttpRequest).build(), messageReaders)
                                    .bodyToMono(String.class).doOnNext(objectValue -> exchange.getAttributes()
                                            .put(GatewayCons.CACHE_REQUEST_BODY_OBJECT_KEY, objectValue))
                                    .map(objectValue -> checkGaryTag(objectValue, config)));
                }
            }

            @Override
            public Object getConfig() {
                return config;
            }
        };
    }

    /**
     * 判断是否符合灰度断言校验
     * @param requestBody
     * @param config
     * @return
     */
    private boolean checkGaryTag(Object requestBody, NuccGaryRoutePredicateFactory.Config config) {
        String content = String.valueOf(requestBody);
        if (content == null) {
            log.warn("没有获取到请求body,请核实");
            return false;
        }
        if (!content.contains("", config.xmlTag); //格式化标签开始
            String tagEnd = String.format("", config.xmlTag); //格式化标签结束
            String xmlTagValue = content.substring(content.indexOf(tag) + tag.length(), content.indexOf(tagEnd)); //xml标签截取
            if (xmlTagValue.matches(config.regexp)) { //按正则判断
                log.debug("断言匹配到了灰度标记:xmltag:{},regexp:{},报文匹配:{}",config.xmlTag, config.regexp, xmlTagValue);
                return true;
            }
            return false;
        }
        return false;
    }

    @Override
    public Predicate apply(NuccGaryRoutePredicateFactory.Config config) {
        throw new UnsupportedOperationException("NuccGaryRoutePredicateFactory is only async.");
    }

    public static class Config {
        private String xmlTag;// 报文的XML标签名
        private String regexp;// 报文内容的正则

        public String getXmlTag() {
            return xmlTag;
        }

        public NuccGaryRoutePredicateFactory.Config setXmlTag(String xmlTag) {
            this.xmlTag = xmlTag;
            return this;
        }

        public String getRegexp() {
            return regexp;
        }

        public NuccGaryRoutePredicateFactory.Config setRegexp(String regexp) {
            this.regexp = regexp;
            return this;
        }
    }
}

        3.4 请求验签过滤器

import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;

/**
 * 请求验签过滤器
 */
@Component
@Slf4j
public class NuccMsgGatewayFilterFactory extends AbstractGatewayFilterFactory {

    public NuccMsgGatewayFilterFactory() {
        super(NuccMsgGatewayFilterFactory.Config.class);
    }

    @Override
    public GatewayFilter apply(Config config) {
        return (exchange, chain) -> {
            Object requestBody = exchange.getAttribute("bodyCache");
            String content = String.valueOf(requestBody);
            if (content == null) {
                log.warn("没有获取到请求body,请核实");
            }
            if (!content.contains("") + 7);// xml报文域
            String sign = content.substring(content.indexOf("{S:") + 3, content.lastIndexOf("}"));// 签名域
            log.debug("mock验签......");
            //..TODO
            log.debug("请求正文:{},签名:{}", xmlStr, sign);
            return chain.filter(exchange);
        };
    }

    @Data
    public static class Config {
        boolean signCheckType; //签名校验类型,true校验,false不校验
    }
}

        3.5 响应加签过滤器方法

加签没有编写过滤器是因为scg已经实现了ModifyResponseBody,我们直接对接口RewriteFunction编写实现类即可,网关Yml配置文件可见配置关系。

import lombok.extern.slf4j.Slf4j;
import org.reactivestreams.Publisher;
import org.springframework.cloud.gateway.filter.factory.rewrite.RewriteFunction;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

@Component("nuccSign")
@Slf4j
public class NuccSignFunction implements RewriteFunction {

    /**
     * 响应进行签名
     *
     * @param serverWebExchange 下游web句柄
     * @param resp              响应报文
     * @return
     */
    @Override
    public Publisher apply(ServerWebExchange serverWebExchange, String resp) {
        log.debug("响应报文签名:{}", resp);
        if (!resp.contains("root")) {
            return Mono.just(resp);
        }
        String xmlStr = resp.substring(resp.indexOf("") + 7);// xml报文域
        resp = resp + "{S:mocksign}"; //MOCK

        return Mono.just(resp);
    }

}

        3.6 sentinel BlockRequest实现类

此方法是通过对BlockRequestHandler接口进行实现完成的,对BlockRequestHandler官方的解释是:

您可以在 GatewayCallbackManager 注册回调进行定制:

setBlockHandler:注册函数用于实现自定义的逻辑处理被限流的请求,对应接口为 BlockRequestHandler。默认实现为 DefaultBlockRequestHandler,当被限流时会返回类似于下面的错误信息:Blocked by Sentinel: FlowException。

所以我先根据我的接口规范,编写了限流或熔断规则发生时,响应给客户端的规范报文

import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.BlockRequestHandler;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeException;
import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.text.ParseException;
import java.util.Date;

/**
 * sentinel拦截后实现类
 *
 * @author hx
 * @Description 完善响应报文,按规范返回
 * @date 2022/4/25 10:46
 */
@Slf4j
@Component
public class NuccSentinelBlockHandler implements BlockRequestHandler {


    @Override
    public Mono handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) {
        log.error("NuccSentinelBlockHandler block throw ", throwable);
        String responseXml;

        if (throwable instanceof ParamFlowException) { //限流规则
            responseXml = errorMsg("00000001", "系统达到最大流量限制");
        } else if (throwable instanceof DegradeException) { //熔断规则
            responseXml = errorMsg("00000002", "系统暂时忙");
        } else {
            responseXml = errorMsg("00000003", "系统错误请稍后重试");
        }

        return ServerResponse.status(HttpStatus.TOO_MANY_REQUESTS).bodyValue(responseXml);
    }

    /**
     * 生成通用响应报文
     *
     * @param sysRtnCd   系统返回代码
     * @param sysRtnDesc 系统返回代码详细信息
     * @return
     */
    private String errorMsg(String sysRtnCd, String sysRtnDesc) {

        return String.format("%s-%s", sysRtnCd, sysRtnDesc); //规范不方便公开,理解这么做的意义即可
    }

}

GatewayCallbackManager注册可以通过CommandLineRunner容器启动后进行加载:

import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager;
import com.yld.gateway.callback.NuccSentinelBlockHandler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

/**
 * 网关配置类
 * @author hx
 * @Description 加载配置类
 * @date 2022/4/25 21:32
 */
@Slf4j
@Component
public class GatewayConfig implements CommandLineRunner {

    @Autowired
    NuccSentinelBlockHandler blockHandler;

    @Override
    public void run(String... args) throws Exception {
        GatewayCallbackManager.setBlockHandler(blockHandler);
    }
}

 4.nacos持久化配置

        首先我最初是通过sentinel的dashboard进行规则配置,但后来发现每次重启应用都需要重新配,这很不合理,翻阅了sentinel的官方文档,其中提到:

Sentinel 的理念是开发者只需要关注资源的定义,当资源定义成功后可以动态增加各种流控降级规则。Sentinel 提供两种方式修改规则:

  • 通过 API 直接修改 (loadRules)
  • 通过 DataSource 适配不同数据源修改

通过 API 修改比较直观,可以通过以下几个 API 修改不同的规则:

FlowRuleManager.loadRules(List rules); // 修改流控规则 DegradeRuleManager.loadRules(List rules); // 修改降级规则

手动修改规则(硬编码方式)一般仅用于测试和演示,生产上一般通过动态规则源的方式来动态管理规则。

既然有这建议我就参考进行配置,按官方文档的意思,配置关系应该是这样的:

sentinel-基于springcloud gateway(scg)的整合方案_第2张图片

1.首先网关应用启动,通过nacos获取gateway.yml配置

2.sentinel数据源组件通过配置(配置样例在上面),获取对应的json配置文件

3.加载成功后,如果应用在sentinel dashboard注册了,那么可以打开dashboard进行监控(不可以修改或增加配置,因为重启应用后就失效了)

        4.1限流规则

引用我的另外一篇文章

springcloud gateway(scg)-sentinel nacos配置-流控篇icon-default.png?t=M3K6https://blog.csdn.net/hanxu00920/article/details/124432569

        4.2熔断规则

引用我的另外一篇文章

springcloud gateway(scg)-sentinel nacos配置-熔断篇icon-default.png?t=M3K6https://blog.csdn.net/hanxu00920/article/details/124445362       

        4.3补充说明

        我的网关接入sentinel是通过springcloud alibaba接入的,也就是引用了spring-cloud-alibaba-sentinel-gateway组件,sentinel官方文档中还提到了可以通过sentinel-spring-cloud-gateway-adapter适配器进行接入,我没有深入去研究二者的区别,但使用前者接入,dashboard是可以识别出网关资源(网关是按route id标注资源,而普通的应用则是通过url标注资源)。


总结

以上内容仅为个人记录,如有可以参考到的地方希望对你有帮助,如果有错误、或更好的方式实现以上功能可以留言。

你可能感兴趣的:(java,微服务,springcloud,gateway,spring)