Sentinel 整合 Spring Cloud + Gateway + 控制台Nacos持久化

项目版本

Spring Boot版本:2.2.9.RELEASE
Spring Cloud版本:Hoxton.SR7
Spring Cloud Alibaba版本:2.2.1.RELEASE

官方doc

所有功能文档概况
官方文档

介绍

本文分为以下几部分:

  • 普通服务集成
  • 控制台整合Nacos 自己Github重新封装地址
  • 网关集成(spring cloud gateway)
  • 控制台启动、参数和指定Nacos地址+namespace等

Nacos持久化最终效果图如下:

image.png

普通服务

普通服务效果图

注:Sentinel兼容Hystrix的FallbackFactory

从 Hystrix 迁移到 Sentinel方案(官方)

Sentinel 与 Hystrix 的对比(官方)

import feign.hystrix.FallbackFactory;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class NacosProviderClientFallback implements FallbackFactory<NacosProviderClient> {

    @Override
    public NacosProviderClient create(Throwable throwable) {
        return new NacosProviderClient() {
            @Override
            public String abc(String name, int age) {
                log.error("用 nacos-provider abc 方法失败, 异常信息={}", throwable.getMessage());
                //throw new RuntimeException("abc:" + throwable.getMessage());
                //这里直接返回BusinessException(),因为
                throw new RuntimeException("aaaaaaadssssssssssss");
//                return throwable.getClass().getName() + " : " + throwable.getMessage();
            }

            @Override
            public String getEcho(String string) {
                log.error("用 nacos-provider getEcho 方法失败", throwable);
                throw new RuntimeException("getEcho:" + throwable.getMessage());
            }
        };
    }
}

基础包

基础包大概截图:


image.png

引入以下两个包,一个是sentinel核心组件,一个是整合nacos实现持久化功能组件(无关顺序

        <!-- alibaba Sentinel组件 -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>

        <!-- alibaba Sentinel整合nacos组件 -->
        <dependency>
            <groupId>com.alibaba.csp</groupId>
            <artifactId>sentinel-datasource-nacos</artifactId>
        </dependency>

添加自定义异常处理(不加会返回默认的错误信息Blocked by Sentinel: XXXX

注: 网上方案实现 UrlBlockHandler 接口的版本已经比较老了,新版本已经没有UrlBlockHandler 接口

/**
 * 没有配资源,默认Block异常处理(只能是Block异常)
 */
@Slf4j
@Component
public class CustomBlockExceptionHandler implements BlockExceptionHandler {

    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, BlockException e) throws Exception {
        httpServletResponse.setStatus(HttpStatus.SERVICE_UNAVAILABLE.value());
        if (e instanceof FlowException) {
            log.error("FlowException 普通服务限流,资源信息:" + JSON.toJSONString(e.getRule()));
            httpServletResponse.setStatus(org.springframework.http.HttpStatus.TOO_MANY_REQUESTS.value());
            ResponseUtil.responseFailed(objectMapper, httpServletResponse, 1000, "API interface limited flow.");
        } else if (e instanceof DegradeException) {
            log.error("DegradeException 普通服务降级,资源信息:" + JSON.toJSONString(e.getRule()));
            ResponseUtil.responseFailed(objectMapper, httpServletResponse, 1001, "API interface has been degraded.");
        } else if (e instanceof ParamFlowException) {
            ParamFlowException ex = (ParamFlowException) e;
            log.error("ParamFlowException 参数热点限流,资源名={},参数={},资源信息={}", ex.getResourceName(), ex.getLimitParam(), JSON.toJSONString(ex.getRule()));
            ResponseUtil.responseFailed(objectMapper, httpServletResponse, 1002, "API interface limited flow by params.");
        } else if (e instanceof AuthorityException) {
            log.error("AuthorityException 授权规则,资源信息:" + JSON.toJSONString(e.getRule()));
            ResponseUtil.responseFailed(objectMapper, httpServletResponse, 1003, "API interface limited by authority.");
        } else if (e instanceof SystemBlockException) {
            SystemBlockException systemBlockException = (SystemBlockException) e;
            log.error("SystemBlockException,资源名:{},资源类型:{}", systemBlockException.getResourceName(), systemBlockException.getRuleLimitApp());
            ResponseUtil.responseFailed(objectMapper, httpServletResponse, 1004, "API interface limited by system.");
        }
    }
}

非必须。配置默认加注解 @SentinelResource(value = "abcdd",fallbackClass = DefaultBlockFallbackHandler.class, defaultFallback = "defaultFallback") 返回的异常,可以做默认异常配置。如果默认不符合异常需求,可以在各接口自己定制自己的异常,可以是同类,也可以是不同类(不同类必须指定fallbackClass属性,参照上面注解例子)

以下走的默认异常 或 不同类异常,不在同一个类中(默认异常加fallbackClass )

@GetMapping(value = "/woqu")
    @SentinelResource(value = "abcdd",
            fallbackClass = DefaultBlockFallbackHandler.class, defaultFallback = "defaultFallback")
    public String woqu(String name){
        if(name.equals("abc")){
            throw new IllegalArgumentException("adffdgdfg");
        }
        if(name.equals("bbb")){
            try {
                Thread.sleep(900);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        return "aaaaaaasdjkl" + name;
    }

以下走的自定义异常(可以用同一个类中的,也可以用不同类中的,不同类加fallbackClass 属性)

    @SentinelResource(value = "chello", blockHandler = "exceptionHandler2", defaultFallback = "fallback2")
    @GetMapping(value = "/hello")
    public String abcd(@RequestParam(value = "name") String name, int age){
        log.info("name={}",name);
        log.info("age={}",age);
        String content = nacosProviderClient.abc(name, age);
        return "我是feign: " + content;
    }

    public String exceptionHandler2(String name, int age, BlockException ex) {
        // Do some log here.
        ex.printStackTrace();
        return "Oops, error occurred at "+ name + ","+ age;
    }

    public String fallback2(Throwable e){
        log.info("进入sentinelResource注解测试,进入fallback2,参数b={}", e.getMessage());
        return "defaultFallback";
    }

最后CustomRequestOriginParser这个只要是给授权黑名单或ip(AuthorityRule)生效的,可以各微服务自定义,这里放到基础包只是演示

简单实现黑名单功能:

@Component
public class CustomRequestOriginParser implements RequestOriginParser {

    @Override
    public String parseOrigin(HttpServletRequest request) {
        // <X> 从 Header 中,获得请求来源
        String origin = request.getHeader("s-user");
        // <Y> 如果为空,给一个默认的
        if (StringUtils.isEmpty(origin)) {
            origin = "default";
        }
        return origin;
    }
}

简单实现ip限流功能:

@Component
public class CustomRequestOriginParser2 implements RequestOriginParser {

   @Override
   public String parseOrigin(HttpServletRequest request) {
       String ip = null;
       try {
           ip = Inet4Address.getLocalHost().getHostAddress();
       } catch (UnknownHostException e) {
           e.printStackTrace();
       }
       String u = request.getRemoteUser();
       return ip;
   }

}

微服务处理

上面基础包搭建完毕,各微服务只需引入上述基础包:

        <!-- sentinel自定义封装组件 -->
        <dependency>
            <groupId>com.myyshop.framework</groupId>
            <artifactId>sentinel-spring-boot-starter</artifactId>
            <version>1.0.0-RELEASE</version>
        </dependency>

整合Nacos持久化需要引入基础包(上面有)。配置则引入如下配置:

注意:下面配置的信息,只有server-addr中的地址修改成自己nacos-config的地址即可,其余信息都不需要动,尤其是groupId、dataId的格式(应用名-flow-rules、应用名-degrade-rules、应用名-system-rules、应用名-authority-rules、应用名-param-flow-rules)、rule-type不能修改,这个是与sentinel控制台源码中推送数据到nacos约定好的。

spring:
  cloud:
    sentinel:
      transport:
        dashboard: 192.168.128.36:8081  #主要改这里,其他基本不用动
      datasource:
        # 名称随意,流控数据规则配置
        flow:
          nacos:
            server-addr: ${spring.cloud.nacos.config.server-addr}
            dataId: ${spring.application.name}-flow-rules
            groupId: SENTINEL_GROUP
            # 规则类型,取值见:
            # org.springframework.cloud.alibaba.sentinel.datasource.RuleType
            rule-type: flow
            namespace: ${myyshop.sentinel.nacos.namespace}
        # 名称随意,流控数据规则配置
        degrade:
          nacos:
            server-addr: ${spring.cloud.nacos.config.server-addr}
            dataId: ${spring.application.name}-degrade-rules
            groupId: SENTINEL_GROUP
            rule-type: degrade
            namespace: ${myyshop.sentinel.nacos.namespace}
        # 名称随意,流控数据规则配置
        system:
          nacos:
            server-addr: ${spring.cloud.nacos.config.server-addr}
            dataId: ${spring.application.name}-system-rules
            groupId: SENTINEL_GROUP
            rule-type: system
            namespace: ${myyshop.sentinel.nacos.namespace}
        # 名称随意,流控数据规则配置
        authority:
          nacos:
            server-addr: ${spring.cloud.nacos.config.server-addr}
            dataId: ${spring.application.name}-authority-rules
            groupId: SENTINEL_GROUP
            rule-type: authority
            namespace: ${myyshop.sentinel.nacos.namespace}
        # 名称随意,流控数据规则配置
        param:
          nacos:
            server-addr: ${spring.cloud.nacos.config.server-addr}
            dataId: ${spring.application.name}-param-flow-rules
            groupId: SENTINEL_GROUP
            rule-type: param-flow
            namespace: ${myyshop.sentinel.nacos.namespace}
      eager: true   #控制台热加载,false为懒加载(第一次调用接口才会加载)

Sentinel控制台整合Nacos实现推拉

使用Sentinel Dashboard动态推、拉数据同步到Nacos。

个人改Alibaba重新封装控制台源码:(分支 lbj_release-1.7)

具体实现可以参考我整理的如下大佬们的文档:

网关限流(Spring Cloud Gateway)

网关限流文档(官方)

网关限流效果图1
网关限流效果图2

Sentinel提供针对Spring Cloud Gateway的参数开关,该开关是针对JVM -D(java -jar -Dproject.name=***)的开关,如果要开启Sentinel控制台对网关特定页面的开关,则需要配置如下(最主要是-Dcsp.sentinel.app.type=1):
-Dcsp.sentinel.dashboard.server=192.168.128.36:8081 -Dcsp.sentinel.app.type=1

如果application.yml中有如下配置,则不需要加入(
-Dcsp.sentinel.dashboard.server=192.168.128.36:8081)

spring:
  cloud:
    sentinel:
      transport:
        dashboard: 192.168.128.36:8081

添加上述jar包启动参数之后,需要加入如下pom包依赖(引包顺序要严格按照下面这样,测试换位置不生效,不知道为什么

       <!--sentinel gateway依赖包-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.csp</groupId>
            <artifactId>sentinel-datasource-nacos</artifactId>
        </dependency>

引入上述pom包依赖后,还需加入如下配置(主要是Nacos持久化配置):

spring:
  cloud:
    sentinel:
      transport:
        dashboard: 192.168.128.36:8081    #主要改这里,其他基本不用动
      datasource:
        gw-flow:
          nacos:
            server-addr: ${spring.cloud.nacos.config.server-addr}
            dataId: ${spring.application.name}-gw-flow-rules
            groupId: SENTINEL_GROUP
            rule-type: gw-flow
            namespace: ${myyshop.sentinel.nacos.namespace}
        gw-api-group:
          nacos:
            server-addr: ${spring.cloud.nacos.config.server-addr}
            dataId: ${spring.application.name}-gw-api-rules
            groupId: SENTINEL_GROUP
            rule-type: gw-api-group
            namespace: ${myyshop.sentinel.nacos.namespace}
        degrade:
          nacos:
            server-addr: ${spring.cloud.nacos.config.server-addr}
            dataId: ${spring.application.name}-degrade-rules
            groupId: SENTINEL_GROUP
            rule-type: degrade
            namespace: ${myyshop.sentinel.nacos.namespace}
      eager: true      #控制台热加载,false为懒加载(第一次调用接口才会加载)

网关需要加入配置(主要是异常相关格式化,还有代码限流、降级等)

@Configuration
public class SentinelGatewayConfig {

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

    public SentinelGatewayConfig(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(Ordered.HIGHEST_PRECEDENCE)
    public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
        return new JsonSentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
    }

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

/*****************************************************************************/

//以下是添加 API 分组和route 维度

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

    private void initCustomizedApis() {
        Set<ApiDefinition> definitions = new HashSet<>();
        ApiDefinition api1 = new ApiDefinition("some_customized_api")
                .setPredicateItems(new HashSet<ApiPredicateItem>() {{
                    add(new ApiPathPredicateItem().setPattern("/Hansen666666"));
//                    add(new ApiPathPredicateItem().setPattern("/gprovider/**")
//                            .setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX));
                }});
        definitions.add(api1);
        GatewayApiDefinitionManager.loadApiDefinitions(definitions);
    }


    /**
     * 配置限流规则
     */
    private void initGatewayRules() {
        Set<GatewayFlowRule> rules = new HashSet<>();

        rules.add(new GatewayFlowRule("some_customized_api")
                .setResourceMode(SentinelGatewayConstants.RESOURCE_MODE_CUSTOM_API_NAME)
                .setCount(2)
                .setIntervalSec(1)
        );

//        rules.add(new GatewayFlowRule("csdn")
//                .setCount(1)
//                .setIntervalSec(1)
//        );
//
//        rules.add(new GatewayFlowRule("gateway-provider")
//            .setCount(3) // 限流阈值
//            .setIntervalSec(1) // 统计时间窗口,单位是秒,默认是 1 秒
//        );

        GatewayRuleManager.loadRules(rules);
    }
}

添加自定义异常处理(不加会返回默认的错误信息Blocked by Sentinel: XXXX),下面为模仿源码写的自定义异常处理。
1.继承SentinelGatewayBlockExceptionHandler类(主要为了重写handle方法)
2.参照DefaultBlockRequestHandler类源码handleRequest方法和acceptsHtml方法(这个是SpringBoot默认返回的404异常页面,也就是说在网页上会有404异常,在Postman会显示Json异常)

public class JsonSentinelGatewayBlockExceptionHandler extends SentinelGatewayBlockExceptionHandler {

    public JsonSentinelGatewayBlockExceptionHandler(List<ViewResolver> viewResolvers, ServerCodecConfigurer serverCodecConfigurer) {
        super(viewResolvers, serverCodecConfigurer);
    }

    @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) -> {
                if(ex instanceof ParamFlowException){
                    return WebfluxResponseUtil.responseFailed(exchange, 10000, HttpStatus.TOO_MANY_REQUESTS.value(),
                            "API interface limited flow by gateway.");
                }
                if(ex instanceof DegradeException){
                    return WebfluxResponseUtil.responseFailed(exchange, 10001, HttpStatus.SERVICE_UNAVAILABLE.value(),
                            "API interface degraded by gateway.");
                }
                return WebfluxResponseUtil.responseFailed(exchange, 10000, HttpStatus.TOO_MANY_REQUESTS.value(),
                        "API interface limited flow by gateway.");
            });
        }
    }

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

Sentinel控制台

控制台文档(官方)
https://github.com/alibaba/Sentinel/wiki/%E6%8E%A7%E5%88%B6%E5%8F%B0

目前集成控制台有两种方式:

各自问题参照官方FAQ
https://github.com/alibaba/Sentinel/wiki/FAQ

image.png

Sentinel的JVM -D启动参数配置项,包含log文件指定路径(官方)
https://github.com/alibaba/Sentinel/wiki/%E5%90%AF%E5%8A%A8%E9%85%8D%E7%BD%AE%E9%A1%B9

控制台VM启动参数

下载jar或重新编译源码获取jar后,然后添加启动参数启动控制台,idea VM options启动参数如下:

-Dserver.port=8081
-Dcsp.sentinel.api.port=8723
-Dcsp.sentinel.dashboard.server=localhost:8081
-Dproject.name=sentinel-lbj
-Dserver.servlet.session.timeout=864000
-Dnacos.serverAddr=localhost:8849
-Dnacos.namespace=d4178075-92ee-429a-baad-6cd01d59b9b8

上面配置解释:

  • -Dserver.port 为控制台启动端口号(不配置默认8080)
  • -Dcsp.sentinel.api.port Sentinel 客户端监控微服务 API 的端口,默认8719
  • -Dcsp.sentinel.dashboard.server 默认控制台提供的监控(就是sentinel-dashboard的地址),此配置必须配置,如不配置,则没有sentinel-dashboard默认控制台
  • -Dproject.name 默认控制台名称,默认sentinel-dashboard
  • -Dserver.servlet.session.timeout 控制台登录过期时间,可以指定分钟,默认30分钟,如 7200 表示 7200 秒,60m 表示 60 分钟
  • -Dnacos.serverAddr 官方没有此配置,自己加的自定义配置,指定Nacos配置中心地址
  • -Dnacos.namespace 官方没有此配置,自己加的自定义配置,指定Nacos持久化命名空间,例如本项目命名空间是sentinel(d4178075-92ee-429a-baad-6cd01d59b9b8)

linux 或 windows 下启动

下载官方jar或拿到源码编译后得到jar,直接执行下面代码:

nohup 
  java -Dserver.port=8081 
          -Dcsp.sentinel.dashboard.server=localhost:8081
          -Dnacos.namespace=d4178075-92ee-429a-baad-6cd01d59b9b8 
          -Dnacos.serverAddr=localhost:8849 
          -Dsentinel.dashboard.auth.username=myyshop 
          -Dsentinel.dashboard.auth.password=myyshop 
-jar 
sentinel-dashboard.jar &

上述是Linux后台运行方式,加入 nohup **** & 就会后台运行,否则关闭服务就断了。Windows类似,不需要加 nohup **** &

Sentinel控制台部分源码(sentinel-dashboard)

以Nacos限流配置为例,FlowRuleNacosPublisher 发布配置到Nacos,发布的配置是JSON格式,但是JSON没有格式化,不美观,以下推送到Nacos实现美化JSON功能,这样容易改Nacos配置。

@Component("flowRuleNacosPublisher")
public class FlowRuleNacosPublisher implements DynamicRulePublisher<List<FlowRuleEntity>> {

    @Autowired
    private ConfigService configService;
    @Autowired
    private Converter<List<FlowRuleEntity>, String> converter;

    @Override
    public void publish(String app, List<FlowRuleEntity> rules) throws Exception {
        AssertUtil.notEmpty(app, "app name cannot be empty");
        if (rules == null) {
            return;
        }

        //lbj增加格式化、美化json内容 - start
        String content = converter.convert(rules);
        JSONArray jsonArray = JSONArray.parseArray(content);
        String prettyContent = JSON.toJSONString(jsonArray, SerializerFeature.PrettyFormat, SerializerFeature.WriteMapNullValue,
                SerializerFeature.WriteDateUseDateFormat);
        //lbj增加格式化、美化json内容 - end

        configService.publishConfig(app + NacosConfigUtil.FLOW_DATA_ID_POSTFIX,
            NacosConfigUtil.GROUP_ID, prettyContent);
    }
}

自己加入Nacos的 -Dnacos.serverAddr 和 -Dnacos.namespace 相关参数,这样可以直接在JVM -D启动参数动态加,而不用改源码,再编译,这样很麻烦:

    @Bean
    public ConfigService nacosConfigService() throws Exception {
        String namespace = System.getProperty("nacos.namespace");
        String serverAddr = Optional.ofNullable(System.getProperty("nacos.serverAddr")).orElse("localhost:8848");
        Properties properties = new Properties();
        properties.put(PropertyKeyConst.SERVER_ADDR, serverAddr);
        if(StringUtil.isNotBlank(namespace)){
            properties.setProperty(PropertyKeyConst.NAMESPACE, namespace);
        }
        return ConfigFactory.createConfigService(properties);

    }

上述配置在 NacosConfig 类中,通过System.getProperty获取启动参数,
获取 namespace 和 serverAddr

踩坑记录

如果你的微服务部署在docker中,并且没有指定host模式(--net=host),默认docker会走bridge模式,那这样docker镜像内部会分配自定义虚拟ip,这样跟主机互ping没有问题,但在一个局域网其他网段ip要想ping通docker内部虚拟ip就不行了。

而现在国内大部分公司都是使用docker去部署服务,Sentinel默认拿到ip是docker内部的虚拟ip,如果Sentinel部署在另外一台机器上(生产都要分开部署),则拿不到当前服务的簇点链路,因为Sentinel访问不到当前服务的端口数据(假如当前服务ip+端口=172.17.0.3:8719)。

注:172.17.0.3是docker内部虚拟ip,192.168.128.20是主机ip,8719是Sentinel客户端监控微服务 API 的端口,默认8719

host模式:使用 --net=host 指定。

none模式:使用 --net=none 指定。

bridge模式:使用 --net=bridge 指定,默认设置。

container模式:使用 --net=container:NAME_or_ID 指定。

因此有两种方案可以解决此情况:

  1. 将docker默认模式改成host模式(--net=host)
  2. 将Sentinel要监控的API端口(默认8719)从docker中绑定到宿主机上,可以使用 -p 参数显式将一个或者一组端口从容器里绑定到宿主机上(ip:hostPort:containerPort)

第一套方案不用说了,如果选定第二套方案,需要每个微服务都配置一个端口暴露给Sentinel,如果10个微服务,就暴露10个,不能重复,需要如下配置(这里以uaa为例):

spring:
  cloud:
    sentinel:
      transport:
        client-ip: 192.168.128.20   #如果是docker容器,需要指定宿主机ip,这样sentinel就不会拉docker内虚拟ip
        port: 8777    #指定Sentinel客户端监控微服务API的端口,

上述ip+端口配置好后(以uaa为例),通过docker run启动uaa服务(这里uaa暴露给sentinel的端口是8777,其他服务端口尽量不能重复),配置如下:

docker run -p 7001:7001 -p 8777:8777 --name ${app_name} \
--link registry2:registry2 \
-v /usr/local/server-log/uaa-server:/logs \
-d ${app_name}:latest

注:8777是Sentinel客户端监控 uaa的端口,不指定默认8719

推荐阅读更多精彩内容