纯净、安全、绿色的下载网站

首页|软件分类|下载排行|最新软件|IT学院

当前位置:首页IT学院IT技术

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

夏天的粽子   2021-09-11 我要评论
想了解配置gateway+nacos动态路由管理流程的相关内容吗夏天的粽子在本文为您仔细讲解gateway配置nacos动态路由的相关知识和一些Code实例欢迎阅读和指正我们先划重点:gateway配置,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但实际上能调用到吗???

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

<2> 前后端联调难

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

4. 其他问题

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

<1> 测试环境排查问题

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

<2> 本地开发

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

三. 智能路由的实现

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

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

三个团队team1team2和team3各负责不同的业务需求其中team1只需要改动A服务team2只需要改动B服务team3需要在qa环境上验证通过智能路由team1team2只在自己的环境上部署了增量应用然后在访问该环境的时候当找不到对应环境的服务时就从基准环境上访问而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发送请求比如CloseableHttpClientRestTemplate解决方案直接见代码:

//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:多应用支持:TomcatSpringbootGateway?

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

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

问题4:SpringBoot版本兼容问题

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

五. 带来的收益

1. 经济价值

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

2. 研发价值

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

3. 测试价值

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

六. 总结

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

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

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

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

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

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

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

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


相关文章

猜您喜欢

  • springcloud 集成nacos配置中心 spring cloud怎样集成nacos配置中心

    想了解spring cloud怎样集成nacos配置中心的相关内容吗Insist_on_progress在本文为您仔细讲解springcloud 集成nacos配置中心的相关知识和一些Code实例欢迎阅读和指正我们先划重点:spring,cloud集成nacos,nacos配置中心,springcloud,nacos下面大家一起来学习吧..
  • Java Pattern与Matcher字符串匹配 Java Pattern与Matcher字符串匹配案例详解

    想了解Java Pattern与Matcher字符串匹配案例详解的相关内容吗知行流浪在本文为您仔细讲解Java Pattern与Matcher字符串匹配的相关知识和一些Code实例欢迎阅读和指正我们先划重点:Java,Pattern与Matcher,Java,Pattern与Matcher字符匹配下面大家一起来学习吧..

网友评论

Copyright 2020 www.sopisoft.net 【绿软下载站】 版权所有 软件发布

声明:所有软件和文章来自软件开发商或者作者 如有异议 请与本站联系 点此查看联系方式