Gateway开发与调试隔离解决方案

使用Spring Cloud开发微服务应用时,开发阶段一般会是开发人员启动一个服务,服务器部署一个服务,这样一个实例会有多个服务并且分布在服务器和开发人员的地址,这样前端调试的时候可能会请求到我们开发人员自己的电脑,有时候还会导致结果不一致。本篇文章主要解决这个问题。

使用不同的命名空间或组

我们公司主要只用Nacos作为注册中心和配置中心,因为Nacos本身是命名空间隔离与组隔离,可以创建不同的命名空间和组,代码推上去后指定不同的命名空间或组即可。如下所示

我们可以在开发时指定为dev环境,当代码推送到服务器编译时指定为debug环境。
首先配置环境为一个变量

1
2
3
4
5
spring:
profiles:
active: ${profile}
application:
name: gateway

这样我们就可以将开发和调试隔离了,但是麻烦的地方就在于需要添加不同的配置,有点麻烦。

自定义负载均衡

除了根据nacos的特性,将名称空间或组隔离之外,还可以在网关中自定义负载均衡器。
Spring Cloud Gateway包含两种负载均衡器,RandomLoadBalancer(随机策略)和RoundRobinLoadBalancer(轮询策略),如果需要解决上面的问题。这两种负载均衡都是不合适的。因为都有可能将请求负载到开发人员的节点上。
所以我们需要自定义负载均衡。如何处理呢,首先开发的时候,我们希望通过网关负载到我们自己的电脑,调试的时候我们希望通过网关将节点负载到服务器。那其实就是获取本机IP来进行请求。代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
@Slf4j
@Profile("dev")
@Component
public class DevLoadBalancer {

public static final String DOCKER = "docker0";

@Autowired
DiscoveryClient discoveryClient;

/**
* 根据serviceId 筛选可用服务
* @param serviceId 服务ID
* @param request 当前请求
* @return ServiceInstance
*/
@SneakyThrows
public ServiceInstance choose(String serviceId, ServerHttpRequest request) {
List<ServiceInstance> instances = discoveryClient.getInstances(serviceId);
//注册中心无实例 抛出异常
if (CollectionUtils.isEmpty(instances)) {
log.warn("No instance available for {}", serviceId);
throw new NotFoundException("No instance available for " + serviceId);
}
String local = getLocalIp();
for (ServiceInstance instance : instances) {
// 一级优先返回本机IP
if (instance.getHost().equals(local)){
log.info("负载到本地节点: {}",instance.getHost());
return instance;
}
}
ServiceInstance instance = instances.get(RandomUtils.nextInt(instances.size()));
log.warn("随机负载到集群,节点地址: {}",instance.getHost());
return instance;
}

/**
* 获取本机IP
* @return local ip
*/
public String getLocalIp() {
try {
for (Enumeration<NetworkInterface> e = NetworkInterface.getNetworkInterfaces(); e.hasMoreElements(); ) {
NetworkInterface item = e.nextElement();
if (!item.getName().equals(DOCKER)) {
for (InterfaceAddress address : item.getInterfaceAddresses()) {
if (item.isLoopback() || !item.isUp()) {
continue;
}
if (address.getAddress() instanceof Inet4Address) {
Inet4Address inet4Address = (Inet4Address) address.getAddress();
return inet4Address.getHostAddress();
}
}
}
}
return InetAddress.getLocalHost().getHostAddress();
} catch (SocketException | UnknownHostException e) {
throw new RuntimeException(e);
}
}
}

通过Nacos获取到所有的ServiceInstance,然后遍历所有的ServiceInstance判断instance的host是否与本机相同。如果相同则返回当前instance,如果所有服务的host都没有与本机相同的地址,那么直接使用随机负载即可。

然后我们需要继承自ReactiveLoadBalancerClientFilter过滤器,并重写filter方法,如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
@Slf4j
@Component
@Profile("dev")
public class DevReactiveLoadBalancerClientFilter extends ReactiveLoadBalancerClientFilter {

@Autowired
DevLoadBalancer loadBalancer;

public DevReactiveLoadBalancerClientFilter(LoadBalancerClientFactory clientFactory, GatewayLoadBalancerProperties properties) {
super(clientFactory, properties);
}

private Mono<Response<ServiceInstance>> choose(ServerWebExchange exchange) {
URI uri = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR);
Assert.notNull(uri,"获取应用失败...");
if (uri.getHost().equals("localhost")){
return Mono.just(new DefaultResponse(new DefaultServiceInstance(uri.getHost(),uri.getPath(),uri.getHost(),uri.getPort(),false)));
}
ServiceInstance serviceInstance = loadBalancer.choose(uri.getHost(), exchange.getRequest());
return Mono.just(new DefaultResponse(serviceInstance));
}

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return choose(exchange).doOnNext(response -> {
URI url = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR);
Assert.notNull(url,"暂无服务可用...");
URI uri = exchange.getRequest().getURI();
String schemePrefix = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_SCHEME_PREFIX_ATTR);
ServiceInstance retrievedInstance = response.getServer();
String overrideScheme = retrievedInstance.isSecure() ? "https" : "http";
if (schemePrefix != null) {
overrideScheme = url.getScheme();
}
DelegatingServiceInstance serviceInstance = new DelegatingServiceInstance(
response.getServer(), overrideScheme);
URI requestUrl = LoadBalancerUriTools.reconstructURI(serviceInstance, uri);
exchange.getAttributes().put(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR, requestUrl);
}).then(chain.filter(exchange));
}
}

当前配置只在dev环境生效。我们看看效果

这种方案不需要多余的配置,唯一的限制就是网关服务需要和业务服务放在一台设备上,开发的时候都是这样部署的,没有影响。

作者

Labradors

发布于

2022-05-11

更新于

2022-06-15

许可协议

评论