您好,登錄后才能下訂單哦!
這篇文章主要講解了“配置gateway+nacos動(dòng)態(tài)路由管理的詳細(xì)流程”,文中的講解內(nèi)容簡單清晰,易于學(xué)習(xí)與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學(xué)習(xí)“配置gateway+nacos動(dòng)態(tài)路由管理的詳細(xì)流程”吧!
配置gateway+nacos動(dòng)態(tài)路由
第一步:首先是設(shè)置配置文件的配置列表
第二步:配置監(jiān)聽nacos監(jiān)聽器
第三步:配置nacos的yml文件
nacos的智能路由實(shí)現(xiàn)與應(yīng)用
一. 概述
二. 遇到的問題
三. 智能路由的實(shí)現(xiàn)
四. 遇到的難點(diǎn)
五. 帶來的收益
六. 總結(jié)
然后在配置讀取配置類上增加刷新注解@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 + '}'; } }
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:監(jiān)聽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("監(jiān)聽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; } /** * 發(fā)布路由 */ private void publisher(String config) { this.clearRoute(); try { log.info("重新更新動(dòng)態(tài)路由"); 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(); } } /** * 發(fā)布路由 */ private void publisher(List<RouteDefinition> routeDefinitions) { this.clearRoute(); try { log.info("重新更新動(dòng)態(tài)路由: "); 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; } }
spring: cloud: gateway: discovery: locator: enabled: true lower-case-service-id: true routes: # 認(rèn)證中心 - id: firefighting-service-user uri: lb://firefighting-service-user predicates: - Path=/user/** # - Weight=group1, 8 filters: - StripPrefix=1 # 轉(zhuǎn)流服務(wù) - 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: # 驗(yàn)證碼處理 # - 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
隨著微服務(wù)的興起,我司也逐漸加入了微服務(wù)的改造浪潮中。但是,隨著微服務(wù)體系的發(fā)展壯大,越來越多的問題暴露出來。其中,測試環(huán)境治理,一直是實(shí)施微服務(wù)的痛點(diǎn)之一,它的痛主要體現(xiàn)在環(huán)境管理困難,應(yīng)用部署困難,技術(shù)方案配合等。最終,基于我司的實(shí)際情況,基于注冊中心nacos實(shí)現(xiàn)的智能路由有效地解決了這個(gè)問題。本文主要介紹我司在測試環(huán)境治理方面遇到的難題與對(duì)應(yīng)的解決方案。
1. 困難的環(huán)境管理與應(yīng)用部署
隨著公司業(yè)務(wù)發(fā)展,業(yè)務(wù)逐漸復(fù)雜化。這在微服務(wù)下帶來的一個(gè)問題就是服務(wù)的不斷激增,且增速越來越快。而在這種情況下,不同業(yè)務(wù)團(tuán)隊(duì)如果想并行開發(fā)的話,都需要一個(gè)環(huán)境。假設(shè)一套完整環(huán)境需要部署1k個(gè)服務(wù),那么n個(gè)團(tuán)隊(duì)就需要部署n*1k個(gè)服務(wù),這顯然是不能接受的。
2. 缺失的技術(shù)方案
從上面的分析可以看出,一個(gè)環(huán)境部署全量的服務(wù)顯然是災(zāi)難性的。那么就需要有一種技術(shù)方案,來解決應(yīng)用部署的難題。最直接的一個(gè)想法就是,每個(gè)環(huán)境只部署修改的服務(wù),然后通過某種方式,實(shí)現(xiàn)該環(huán)境的正常使用。當(dāng)然,這也是我們最終的解決方案。下面會(huì)做一個(gè)介紹。
3. 研發(fā)問題
除了這兩個(gè)大的問題,還有一些其他的問題也急需解決。包括后端多版本并行聯(lián)調(diào)和前后端聯(lián)調(diào)的難題。下面以實(shí)際的例子來說明這兩個(gè)問題。
<1> 后端多版本并行聯(lián)調(diào)難
某一個(gè)微服務(wù),有多個(gè)版本并行開發(fā)時(shí),后端聯(lián)調(diào)時(shí)調(diào)用容易錯(cuò)亂。例如這個(gè)例子,1.1版本的服務(wù)A需要調(diào)用1.1版本的服務(wù)B,但實(shí)際上能調(diào)用到嗎???
目前使用的配置注冊中心是nacos,nacos自身有一套自己的服務(wù)發(fā)現(xiàn)體系,但這是基于namespace和group的同頻服務(wù)發(fā)現(xiàn),對(duì)于跨namespace的服務(wù),它就不管用了。
<2> 前后端聯(lián)調(diào)難
前端和后端的聯(lián)調(diào)困難,這個(gè)問題也是經(jīng)常遇到的。主要體現(xiàn)在,后端聯(lián)調(diào)往往需要啟動(dòng)多個(gè)微服務(wù)(因?yàn)榉?wù)的依賴性)。而前端要對(duì)應(yīng)到某一個(gè)后端,也需要特殊配置(比如指定ip等)。下面這個(gè)例子,后端人員開發(fā)服務(wù)A,但卻要啟動(dòng)4個(gè)服務(wù)才能聯(lián)調(diào)。因?yàn)榉?wù)A依賴于服務(wù)B,C,D。
4. 其他問題
除了以上的問題外,還有一些小的問題也可以關(guān)注下:
<1> 測試環(huán)境排查問題
這個(gè)問題不算棘手,登錄服務(wù)器查看日志即可。但是能否再靈活點(diǎn),比如讓開發(fā)人員debug或者在本地調(diào)試問題呢?
<2> 本地開發(fā)
本地開發(fā),后端往往也依賴多個(gè)服務(wù),能不能只啟動(dòng)待開發(fā)的服務(wù),而不啟動(dòng)其他旁路服務(wù)呢?
基于這些需求,智能路由應(yīng)運(yùn)而生。它正是為了解決這個(gè)問題。最終,我們通過它解決了測試環(huán)境治理的難題。
智能路由,能根據(jù)不同環(huán)境,不同用戶,甚至不同機(jī)器進(jìn)行精確路由。下面以一個(gè)例子說明。
三個(gè)團(tuán)隊(duì),team1,team2和team3各負(fù)責(zé)不同的業(yè)務(wù)需求。其中team1只需要改動(dòng)A服務(wù),team2只需要改動(dòng)B服務(wù),team3需要在qa環(huán)境上驗(yàn)證。通過智能路由,team1,team2只在自己的環(huán)境上部署了增量應(yīng)用,然后在訪問該環(huán)境的時(shí)候,當(dāng)找不到對(duì)應(yīng)環(huán)境的服務(wù)時(shí),就從基準(zhǔn)環(huán)境上訪問。而team3只做qa,因此直接訪問基準(zhǔn)環(huán)境即可??梢钥吹剑鶞?zhǔn)環(huán)境上部署了全量服務(wù),除此之外,其他小環(huán)境都是增量服務(wù)。
下面介紹智能路由的具體實(shí)現(xiàn)方案。
1. 原理
通過上圖,可以看到,智能路由其實(shí)就是流量染色加上服務(wù)發(fā)現(xiàn)。
流量染色:將不同團(tuán)隊(duì)的流量進(jìn)行染色,然后透傳到整個(gè)鏈路中。
服務(wù)發(fā)現(xiàn):注冊中心提供正確的服務(wù)發(fā)現(xiàn),當(dāng)在本環(huán)境發(fā)現(xiàn)不到待調(diào)用的服務(wù)時(shí),自動(dòng)訪問基準(zhǔn)環(huán)境的服務(wù)。
通過流量染色,區(qū)分出哪些流量是哪些團(tuán)隊(duì)的,從而在服務(wù)發(fā)現(xiàn)時(shí),能正確調(diào)用到正確的服務(wù)。
另外,我司使用的注冊中心是nacos,因此本文將主要介紹基于nacos的智能路由實(shí)現(xiàn),其他注冊中心同理,做相應(yīng)改造即可。
2. 具體實(shí)現(xiàn) <1> 流量染色
智能路由的第一步,就是要做流量的染色,將流量能夠沿著鏈路一直透傳下去。那么就需要找到流量的入口,然后在入口處進(jìn)行染色。下圖是網(wǎng)站的內(nèi)部調(diào)用情況,可以看到,流量從Nginx進(jìn)來,最終打到服務(wù)集群,因此需要對(duì)Nginx進(jìn)行染色。
流量染色主要是在流量的頭部加上一些標(biāo)記,以便識(shí)別。這里利用了Nginx的proxy_set_header,我們通過下列方式來設(shè)置頭部。
## nginx的匹配規(guī)則設(shè)置header proxy_set_header req_context "{'version': '1.0'}"
這樣子,我們就為版本是1.0的流量設(shè)置了頭部,其中的參數(shù)可以任意添加,這里僅列舉最重要的一個(gè)參數(shù)。
另外,還有一個(gè)問題,流量只有從Nginx進(jìn)來,才會(huì)帶上這個(gè)頭部。如果是在內(nèi)部直接訪問某個(gè)中間的服務(wù),那么這個(gè)時(shí)候流量是沒有頭部的。對(duì)此,我們的解決方案是filter,通過filter可以動(dòng)態(tài)地?cái)r截請求,修改請求頭部,為其初始化一個(gè)默認(rèn)值。
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. 初始化請求上下文,如果沒有,就進(jìn)行初始化 initContext(context); try { filterChain.doFilter(servletRequest, servletResponse); } finally { ContextUtil.clear(); } } public void initContext(String contextStr) { //json轉(zhuǎn)object Context context = JSONObject.parseObject(contextStr, GlobalContext.class); //避免假初始化 if (context == null) { context = new Context(); } //這里進(jìn)行初始化,如果沒值,設(shè)置一個(gè)默認(rèn)值 if (StringUtils.isEmpty(context.getVersion())) { context.setVersion("master"); } ... //存儲(chǔ)到上下文中 ContextUtil.setCurrentContext(context); } }
通過這個(gè)filter,保證了在中間環(huán)節(jié)訪問時(shí),流量仍然被染色。
<2> 流量透傳
流量在入口被染色后,需要透傳到整個(gè)鏈路,因此需要對(duì)服務(wù)做一些處理,下面分幾種情形分別處理。
1~ Spring Cloud Gateway
對(duì)于Gateway,保證請求在轉(zhuǎn)發(fā)過程中的header不丟,這個(gè)是必要的。這里通過Gateway自帶的GlobalFilter來實(shí)現(xiàn),代碼如下:
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. 構(gòu)造ServerHttpRequest ServerHttpRequest serverHttpRequest = exchange.getRequest().mutate().header(ContextUtil.REQUEST_CONTEXT, context).build(); //3. 構(gòu)造ServerWebExchange ServerWebExchange serverWebExchange = exchange.mutate().request(serverHttpRequest).build(); return chain.filter(serverWebExchange).then( Mono.fromRunnable( () -> { ContextUtil.clear(); }) ); } }
2~ SpringCloud:Feign
這一類也是最常用的,SC的服務(wù)集群都通過Feign進(jìn)行交互,因此只需要配置Feign透傳即可。在這里,我們利用了Feign自帶的RequestInterceptor,實(shí)現(xiàn)請求攔截。代碼如下:
@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發(fā)送請求。比如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()));
只需要粗暴地在發(fā)送頭部中增加header即可,而其請求上下文直接通過當(dāng)前線程上下文獲取即可。
<3> 配置負(fù)載規(guī)則
完成了流量染色,下面就差服務(wù)發(fā)現(xiàn)了。服務(wù)發(fā)現(xiàn)基于注冊中心nacos,因此需要修改負(fù)載規(guī)則。在這里,我們配置Ribbon的負(fù)載規(guī)則,修改為自定義負(fù)載均衡器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("請求服務(wù)異常!異常信息:{}", ee); } catch (Exception e) { log.error("請求服務(wù)異常!異常信息:{}", e); } return null; } }
從代碼中可以看到,最終通過nacosNamingService.getInstance()方法獲取實(shí)例。
<4> 配置智能路由規(guī)則
上面的負(fù)載規(guī)則,最終調(diào)用的是nacosNamingService.getInstance()方法,該方法里面定義了智能路由規(guī)則,主要功能是根據(jù)流量進(jìn)行服務(wù)精準(zhǔn)匹配。
規(guī)則如下:
1~開關(guān)判斷:是否開啟路由功能,沒有則走nacos默認(rèn)路由。
2~獲取實(shí)例:根據(jù)流量,獲取對(duì)應(yīng)實(shí)例。其中,路由匹配按照一定的優(yōu)先級(jí)進(jìn)行匹配。
路由規(guī)則:IP優(yōu)先 > 環(huán)境 + 組 > 環(huán)境 + 默認(rèn)組
解釋一下這個(gè)規(guī)則,首先是獲取實(shí)例,需要先獲取nacos上面的所有可用實(shí)例,然后遍歷,從中選出一個(gè)最合適的實(shí)例。
然后IP優(yōu)先的含義是,如果在本地調(diào)試服務(wù),那么從本地直接訪問網(wǎng)站,請求就會(huì)優(yōu)先訪問本地服務(wù),那么就便于開發(fā)人員調(diào)試了,debug,本地開發(fā)都不是問題了!
其實(shí)是,環(huán)境 + 組,這個(gè)規(guī)則代表了如果存在對(duì)應(yīng)的環(huán)境和組都相同的服務(wù),那作為最符合的實(shí)例肯定優(yōu)先返回,其實(shí)是環(huán)境 + 默認(rèn)組,最后如果都沒有,就訪問基準(zhǔn)環(huán)境(master)。
注:環(huán)境和組的概念對(duì)應(yīng)nacos上的namespace和group,如有不懂,請自行查看nacos官方文檔。
最終代碼如下:
public class NacosNamingService { public Instance getInstance(String serviceName, String groupName) throws NacosException { //1. 判斷智能路由開關(guān)是否開啟,沒有走默認(rèn)路由 if (!isEnable()) { return discoveryProperties.namingServiceInstance().selectOneHealthyInstance(serviceName, groupName); } Context context = ContextUtil.getCurrentContext(); if (Context == null) { return NacosNamingFactory.getNamingService(CommonConstant.Env.MASTER).selectOneHealthyInstance(serviceName); } //2. 獲取實(shí)例 return getInstance(serviceName, context); } public Instance getInstance(String serviceName, Context context) throws NacosException { Instance envAndGroupInstance = null; Instance envDefGroupInstance = null; Instance defaultInstance = null; //2.1 獲取所有可以調(diào)用的命名空間 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) { // 優(yōu)先本機(jī)匹配 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> 配置智能路由定時(shí)任務(wù)
剛才在介紹智能路由的匹配規(guī)則時(shí),提到“獲取所有可以調(diào)用的命名空間”。這是因?yàn)椋琻acos上可能有很多個(gè)命名空間namespace,而我們需要把所有namespace上的所有可用服務(wù)都獲取到,而nacos源碼中,一個(gè)namespace對(duì)應(yīng)一個(gè)NamingService。因此我們需要定時(shí)獲取nacos上所有的NamingService,存儲(chǔ)到本地,再通過NamingService去獲取實(shí)例。因此我們的做法是,配置一個(gè)監(jiān)聽器,定期監(jiān)聽nacos上的namespace變化,然后定期更新,維護(hù)到服務(wù)的內(nèi)部緩存中。代碼如下:
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; }
下面是智能路由實(shí)現(xiàn)過程,遇到的一些問題及解決方案。
問題1:namespace啟動(dòng)了太多線程,導(dǎo)致線程數(shù)過大?
因?yàn)榉?wù)需要維護(hù)過多的namespace,每個(gè)namespace內(nèi)部又啟動(dòng)多個(gè)線程維護(hù)服務(wù)實(shí)例信息,導(dǎo)致服務(wù)總線程數(shù)過大。
解決方案:每個(gè)namespace設(shè)置只啟動(dòng)2個(gè)線程,通過下列參數(shù)設(shè)置:
properties.setProperty(PropertyKeyConst.NAMING_CLIENT_BEAT_THREAD_COUNT, "1"); properties.setProperty(PropertyKeyConst.NAMING_POLLING_THREAD_COUNT, "1");
問題2:支持一個(gè)線程調(diào)用多個(gè)服務(wù)?
每個(gè)請求都會(huì)創(chuàng)建一個(gè)線程,這個(gè)線程可能會(huì)調(diào)用多次其他服務(wù)。
解決方案:既然調(diào)用多次,那就創(chuàng)建上下文,并保持上下文,調(diào)用結(jié)束后再清除。見代碼:
try { filterChain.doFilter(servletRequest, servletResponse); } finally { ContextUtil.clear(); }
問題3:多應(yīng)用支持:Tomcat,Springboot,Gateway?
我們內(nèi)部有多種框架,如何保證這些不同框架服務(wù)的支持?
解決方案:針對(duì)不同應(yīng)用,開發(fā)不同的starter包。
問題4:SpringBoot版本兼容問題
解決方案:針對(duì)1.x和2.x單獨(dú)開發(fā)starter包。
1. 經(jīng)濟(jì)價(jià)值
同樣的資源,多部署了n套環(huán)境,極大提高資源利用率。(畢竟增量環(huán)境和全量環(huán)境的代價(jià)還是相差很大的)
2. 研發(fā)價(jià)值
本地開發(fā)排查測試問題方便,極大提高研發(fā)效率。前面提到的IP優(yōu)先規(guī)則,保證了這一點(diǎn)。本地請求總是最優(yōu)先打到本地上。
3. 測試價(jià)值
多部署n套環(huán)境,支持更多版本,提高測試效率。同時(shí)只需要部署增量應(yīng)用,也提高部署效率。
通過智能路由,我司實(shí)現(xiàn)了部署成本大幅減少,部署效率大幅提高,研發(fā)測試效率大幅提高。
最后總結(jié)下智能路由的主要功能:
1. 多環(huán)境管理:支持多環(huán)境路由,除了基準(zhǔn)環(huán)境外,其他環(huán)境只部署增量應(yīng)用。
2. 多用戶支持:支持多用戶公用一套環(huán)境,避免開發(fā)不同版本造成的沖突。
3. 前端研發(fā)路由:對(duì)前端研發(fā)人員,可以方便快捷地同一個(gè)任意后端人員對(duì)接。
4. 后端研發(fā)路由:對(duì)后端研發(fā)人員,無論什么環(huán)境都可以快速調(diào)試,快速發(fā)現(xiàn)問題。
5. 友好且兼容:對(duì)微服務(wù)無侵入性,且支持 Web、WebFlux、Tomcat等應(yīng)用。
感謝各位的閱讀,以上就是“配置gateway+nacos動(dòng)態(tài)路由管理的詳細(xì)流程”的內(nèi)容了,經(jīng)過本文的學(xué)習(xí)后,相信大家對(duì)配置gateway+nacos動(dòng)態(tài)路由管理的詳細(xì)流程這一問題有了更深刻的體會(huì),具體使用情況還需要大家實(shí)踐驗(yàn)證。這里是億速云,小編將為大家推送更多相關(guān)知識(shí)點(diǎn)的文章,歡迎關(guān)注!
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。