配置gateway+nacos动态路由管理流程

目录
  • 配置gateway+nacos动态路由
    • 第一步:首先是设置配置文件的配置列表
    • 第二步:配置监听nacos监听器
    • 第三步:配置nacos的yml文件
  • nacos的智能路由实现与应用
    • 一. 概述
    • 二. 遇到的问题
    • 三. 智能路由的实现
    • 四. 遇到的难点
    • 五. 带来的收益
    • 六. 总结

配置gateway+nacos动态路由

第一步:首先是设置配置文件的配置列表

然后在配置读取配置类上增加刷新注解@RefreshScope

import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.cloud.gateway.filter.FilterDefinition;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
 * @author :lhb
 * @date :Created in 2020-09-09 08:59
 * @description:GateWay路由配置
 * @modified By:
 * @version: $
 */
@Slf4j
@RefreshScope
@Component
@ConfigurationProperties(prefix = "spring.cloud.gateway")
public class GatewayRoutes {
    /**
     * 路由列表.
     */
    @NotNull
    @Valid
    private List<RouteDefinition> routes = new ArrayList<>();
    /**
     * 适用于每条路线的过滤器定义列表
     */
    private List<FilterDefinition> defaultFilters = new ArrayList<>();
    private List<MediaType> streamingMediaTypes = Arrays
            .asList(MediaType.TEXT_EVENT_STREAM, MediaType.APPLICATION_STREAM_JSON);
    public List<RouteDefinition> getRoutes() {
        return routes;
    }
    public void setRoutes(List<RouteDefinition> routes) {
        this.routes = routes;
        if (routes != null && routes.size() > 0 && log.isDebugEnabled()) {
            log.debug("Routes supplied from Gateway Properties: " + routes);
        }
    }
    public List<FilterDefinition> getDefaultFilters() {
        return defaultFilters;
    }
    public void setDefaultFilters(List<FilterDefinition> defaultFilters) {
        this.defaultFilters = defaultFilters;
    }
    public List<MediaType> getStreamingMediaTypes() {
        return streamingMediaTypes;
    }
    public void setStreamingMediaTypes(List<MediaType> streamingMediaTypes) {
        this.streamingMediaTypes = streamingMediaTypes;
    }
    @Override
    public String toString() {
        return "GatewayProperties{" + "routes=" + routes + ", defaultFilters="
                + defaultFilters + ", streamingMediaTypes=" + streamingMediaTypes + '}';
    }
}

第二步:配置监听nacos监听器

import cn.hutool.core.exceptions.ExceptionUtil;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.nacos.api.NacosFactory;
import com.alibaba.nacos.api.PropertyKeyConst;
import com.alibaba.nacos.api.config.ConfigService;
import com.alibaba.nacos.api.config.listener.Listener;
import com.alibaba.nacos.api.exception.NacosException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.MapUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.scope.refresh.RefreshScope;
import org.springframework.cloud.gateway.event.RefreshRoutesEvent;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionWriter;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
 * @author :lhb
 * @date :Created in 2020-09-08 16:39
 * @description:监听nacos配置变更
 * @modified By:
 * @version: $
 */
@Slf4j
@Component
public class GateWayNacosConfigListener implements ApplicationEventPublisherAware {
    @Autowired
    private RouteDefinitionWriter routedefinitionWriter;
    private ApplicationEventPublisher publisher;
    private static final Map<String, RouteDefinition> ROUTE_MAP = new ConcurrentHashMap<>();
    @Autowired
    private GatewayRoutes gatewayRoutes;
    @Resource
    private RefreshScope refreshScope;
    @Value(value = "${spring.cloud.nacos.config.server-addr}")
    private String serverAddr;
    @Value(value = "${spring.cloud.nacos.config.group:DEFAULT_GROUP}")
    private String group;
    @Value(value = "${spring.cloud.nacos.config.namespace}")
    private String namespace;
    private String routeDataId = "gateway-routes.yml";
    @PostConstruct
    public void onMessage() throws NacosException {
        log.info("serverAddr={}", serverAddr);
        Properties properties = new Properties();
        properties.put(PropertyKeyConst.SERVER_ADDR, serverAddr);
        properties.put(PropertyKeyConst.NAMESPACE, namespace);
        ConfigService configService = NacosFactory.createConfigService(properties);
        this.publisher(gatewayRoutes.getRoutes());
        log.info("gatewayProperties=" + JSONObject.toJSONString(gatewayRoutes));
        configService.addListener(routeDataId, group, new Listener() {
            @Override
            public Executor getExecutor() {
                return null;
            }
            @Override
            public void receiveConfigInfo(String config) {
                log.info("监听nacos配置: {}, 旧的配置: {}, 新的配置: {}", routeDataId, gatewayRoutes, config);
                refreshScope.refresh("gatewayRoutes");
                try {
                    TimeUnit.SECONDS.sleep(5);
                } catch (InterruptedException e) {
                    log.error(ExceptionUtil.getMessage(e));
                }
                publisher(gatewayRoutes.getRoutes());
            }
        });
    }
    private boolean rePut(List<RouteDefinition> routeDefinitions) {
        if (MapUtils.isEmpty(ROUTE_MAP) && CollectionUtils.isEmpty(routeDefinitions)) {
            return true;
        }
        if (CollectionUtils.isEmpty(routeDefinitions)) {
            return true;
        }
        Set<String> strings = ROUTE_MAP.keySet();
        return strings.stream().sorted().collect(Collectors.joining())
                .equals(routeDefinitions.stream().map(v -> v.getId()).sorted().collect(Collectors.joining()));
    }
    /**
     * 增加路由
     *
     * @param def
     * @return
     */
    public Boolean addRoute(RouteDefinition def) {
        try {
            log.info("添加路由: {} ", def);
            routedefinitionWriter.save(Mono.just(def)).subscribe();
            ROUTE_MAP.put(def.getId(), def);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return true;
    }
    /**
     * 删除路由
     *
     * @return
     */
    public Boolean clearRoute() {
        for (String id : ROUTE_MAP.keySet()) {
            routedefinitionWriter.delete(Mono.just(id)).subscribe();
        }
        ROUTE_MAP.clear();
        return false;
    }
    /**
     * 发布路由
     */
    private void publisher(String config) {
        this.clearRoute();
        try {
            log.info("重新更新动态路由");
            List<RouteDefinition> gateway = JSONObject.parseArray(config, RouteDefinition.class);
            for (RouteDefinition route : gateway) {
                this.addRoute(route);
            }
            publisher.publishEvent(new RefreshRoutesEvent(this.routedefinitionWriter));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    /**
     * 发布路由
     */
    private void publisher(List<RouteDefinition> routeDefinitions) {
        this.clearRoute();
        try {
            log.info("重新更新动态路由: ");
            for (RouteDefinition route : routeDefinitions) {
                this.addRoute(route);
            }
            publisher.publishEvent(new RefreshRoutesEvent(this.routedefinitionWriter));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher app) {
        publisher = app;
    }
}

第三步:配置nacos的yml文件

spring:
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true
          lower-case-service-id: true
      routes:
        # 认证中心
        - id: firefighting-service-user
          uri: lb://firefighting-service-user
          predicates:
            - Path=/user/**
          #          - Weight=group1, 8
          filters:
            - StripPrefix=1
          # 转流服务
        - id: liveStream
          uri: http://192.168.1.16:8081
          predicates:
            - Path=/liveStream/**
          #          - Weight=group1, 8
          filters:
            - StripPrefix=1
        - id: firefighting-service-directcenter
          uri: lb://firefighting-service-directcenter
          predicates:
            - Path=/directcenter/**
          filters:
            - StripPrefix=1
        - id: firefighting-service-datainput
          uri: lb://firefighting-service-datainput
          predicates:
            - Path=/datainput/**
          filters:
            - StripPrefix=1
        - id: firefighting-service-squadron
          uri: lb://firefighting-service-squadron
          predicates:
            - Path=/squadron/**
          filters:
            - StripPrefix=1
        - id: firefighting-service-iot
          uri: lb://firefighting-service-iot
          predicates:
            - Path=/iot/**
          filters:
            - StripPrefix=1
        - id: websocket
          uri: lb:ws://firefighting-service-notice
          predicates:
            - Path=/notice/socket/**
          filters:
            - StripPrefix=1
        - id: firefighting-service-notice
          uri: lb://firefighting-service-notice
          predicates:
            - Path=/notice/**
          filters:
            # 验证码处理
            #            - CacheRequest
            #            - ImgCodeFilter
            - StripPrefix=1
        - id: websocket
          uri: lb:ws://firefighting-service-notice
          predicates:
            - Path=/notice/socket/**
          filters:
            - StripPrefix=1
        - id: firefighting-service-supervise
          uri: lb://firefighting-service-supervise
          predicates:
            - Path=/supervise/**
          filters:
            - StripPrefix=1
        - id: firefighting-service-new-supervise
          uri: lb://firefighting-service-new-supervise
          predicates:
            - Path=/new/supervise/**
          filters:
            - StripPrefix=2
        - id: firefighting-service-train
          uri: lb://firefighting-service-train
          predicates:
            - Path=/train/**
          filters:
            - StripPrefix=1
        - id: firefighting-support-user
          uri: lb://firefighting-support-user
          predicates:
            - Path=/support/**
          filters:
            - StripPrefix=1
        - id: firefighting-service-firesafety
          uri: lb://firefighting-service-firesafety
          predicates:
            - Path=/firesafety/**
          filters:
            - StripPrefix=1
        - id: firefighting-service-bigdata
          uri: lb://firefighting-service-bigdata
          predicates:
            - Path=/bigdata/**
          filters:
            - StripPrefix=1
        - id: firefighting-service-act-datainput
          uri: lb://firefighting-service-act-datainput
          predicates:
            - Path=/act_datainput/**
          filters:
            - StripPrefix=1
        - id: firefighting-service-publicity
          uri: lb://firefighting-service-publicity
          predicates:
            - Path=/publicity/**
          filters:
            - StripPrefix=1
        - id: firefighting-service-preplan
          uri: lb://firefighting-service-preplan
          predicates:
            - Path=/preplan/**
          filters:
            - StripPrefix=1
        - id: firefighting-service-uav
          uri: lb://firefighting-service-uav
          predicates:
            - Path=/uav/**
          filters:
            - StripPrefix=1
        - id: firefighting-service-ard-mgr
          uri: lb://firefighting-service-ard-mgr
          predicates:
            - Path=/ard_mgr/**
          filters:
            - StripPrefix=1
        - id: admin-server
          uri: lb://admin-server
          predicates:
            - Path=/adminsServer/**
          filters:
            - StripPrefix=1

nacos的智能路由实现与应用

一. 概述

随着微服务的兴起,我司也逐渐加入了微服务的改造浪潮中。但是,随着微服务体系的发展壮大,越来越多的问题暴露出来。其中,测试环境治理,一直是实施微服务的痛点之一,它的痛主要体现在环境管理困难,应用部署困难,技术方案配合等。最终,基于我司的实际情况,基于注册中心nacos实现的智能路由有效地解决了这个问题。本文主要介绍我司在测试环境治理方面遇到的难题与对应的解决方案。

二. 遇到的问题

1. 困难的环境管理与应用部署

随着公司业务发展,业务逐渐复杂化。这在微服务下带来的一个问题就是服务的不断激增,且增速越来越快。而在这种情况下,不同业务团队如果想并行开发的话,都需要一个环境。假设一套完整环境需要部署1k个服务,那么n个团队就需要部署n*1k个服务,这显然是不能接受的。

2. 缺失的技术方案

从上面的分析可以看出,一个环境部署全量的服务显然是灾难性的。那么就需要有一种技术方案,来解决应用部署的难题。最直接的一个想法就是,每个环境只部署修改的服务,然后通过某种方式,实现该环境的正常使用。当然,这也是我们最终的解决方案。下面会做一个介绍。

3. 研发问题

除了这两个大的问题,还有一些其他的问题也急需解决。包括后端多版本并行联调和前后端联调的难题。下面以实际的例子来说明这两个问题。

<1> 后端多版本并行联调难

某一个微服务,有多个版本并行开发时,后端联调时调用容易错乱。例如这个例子,1.1版本的服务A需要调用1.1版本的服务B,但实际上能调用到吗???

目前使用的配置注册中心是nacos,nacos自身有一套自己的服务发现体系,但这是基于namespace和group的同频服务发现,对于跨namespace的服务,它就不管用了。

<2> 前后端联调难

前端和后端的联调困难,这个问题也是经常遇到的。主要体现在,后端联调往往需要启动多个微服务(因为服务的依赖性)。而前端要对应到某一个后端,也需要特殊配置(比如指定ip等)。下面这个例子,后端人员开发服务A,但却要启动4个服务才能联调。因为服务A依赖于服务B,C,D。

4. 其他问题

除了以上的问题外,还有一些小的问题也可以关注下:

<1> 测试环境排查问题

这个问题不算棘手,登录服务器查看日志即可。但是能否再灵活点,比如让开发人员debug或者在本地调试问题呢?

<2> 本地开发

本地开发,后端往往也依赖多个服务,能不能只启动待开发的服务,而不启动其他旁路服务呢?

三. 智能路由的实现

基于这些需求,智能路由应运而生。它正是为了解决这个问题。最终,我们通过它解决了测试环境治理的难题。

智能路由,能根据不同环境,不同用户,甚至不同机器进行精确路由。下面以一个例子说明。

三个团队,team1,team2和team3各负责不同的业务需求。其中team1只需要改动A服务,team2只需要改动B服务,team3需要在qa环境上验证。通过智能路由,team1,team2只在自己的环境上部署了增量应用,然后在访问该环境的时候,当找不到对应环境的服务时,就从基准环境上访问。而team3只做qa,因此直接访问基准环境即可。可以看到,基准环境上部署了全量服务,除此之外,其他小环境都是增量服务。

下面介绍智能路由的具体实现方案。

1. 原理

通过上图,可以看到,智能路由其实就是流量染色加上服务发现

流量染色:将不同团队的流量进行染色,然后透传到整个链路中。

服务发现:注册中心提供正确的服务发现,当在本环境发现不到待调用的服务时,自动访问基准环境的服务。

通过流量染色,区分出哪些流量是哪些团队的,从而在服务发现时,能正确调用到正确的服务。

另外,我司使用的注册中心是nacos,因此本文将主要介绍基于nacos的智能路由实现,其他注册中心同理,做相应改造即可。

2. 具体实现 <1> 流量染色

智能路由的第一步,就是要做流量的染色,将流量能够沿着链路一直透传下去。那么就需要找到流量的入口,然后在入口处进行染色。下图是网站的内部调用情况,可以看到,流量从Nginx进来,最终打到服务集群,因此需要对Nginx进行染色。

流量染色主要是在流量的头部加上一些标记,以便识别。这里利用了Nginx的proxy_set_header,我们通过下列方式来设置头部。

## nginx的匹配规则设置header
proxy_set_header req_context  "{'version': '1.0'}"

这样子,我们就为版本是1.0的流量设置了头部,其中的参数可以任意添加,这里仅列举最重要的一个参数。

另外,还有一个问题,流量只有从Nginx进来,才会带上这个头部。如果是在内部直接访问某个中间的服务,那么这个时候流量是没有头部的。对此,我们的解决方案是filter,通过filter可以动态地拦截请求,修改请求头部,为其初始化一个默认值。

public class FlowDyeFilter implements Filter {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        //1. 获取servletrequest
        HttpServletRequest request = (HttpServletRequest)servletRequest;
        //2. 获取请求头部
        String context = request.getHeader(ContextUtil.REQUEST_CONTEXT);
        //3. 初始化请求上下文,如果没有,就进行初始化
        initContext(context);
        try {
            filterChain.doFilter(servletRequest, servletResponse);
        } finally {
            ContextUtil.clear();
        }
    }

    public void initContext(String contextStr) {
        //json转object
        Context context = JSONObject.parseObject(contextStr, GlobalContext.class);
        //避免假初始化
        if (context == null) {
            context  = new Context();
        }
        //这里进行初始化,如果没值,设置一个默认值
        if (StringUtils.isEmpty(context.getVersion())) {
            context.setVersion("master");
        }
        ...
        //存储到上下文中
        ContextUtil.setCurrentContext(context);
    }
}
 

通过这个filter,保证了在中间环节访问时,流量仍然被染色。

<2> 流量透传

流量在入口被染色后,需要透传到整个链路,因此需要对服务做一些处理,下面分几种情形分别处理。

1~ Spring Cloud Gateway

对于Gateway,保证请求在转发过程中的header不丢,这个是必要的。这里通过Gateway自带的GlobalFilter来实现,代码如下:

public class WebfluxFlowDyeFilter implements GlobalFilter, Ordered {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //1. 获取请求上下文
        String context = exchange.getRequest().getHeaders().getFirst(ContextUtil.REQUEST_CONTEXT);
        //2. 构造ServerHttpRequest
        ServerHttpRequest serverHttpRequest = exchange.getRequest().mutate().header(ContextUtil.REQUEST_CONTEXT, context).build();
        //3. 构造ServerWebExchange
        ServerWebExchange serverWebExchange = exchange.mutate().request(serverHttpRequest).build();
        return chain.filter(serverWebExchange).then(
            Mono.fromRunnable( () -> {
                ContextUtil.clear();
            })
        );
    }
}

2~ SpringCloud:Feign

这一类也是最常用的,SC的服务集群都通过Feign进行交互,因此只需要配置Feign透传即可。在这里,我们利用了Feign自带的RequestInterceptor,实现请求拦截。代码如下:

@Configuration
public class FeignAutoConfiguration {
    @Bean
    public RequestInterceptor headerInterceptor() {
        return requestTemplate -> {
            setRequestContext(requestTemplate);
        };
    }

    private void setRequestContext(RequestTemplate requestTemplate) {
        Context context = ContextUtil.getCurrentContext();
        if (context != null) {
            requestTemplate.header(ContextUtil.REQUEST_CONTEXT, JSON.toJSONString(ContextUtil.getCurrentContext()));
        }
    }
}

3~ HTTP

最后一类,也是用得最少的一类,即直接通过HTTP发送请求。比如CloseableHttpClient,RestTemplate。解决方案直接见代码:

//RestTemplate
HttpHeaders headers = new HttpHeaders();
headers.set(ContextUtil.REQUEST_CONTEXT,JSONObject.toJSONString(ContextUtil.getCurrentContext()));

//CloseableHttpClient
HttpGet httpGet = new HttpGet(uri);
httpGet.setHeader(ContextUtil.REQUEST_CONTEXT,JSONObject.toJSONString(ContextUtil.getCurrentContext()));

只需要粗暴地在发送头部中增加header即可,而其请求上下文直接通过当前线程上下文获取即可。

<3> 配置负载规则

完成了流量染色,下面就差服务发现了。服务发现基于注册中心nacos,因此需要修改负载规则。在这里,我们配置Ribbon的负载规则,修改为自定义负载均衡器NacosWeightLoadBalancerRule。


    @Bean
    @Scope("prototype")
    public IRule getRibbonRule() {
        return new NacosWeightLoadBalancerRule();
    }
public class NacosWeightLoadBalancerRule extends AbstractLoadBalancerRule {
    @Override
    public Server choose(Object o) {
        DynamicServerListLoadBalancer loadBalancer = (DynamicServerListLoadBalancer) getLoadBalancer();
        String name = loadBalancer.getName();
        try {
            Instance instance = nacosNamingService.getInstance(name);
            return new NacosServer(instance);
        } catch (NacosException ee) {
            log.error("请求服务异常!异常信息:{}", ee);
        } catch (Exception e) {
            log.error("请求服务异常!异常信息:{}", e);
        }
        return null;
    }
}

从代码中可以看到,最终通过nacosNamingService.getInstance()方法获取实例。

<4> 配置智能路由规则

上面的负载规则,最终调用的是nacosNamingService.getInstance()方法,该方法里面定义了智能路由规则,主要功能是根据流量进行服务精准匹配。

规则如下:

1~开关判断:是否开启路由功能,没有则走nacos默认路由。

2~获取实例:根据流量,获取对应实例。其中,路由匹配按照一定的优先级进行匹配。

路由规则:IP优先 > 环境 + 组 > 环境 + 默认组

解释一下这个规则,首先是获取实例,需要先获取nacos上面的所有可用实例,然后遍历,从中选出一个最合适的实例。

然后IP优先的含义是,如果在本地调试服务,那么从本地直接访问网站,请求就会优先访问本地服务,那么就便于开发人员调试了,debug,本地开发都不是问题了!

其实是,环境 + 组,这个规则代表了如果存在对应的环境和组都相同的服务,那作为最符合的实例肯定优先返回,其实是环境 + 默认组,最后如果都没有,就访问基准环境(master)。

注:环境和组的概念对应nacos上的namespace和group,如有不懂,请自行查看nacos官方文档。

最终代码如下:

public class NacosNamingService {
    public Instance getInstance(String serviceName, String groupName) throws NacosException {
        //1. 判断智能路由开关是否开启,没有走默认路由
        if (!isEnable()) {
            return discoveryProperties.namingServiceInstance().selectOneHealthyInstance(serviceName, groupName);
        }

        Context context = ContextUtil.getCurrentContext();
        if (Context == null) {
            return NacosNamingFactory.getNamingService(CommonConstant.Env.MASTER).selectOneHealthyInstance(serviceName);
        }
        //2. 获取实例
        return getInstance(serviceName, context);
    }

    public Instance getInstance(String serviceName, Context context) throws NacosException {
        Instance envAndGroupInstance = null;
        Instance envDefGroupInstance = null;
        Instance defaultInstance = null;
        //2.1 获取所有可以调用的命名空间
        List<Namespace> namespaces = NacosNamingFactory.getNamespaces();
        for (Namespace namespace : namespaces) {
            String thisEnvName = namespace.getNamespace();
            NamingService namingService = NacosNamingFactory.getNamingService(thisEnvName);
            List<Instance> instances = new ArrayList<>();
            List<Instance> instances1 = namingService.selectInstances(serviceName, true);
            List<Instance> instances2 = namingService.selectInstances(serviceName, groupName, true);
            instances.addAll(instances1);
            instances.addAll(instances2);
            //2.2 路由匹配
            for (Instance instance : instances) {
                // 优先本机匹配
                if (instance.getIp().equals(clientIp)) {
                    return instance;
                }
                String thisGroupName = null;
                String thisServiceName = instance.getServiceName();
                if (thisServiceName != null && thisServiceName.split("@@") != null) {
                    thisGroupName = thisServiceName.split("@@")[0];
                }
                if (thisEnvName.equals(envName) && thisGroupName.equals(groupName)) {
                    envAndGroupInstance = instance;
                }
                if (thisEnvName.equals(envName) && thisGroupName.equals(CommonConstant.DEFAULT_GROUP)) {
                    envDefGroupInstance = instance;
                }
                if (thisEnvName.equals(CommonConstant.Env.MASTER) && thisGroupName.equals(CommonConstant.DEFAULT_GROUP)) {
                    defaultInstance = instance;
                }
            }
        }
        if (envAndGroupInstance != null) {
            return envAndGroupInstance;
        }
        if (envDefGroupInstance != null) {
            return envDefGroupInstance;
        }
        return defaultInstance;
    }

    @Autowired
    private NacosDiscoveryProperties discoveryProperties;
}

<5> 配置智能路由定时任务

刚才在介绍智能路由的匹配规则时,提到“获取所有可以调用的命名空间”。这是因为,nacos上可能有很多个命名空间namespace,而我们需要把所有namespace上的所有可用服务都获取到,而nacos源码中,一个namespace对应一个NamingService。因此我们需要定时获取nacos上所有的NamingService,存储到本地,再通过NamingService去获取实例。因此我们的做法是,配置一个监听器,定期监听nacos上的namespace变化,然后定期更新,维护到服务的内部缓存中。代码如下:

Slf4j
@Configuration
@ConditionalOnRouteEnabled
public class RouteAutoConfiguration {

    @Autowired(required = false)
    private RouteProperties routeProperties;

    @PostConstruct
    public void init() {
        log.info("初始化智能路由!");
        NacosNamingFactory.initNamespace();
        addListener();
    }

    private void addListener() {
        int period = routeProperties.getPeriod();
        NacosExecutorService nacosExecutorService = new NacosExecutorService("namespace-listener");
        nacosExecutorService.execute(period);
    }
}
    public static void initNamespace() {
        ApplicationContext applicationContext = SpringContextUtil.getContext();
        if (applicationContext == null) {
            return;
        }
        String serverAddr = applicationContext.getEnvironment().getProperty("spring.cloud.nacos.discovery.server-addr");
        if (serverAddr == null) {
            throw new RuntimeException("nacos地址为空!");
        }
        String url = serverAddr + "/nacos/v1/console/namespaces?namespaceId=";
        RestResult<String> restResult = HttpUtil.doGetJson(url, RestResult.class);
        List<Namespace> namespaces = JSON.parseArray(JSONObject.toJSONString(restResult.getData()), Namespace.class);;
        NacosNamingFactory.setNamespaces(namespaces);
    }
public class NacosExecutorService {
    public void execute(int period) {
        executorService.scheduleWithFixedDelay(new NacosWorker(), 5, period, TimeUnit.SECONDS);
    }
    public NacosExecutorService(String name) {
        executorService = Executors.newScheduledThreadPool(Runtime.getRuntime().availableProcessors(), new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                Thread t = new Thread(r);
                t.setName("jdh-system-" + name);
                t.setDaemon(true);
                return t;
            }
        });
    }
    final ScheduledExecutorService executorService;
}

四. 遇到的难点

下面是智能路由实现过程,遇到的一些问题及解决方案。

问题1namespace启动了太多线程,导致线程数过大?

因为服务需要维护过多的namespace,每个namespace内部又启动多个线程维护服务实例信息,导致服务总线程数过大。

解决方案每个namespace设置只启动2个线程,通过下列参数设置:

properties.setProperty(PropertyKeyConst.NAMING_CLIENT_BEAT_THREAD_COUNT, "1");
properties.setProperty(PropertyKeyConst.NAMING_POLLING_THREAD_COUNT, "1");

问题2支持一个线程调用多个服务?

每个请求都会创建一个线程,这个线程可能会调用多次其他服务。

解决方案既然调用多次,那就创建上下文,并保持上下文,调用结束后再清除。见代码:

try {
    filterChain.doFilter(servletRequest, servletResponse);
} finally {
    ContextUtil.clear();
}

问题3:多应用支持:Tomcat,Springboot,Gateway?

我们内部有多种框架,如何保证这些不同框架服务的支持?

解决方案针对不同应用,开发不同的starter包。

问题4:SpringBoot版本兼容问题

解决方案:针对1.x和2.x单独开发starter包。

五. 带来的收益

1. 经济价值

同样的资源,多部署了n套环境,极大提高资源利用率。(毕竟增量环境和全量环境的代价还是相差很大的)

2. 研发价值

本地开发排查测试问题方便,极大提高研发效率。前面提到的IP优先规则,保证了这一点。本地请求总是最优先打到本地上。

3. 测试价值

多部署n套环境,支持更多版本,提高测试效率。同时只需要部署增量应用,也提高部署效率。

六. 总结

通过智能路由,我司实现了部署成本大幅减少,部署效率大幅提高,研发测试效率大幅提高。

最后总结下智能路由的主要功能:

1. 多环境管理:支持多环境路由,除了基准环境外,其他环境只部署增量应用。

2. 多用户支持:支持多用户公用一套环境,避免开发不同版本造成的冲突。

3. 前端研发路由:对前端研发人员,可以方便快捷地同一个任意后端人员对接。

4. 后端研发路由:对后端研发人员,无论什么环境都可以快速调试,快速发现问题。

5. 友好且兼容:对微服务无侵入性,且支持 Web、WebFlux、Tomcat等应用。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持我们。

(0)

相关推荐

  • Spring Cloud Gateway + Nacos 实现动态路由

    本节开始介绍 SpringCloud Gateway 中动态路由的实现方法,包括: Nacos 集成动态路由配置,更新配置文件即自动更新路由 MySQL + 二级缓存实现,主要基于 Gateway 的一些特性进行重写,实现路由信息的自动更新 这篇文章主要介绍第一种方式:将配置文件放到 Nacos 进行托管,网关服务通过引入 Nacos 而自动更新路由配置信息.实现较为简单. 本节代码在:https://github.com/laolunsi/spring-boot-examples,参考例 23

  • spring-cloud-gateway动态路由的实现方法

    概述 线上项目发布一般有以下几种方案: 机发布 蓝绿部署 滚动部署 灰度发布 停机发布 这种发布一般在夜里或者进行大版本升级的时候发布,因为需要停机,所以现在大家都在研究 Devops 方案. 蓝绿部署 需要准备两个相同的环境.一个环境新版本,一个环境旧版本,通过负载均衡进行切换与回滚,目的是为了减少服务停止时间. 滚动部署 就是在升级过程中,并不一下子启动所有新版本,是先启动一台新版本,再停止一台老版本,然后再启动一台新版本,再停止一台老版本,直到升级完成.基于 k8s 的升级方案默认就是滚动

  • Nacos+Spring Cloud Gateway动态路由配置实现步骤

    目录 前言 一.Nacos环境准备 1.启动Nacos配置中心并创建路由配置 2.连接Nacos配置中心 二.项目构建 1.项目结构 2.编写测试代码 三.测试动态网关配置 1.启动服务,观察注册中心 2.访问网关,观察服务日志 四.总结 前言 Nacos最近项目一直在使用,其简单灵活,支持更细粒度的命令空间,分组等为麻烦复杂的环境切换提供了方便:同时也很好支持动态路由的配置,只需要简单的几步即可.在国产的注册中心.配置中心中比较突出,容易上手,本文通过gateway.nacos-consume

  • 基于Nacos实现Spring Cloud Gateway实现动态路由的方法

    简介 该文档主要介绍以Nacos为配置中心,实现Spring Cloud GateWay 实现动态路由的功能.Spring Cloud Gateway启动时候,就将路由配置和规则加载到内存里,无法做到不重启网关就可以动态的对应路由的配置和规则进行增加,修改和删除.通过nacos的配置下发的功能可以实现在不重启网关的情况下,实现动态路由. 集成 Spring Cloud GateWay集成 spring-cloud-starter-gateway:路由转发.请求过滤(权限校验.限流以及监控等) s

  • 配置gateway+nacos动态路由管理流程

    目录 配置gateway+nacos动态路由 第一步:首先是设置配置文件的配置列表 第二步:配置监听nacos监听器 第三步:配置nacos的yml文件 nacos的智能路由实现与应用 一. 概述 二. 遇到的问题 三. 智能路由的实现 四. 遇到的难点 五. 带来的收益 六. 总结 配置gateway+nacos动态路由 第一步:首先是设置配置文件的配置列表 然后在配置读取配置类上增加刷新注解@RefreshScope import lombok.extern.slf4j.Slf4j; imp

  • 关于springboot中nacos动态路由的配置

    目录 nacos动态路由的配置 1.作为一个动态路由维护管理的类 2.基于Nacos动态配置路由服务 3.yml配置 4. nacos网关配置 5.最后:我建的是 Springboot配置Nacos出现的问题 报错信息 具体如下 nacos动态路由的配置 什么都不说了,springboot-nacos 不懂得的下面自行学习啦我直接贴下代码! 首先... 我自己有个服务器.在无聊之时写的代码,主要是通过网关来调用接口所以有了下面的代码. 1.作为一个动态路由维护管理的类 @Service publ

  • SpringCloud Gateway 利用 Mysql 实现动态路由的方法

    需求描述 标准网关动态路由功能是重要的一环,将路由.断言以及过滤器信息,持久化到 Mysql 中,通过配置后台页面实现路由.断言.以及过滤器等配置的增删改查. Spring Cloud Gateway 路由及黑白名单实现背景 Spring Cloud 路由API Spring Cloud Gateway 通过定义 RouteDefinitionRepository 来实现动态路由. //保存路由缓存 public interface RouteDefinitionWriter { Mono<Vo

  • GateWay路由规则与动态路由详细介绍

    目录 1.路由规则 2.动态路由 1.路由规则 Spring Cloud GateWay 帮我们内置了很多 Predicates功能,实现了各种路由匹配规则(通过 Header.请求参数等作为条件)匹配到对应的路由. 时间点后匹配 spring:    cloud:        gateway:            routes:                - id: after_route                    uri: https://example.org     

  • 详解Spring Cloud Gateway 数据库存储路由信息的扩展方案

    动态路由背景 ​ 无论你在使用Zuul还是Spring Cloud Gateway 的时候,官方文档提供的方案总是基于配置文件配置的方式 例如: # zuul 的配置形式 routes: pig-auth: path: /auth/** serviceId: pig-auth stripPrefix: true # gateway 的配置形式 routes: - id: pigx-auth uri: lb://pigx-auth predicates: - Path=/auth/** filte

  • Vue 动态路由的实现详情

    前言: 动态路由是一个常用的功能,根据后台返回的路由json表,前端动态显示可跳转的路由项,下面来讲解一下具体的实现方式: 大致业务需求:前端在login登陆页,输入账号密码后,点击登陆,接口返回登陆成功的同时,也会把该用户可操作的路由链表返回给前端(JSON数组格式),前端拿到这个JSON数组,来渲染成侧边栏列表(说的是PC端),我们需要将所有的页面都写好,然后去匹配后台返回的部分路由来进行展示(比如项目一共有100个页面,后台返回了10个路由地址,那么我们只展示返回的对应的10个即可) 说一

  • nacos gateway动态路由实战

    目录 nacos gateway动态路由 一.引入本次测试需要的pom依赖 二.配置文件设置(*配置文件不生效的把名字改为bootstrap.yml) 三.动态路由实现 四.nacos配置中心设置 五.测试 gateway网关相关配置 在启动类中添加注解@EeableDiscoveryClient 配置nacos注册中心地址 bootstrap.properties 填写配置中心地址 nacos里创建命名空间gateway 在主类中屏蔽数据源 在项目里创建application.yml naco

随机推荐