配置gateway+nacos動態(tài)路由管理流程
配置gateway+nacos動態(tài)路由
第一步:首先是設置配置文件的配置列表
然后在配置讀取配置類上增加刷新注解@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 + '}';
}
}
第二步:配置監(jiān)聽nacos監(jiān)聽器
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("重新更新動態(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("重新更新動態(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;
}
}
第三步:配置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的智能路由實現(xiàn)與應用
一. 概述
隨著微服務的興起,我司也逐漸加入了微服務的改造浪潮中。但是,隨著微服務體系的發(fā)展壯大,越來越多的問題暴露出來。其中,測試環(huán)境治理,一直是實施微服務的痛點之一,它的痛主要體現(xiàn)在環(huán)境管理困難,應用部署困難,技術方案配合等。最終,基于我司的實際情況,基于注冊中心nacos實現(xiàn)的智能路由有效地解決了這個問題。本文主要介紹我司在測試環(huán)境治理方面遇到的難題與對應的解決方案。
二. 遇到的問題
1. 困難的環(huán)境管理與應用部署
隨著公司業(yè)務發(fā)展,業(yè)務逐漸復雜化。這在微服務下帶來的一個問題就是服務的不斷激增,且增速越來越快。而在這種情況下,不同業(yè)務團隊如果想并行開發(fā)的話,都需要一個環(huán)境。假設一套完整環(huán)境需要部署1k個服務,那么n個團隊就需要部署n*1k個服務,這顯然是不能接受的。
2. 缺失的技術方案
從上面的分析可以看出,一個環(huán)境部署全量的服務顯然是災難性的。那么就需要有一種技術方案,來解決應用部署的難題。最直接的一個想法就是,每個環(huán)境只部署修改的服務,然后通過某種方式,實現(xiàn)該環(huán)境的正常使用。當然,這也是我們最終的解決方案。下面會做一個介紹。
3. 研發(fā)問題
除了這兩個大的問題,還有一些其他的問題也急需解決。包括后端多版本并行聯(lián)調(diào)和前后端聯(lián)調(diào)的難題。下面以實際的例子來說明這兩個問題。
<1> 后端多版本并行聯(lián)調(diào)難
某一個微服務,有多個版本并行開發(fā)時,后端聯(lián)調(diào)時調(diào)用容易錯亂。例如這個例子,1.1版本的服務A需要調(diào)用1.1版本的服務B,但實際上能調(diào)用到嗎???

目前使用的配置注冊中心是nacos,nacos自身有一套自己的服務發(fā)現(xiàn)體系,但這是基于namespace和group的同頻服務發(fā)現(xiàn),對于跨namespace的服務,它就不管用了。
<2> 前后端聯(lián)調(diào)難
前端和后端的聯(lián)調(diào)困難,這個問題也是經(jīng)常遇到的。主要體現(xiàn)在,后端聯(lián)調(diào)往往需要啟動多個微服務(因為服務的依賴性)。而前端要對應到某一個后端,也需要特殊配置(比如指定ip等)。下面這個例子,后端人員開發(fā)服務A,但卻要啟動4個服務才能聯(lián)調(diào)。因為服務A依賴于服務B,C,D。

4. 其他問題
除了以上的問題外,還有一些小的問題也可以關注下:
<1> 測試環(huán)境排查問題
這個問題不算棘手,登錄服務器查看日志即可。但是能否再靈活點,比如讓開發(fā)人員debug或者在本地調(diào)試問題呢?
<2> 本地開發(fā)
本地開發(fā),后端往往也依賴多個服務,能不能只啟動待開發(fā)的服務,而不啟動其他旁路服務呢?
三. 智能路由的實現(xiàn)
基于這些需求,智能路由應運而生。它正是為了解決這個問題。最終,我們通過它解決了測試環(huán)境治理的難題。
智能路由,能根據(jù)不同環(huán)境,不同用戶,甚至不同機器進行精確路由。下面以一個例子說明。
三個團隊,team1,team2和team3各負責不同的業(yè)務需求。其中team1只需要改動A服務,team2只需要改動B服務,team3需要在qa環(huán)境上驗證。通過智能路由,team1,team2只在自己的環(huán)境上部署了增量應用,然后在訪問該環(huán)境的時候,當找不到對應環(huán)境的服務時,就從基準環(huán)境上訪問。而team3只做qa,因此直接訪問基準環(huán)境即可。可以看到,基準環(huán)境上部署了全量服務,除此之外,其他小環(huán)境都是增量服務。

下面介紹智能路由的具體實現(xiàn)方案。
1. 原理
通過上圖,可以看到,智能路由其實就是流量染色加上服務發(fā)現(xiàn)。
流量染色:將不同團隊的流量進行染色,然后透傳到整個鏈路中。
服務發(fā)現(xiàn):注冊中心提供正確的服務發(fā)現(xiàn),當在本環(huán)境發(fā)現(xiàn)不到待調(diào)用的服務時,自動訪問基準環(huán)境的服務。
通過流量染色,區(qū)分出哪些流量是哪些團隊的,從而在服務發(fā)現(xiàn)時,能正確調(diào)用到正確的服務。
另外,我司使用的注冊中心是nacos,因此本文將主要介紹基于nacos的智能路由實現(xiàn),其他注冊中心同理,做相應改造即可。
2. 具體實現(xiàn) <1> 流量染色
智能路由的第一步,就是要做流量的染色,將流量能夠沿著鏈路一直透傳下去。那么就需要找到流量的入口,然后在入口處進行染色。下圖是網(wǎng)站的內(nèi)部調(diào)用情況,可以看到,流量從Nginx進來,最終打到服務集群,因此需要對Nginx進行染色。

流量染色主要是在流量的頭部加上一些標記,以便識別。這里利用了Nginx的proxy_set_header,我們通過下列方式來設置頭部。
## nginx的匹配規(guī)則設置header
proxy_set_header req_context "{'version': '1.0'}"
這樣子,我們就為版本是1.0的流量設置了頭部,其中的參數(shù)可以任意添加,這里僅列舉最重要的一個參數(shù)。
另外,還有一個問題,流量只有從Nginx進來,才會帶上這個頭部。如果是在內(nèi)部直接訪問某個中間的服務,那么這個時候流量是沒有頭部的。對此,我們的解決方案是filter,通過filter可以動態(tài)地攔截請求,修改請求頭部,為其初始化一個默認值。
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,保證了在中間環(huán)節(jié)訪問時,流量仍然被染色。
<2> 流量透傳
流量在入口被染色后,需要透傳到整個鏈路,因此需要對服務做一些處理,下面分幾種情形分別處理。
1~ Spring Cloud Gateway
對于Gateway,保證請求在轉發(fā)過程中的header不丟,這個是必要的。這里通過Gateway自帶的GlobalFilter來實現(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. 構造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,實現(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即可,而其請求上下文直接通過當前線程上下文獲取即可。
<3> 配置負載規(guī)則
完成了流量染色,下面就差服務發(fā)現(xiàn)了。服務發(fā)現(xiàn)基于注冊中心nacos,因此需要修改負載規(guī)則。在這里,我們配置Ribbon的負載規(guī)則,修改為自定義負載均衡器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> 配置智能路由規(guī)則
上面的負載規(guī)則,最終調(diào)用的是nacosNamingService.getInstance()方法,該方法里面定義了智能路由規(guī)則,主要功能是根據(jù)流量進行服務精準匹配。
規(guī)則如下:
1~開關判斷:是否開啟路由功能,沒有則走nacos默認路由。
2~獲取實例:根據(jù)流量,獲取對應實例。其中,路由匹配按照一定的優(yōu)先級進行匹配。
路由規(guī)則:IP優(yōu)先 > 環(huán)境 + 組 > 環(huán)境 + 默認組
解釋一下這個規(guī)則,首先是獲取實例,需要先獲取nacos上面的所有可用實例,然后遍歷,從中選出一個最合適的實例。
然后IP優(yōu)先的含義是,如果在本地調(diào)試服務,那么從本地直接訪問網(wǎng)站,請求就會優(yōu)先訪問本地服務,那么就便于開發(fā)人員調(diào)試了,debug,本地開發(fā)都不是問題了!
其實是,環(huán)境 + 組,這個規(guī)則代表了如果存在對應的環(huán)境和組都相同的服務,那作為最符合的實例肯定優(yōu)先返回,其實是環(huán)境 + 默認組,最后如果都沒有,就訪問基準環(huán)境(master)。
注:環(huán)境和組的概念對應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 獲取所有可以調(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)先本機匹配
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> 配置智能路由定時任務
剛才在介紹智能路由的匹配規(guī)則時,提到“獲取所有可以調(diào)用的命名空間”。這是因為,nacos上可能有很多個命名空間namespace,而我們需要把所有namespace上的所有可用服務都獲取到,而nacos源碼中,一個namespace對應一個NamingService。因此我們需要定時獲取nacos上所有的NamingService,存儲到本地,再通過NamingService去獲取實例。因此我們的做法是,配置一個監(jiān)聽器,定期監(jiān)聽nacos上的namespace變化,然后定期更新,維護到服務的內(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;
}
四. 遇到的難點
下面是智能路由實現(xiàn)過程,遇到的一些問題及解決方案。
問題1:namespace啟動了太多線程,導致線程數(shù)過大?
因為服務需要維護過多的namespace,每個namespace內(nèi)部又啟動多個線程維護服務實例信息,導致服務總線程數(shù)過大。
解決方案:每個namespace設置只啟動2個線程,通過下列參數(shù)設置:
properties.setProperty(PropertyKeyConst.NAMING_CLIENT_BEAT_THREAD_COUNT, "1"); properties.setProperty(PropertyKeyConst.NAMING_POLLING_THREAD_COUNT, "1");
問題2:支持一個線程調(diào)用多個服務?
每個請求都會創(chuàng)建一個線程,這個線程可能會調(diào)用多次其他服務。
解決方案:既然調(diào)用多次,那就創(chuàng)建上下文,并保持上下文,調(diào)用結束后再清除。見代碼:
try {
filterChain.doFilter(servletRequest, servletResponse);
} finally {
ContextUtil.clear();
}
問題3:多應用支持:Tomcat,Springboot,Gateway?
我們內(nèi)部有多種框架,如何保證這些不同框架服務的支持?
解決方案:針對不同應用,開發(fā)不同的starter包。
問題4:SpringBoot版本兼容問題
解決方案:針對1.x和2.x單獨開發(fā)starter包。
五. 帶來的收益
1. 經(jīng)濟價值
同樣的資源,多部署了n套環(huán)境,極大提高資源利用率。(畢竟增量環(huán)境和全量環(huán)境的代價還是相差很大的)
2. 研發(fā)價值
本地開發(fā)排查測試問題方便,極大提高研發(fā)效率。前面提到的IP優(yōu)先規(guī)則,保證了這一點。本地請求總是最優(yōu)先打到本地上。
3. 測試價值
多部署n套環(huán)境,支持更多版本,提高測試效率。同時只需要部署增量應用,也提高部署效率。
六. 總結
通過智能路由,我司實現(xiàn)了部署成本大幅減少,部署效率大幅提高,研發(fā)測試效率大幅提高。
最后總結下智能路由的主要功能:
1. 多環(huán)境管理:支持多環(huán)境路由,除了基準環(huán)境外,其他環(huán)境只部署增量應用。
2. 多用戶支持:支持多用戶公用一套環(huán)境,避免開發(fā)不同版本造成的沖突。
3. 前端研發(fā)路由:對前端研發(fā)人員,可以方便快捷地同一個任意后端人員對接。
4. 后端研發(fā)路由:對后端研發(fā)人員,無論什么環(huán)境都可以快速調(diào)試,快速發(fā)現(xiàn)問題。
5. 友好且兼容:對微服務無侵入性,且支持 Web、WebFlux、Tomcat等應用。
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關文章
Spring Boot集成ShedLock分布式定時任務的實現(xiàn)示例
ShedLock確保您計劃的任務最多同時執(zhí)行一次。如果一個任務正在一個節(jié)點上執(zhí)行,則它會獲得一個鎖,該鎖將阻止從另一個節(jié)點(或線程)執(zhí)行同一任務。2021-05-05
Servlet3.0學習總結之基于Servlet3.0的文件上傳實例
本篇文章主要介紹了Servlet3.0學習總結之基于Servlet3.0的文件上傳實例,具有一定的參考價值,有興趣的可以了解一下2017-07-07
MapStruct @Mapping注解之處理映射中的Null值方式
這篇文章主要介紹了MapStruct @Mapping注解之處理映射中的Null值方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2025-03-03
Java實現(xiàn)根據(jù)模板讀取PDF并替換指定內(nèi)容
在實際開發(fā)里,經(jīng)常會遇到需要根據(jù)?PDF?模板文檔生成特定?PDF?的需求,本文將利用Java中的iText實現(xiàn)讀取?PDF?模板文檔并替換指定內(nèi)容,最后重新生成新PDF,感興趣的可以了解下2025-02-02

