springCloudGateway+nacos自定义负载均衡-通过IP隔离开发环境
先说一下想法,小公司开发项目,参考若依框架使用的
spring-cloud-starter-gateway
和spring-cloud-starter-alibaba-nacos
, 用到了nacos的配置中心和注册中心,有多个模块(每个模块都是一个服务)。
想本地开发,且和其他同事不互相影响的情况下,有同事建议 本地部署 完整的环境(nacos、需要的模块、前端等),除了 数据库和redis共用。这种方案也能用,就是占用自己电脑资源太多了(ps 自己电脑摸鱼的硬件资源就少了)。
我通过看nacos相关的教程,发现可以通过 配置中心 和 注册中心 的namespace
命名不同进行隔离,这样可以不用部署nacos服务。
继续分析发现,网关模块 默认的 负载均衡ReactorServiceInstanceLoadBalancer
的实现RoundRobinLoadBalancer
是通过轮询 访问服务的,可以 改造 负载均衡 来实现 本地开发环境最小化(只允许开发的服务模块),其他服务用 测试环境提供。
当前改造只适用请求通过网关,如果模块服务之间 直连调用,就需要在每个模块增加类似处理。暂不考虑
(ps 本人小白一枚springCloudGateway使用的少)本文章限定在这个项目里面,如果自己项目环境不一样,不保证能拿来直接用。
实现截图-不想看原理的先食用结果
参考
本来想通过搜索和AI找到解决方案,结果代码抄下来每一个能实现的。要么没有引用的类,要么无法实现自己的想法。所以就不贴了。
每个项目的依赖和版本情况都不一样,最好的方法是 找到框架的核心实现类,在跟踪调试和找到引用与初始化的地方。想办法自定义实现注入-这种方式最好,毕竟都是框架给留的口子。实在不行,就只能通过魔改或劫持的方式实现了。
原理图-设想
改造前请求流程
这种情况,适用于 非开发环境,简单高效
改造后请求流程
改造后的,适用于 本地开发,通过网关只请求自己的服务,或者自己没有只请求测试环境(简单的测试环境所有服务一台机器上)。尽最大可能不影响 其他同事开发。
图中没有画 锁定请求服务IP的情况(这块代码没测试)。
项目框架主要依赖
参考时候请注意 依赖包版本是否差距过大
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
<version>3.1.4</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<version>2021.0.4.0</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
<version>2021.0.4.0</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
<version>2021.0.4.0</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
<version>2021.0.4.0</version>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
<version>1.8.5</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-loadbalancer</artifactId>
<version>3.1.5</version>
</dependency>
自定义实现
自定义负载均衡类
根据 org.springframework.cloud.loadbalancer.core.RoundRobinLoadBalancer
轮询策略 改造,这块代码倒还好实现。TestLocalLockIpLoadBalancer
直接注入spring中,ObjectProvider<ServiceInstanceListSupplier>
获取不到服务列表,看了spring实现,要完全替换需要注入好几个类,有点麻烦。最后改成运行时替换。
自定义路由负载均衡,负载均衡策略:
- 先根据请求IP查询IP相同的服务
- 再找和网关IP相同的服务
- 最后轮询其他IP服务
package *.*.*.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.*;
import org.springframework.cloud.loadbalancer.core.NoopServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.core.ReactorServiceInstanceLoadBalancer;
import org.springframework.cloud.loadbalancer.core.SelectedInstanceCallback;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import org.springframework.http.HttpHeaders;
import reactor.core.publisher.Mono;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 自定义路由负载均衡->初始化,负载均衡策略: 先找请求IP查询IP相同的服务、再找和网关IP相同的服务、最后轮询其他IP服务
*
* @see org.springframework.cloud.loadbalancer.core.RoundRobinLoadBalancer
* @author z
* @date 2025/1/10
*/
public class TestLocalLockIpLoadBalancer implements ReactorServiceInstanceLoadBalancer {
private static final Logger log = LoggerFactory.getLogger(TestLocalLockIpLoadBalancer.class);
final AtomicInteger position;
final String serviceId;
ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;
private String localIp;
public TestLocalLockIpLoadBalancer setLocalIp(String localIp) { this.localIp = localIp; return this; }
public TestLocalLockIpLoadBalancer(ObjectProvider<ServiceInstanceListSupplier> provide, String serviceId) {
this(provide, serviceId, new Random().nextInt(1000));
}
public TestLocalLockIpLoadBalancer(ObjectProvider<ServiceInstanceListSupplier> provide, String serviceId, int seedPosition) {
this.serviceId = serviceId;
this.serviceInstanceListSupplierProvider = provide;
this.position = new AtomicInteger(seedPosition);
System.out.println("自定义路由负载均衡->初始化,负载均衡策略: 先找请求IP查询IP相同的服务、再找和网关IP相同的服务、最后轮询其他IP服务");
}
@Override
public Mono<Response<ServiceInstance>> choose(Request request) {
System.out.println("自定义路由负载均衡---->");
ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider.getIfAvailable(NoopServiceInstanceListSupplier::new);
String ip = getIpAddr(request);
return supplier.get(request).next().map(services -> processInstanceResponse(supplier, services, ip));
}
private Response<ServiceInstance> processInstanceResponse(ServiceInstanceListSupplier supplier
, List<ServiceInstance> services, String reqIp) {
Response<ServiceInstance> serviceInstanceResponse = getInstanceResponse(services, reqIp);
if (supplier instanceof SelectedInstanceCallback && serviceInstanceResponse.hasServer()) {
((SelectedInstanceCallback) supplier).selectedServiceInstance(serviceInstanceResponse.getServer());
}
return serviceInstanceResponse;
}
private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances, String reqIp) {
System.out.println("自定义路由负载均衡--- 服务数量:" + instances.size());
if (instances.isEmpty()) {
log.warn("没有可供服务的服务器: {}, 请求: {}" , serviceId, reqIp);
return new EmptyResponse();
}
if (instances.size() == 1) {
System.out.println("自定义路由负载均衡--- 服务只有一个: " + instances.get(0).getHost());
return new DefaultResponse(instances.get(0));
}
if( null != reqIp && !reqIp.isEmpty()){
for (ServiceInstance instance : instances) {
if(instance.getHost().equals(reqIp)){
System.out.println("自定义路由负载均衡--- 策略1: 自己处理 " + reqIp);
return new DefaultResponse(instance);
}
}
}
for (ServiceInstance instance : instances) {
if(instance.getHost().equals(localIp)){
System.out.println("自定义路由负载均衡--- 策略2: 测试处理 " + localIp);
return new DefaultResponse(instance);
}
}
int pos = this.position.incrementAndGet() & Integer.MAX_VALUE;
ServiceInstance instance = instances.get(pos % instances.size());
System.out.println("自定义路由负载均衡--- 策略3: 默认轮询:" + instance.getHost());
return new DefaultResponse(instance);
}
/** 获取客户端IP */
private String getIpAddr(Request request) {
DefaultRequestContext requestContext = (DefaultRequestContext) request.getContext();
RequestData clientRequest = (RequestData) requestContext.getClientRequest();
HttpHeaders headers = clientRequest.getHeaders();
if(headers.containsKey("sourceIp")){
return headers.getFirst("sourceIp");
}
return null;
}
}
自定义配置注入-偷天换日
最麻烦的事,是TestLocalLockIpLoadBalancer
注入spring后,ObjectProvider<ServiceInstanceListSupplier>
获取不到服务列表。当前操作是运行时替换。在不考虑性能的情况下,这个只要 重写LoadBalancerClientFactory
工厂类,改造获取 负载均衡 实例的方法就行。
注入的全局拦截器
LockIpGlobalFilter
,主要是获取 当前请求的 来源IP 放到请求头里面,因为 负载均衡的Request
好像是获取不到IP信息。
自定义工厂类LockIpLoadBalancerClientFactory
实现,重写获取负载均衡方法(替换为自定义类)
package *.*.*.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClientsProperties;
import org.springframework.cloud.commons.util.InetUtils;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClientSpecification;
import org.springframework.cloud.loadbalancer.core.RoundRobinLoadBalancer;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* 路由过滤 优先 请求ip对应的服务实例
*
* @see org.springframework.cloud.gateway.filter.ReactiveLoadBalancerClientFilter
* @author z.
* @date 2025/1/10
*/
@Configuration
@ConditionalOnProperty(value = "spring.cloud.lockIp", havingValue = "true")
public class TestLocalLockIpConfig {
private static final Logger log = LoggerFactory.getLogger(TestLocalLockIpConfig.class);
@Autowired
private InetUtils inetUtils;
@Bean
public LockIpGlobalFilter lockIpGlobalFilter(){
System.out.println(">>>>>>>>>自定义全局过滤器-注册");
return new LockIpGlobalFilter();
}
@Bean
public LoadBalancerClientFactory loadBalancerClientFactory(LoadBalancerClientsProperties properties,
ObjectProvider<List<LoadBalancerClientSpecification>> configurations) {
System.out.println(">>>>>>>>>自定义路由工厂-注册");
LockIpLoadBalancerClientFactory clientFactory = new LockIpLoadBalancerClientFactory(properties);
clientFactory.setConfigurations(configurations.getIfAvailable(Collections::emptyList));
return clientFactory.setLocalIp(inetUtils.findFirstNonLoopbackHostInfo().getIpAddress());
}
public static class LockIpGlobalFilter implements GlobalFilter, Ordered{
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
System.out.println("------->自定义全局过滤器: " + request.getURI().getPath());
String ip = remoteIp(request);
if(ip != null && !UNKNOWN.equals(ip)){
ServerHttpRequest.Builder mutate = request.mutate();
mutate.header("sourceIp", ip);
}
System.out.println("自定义全局过滤器-获取IP: " + ip);
return chain.filter(exchange);
}
@Override
public int getOrder() { return 0; }
private static final String UNKNOWN = "unknown";
public String remoteIp(ServerHttpRequest request){
HttpHeaders headers = request.getHeaders();
String def = null != request.getRemoteAddress() ? request.getRemoteAddress().getHostString() : UNKNOWN;
String[] keys = {"x-forwarded-for","Proxy-Client-IP","X-Forwarded-For","WL-Proxy-Client-IP","X-Real-IP"};
String ip = Arrays.stream(keys).map(headers::getFirst)
.filter(v-> v != null && !v.isEmpty() && !UNKNOWN.equalsIgnoreCase(v))
.findFirst().orElse(def);
//本机请求 负载均衡 第二个策略 优先使用本机服务
if( "0:0:0:0:0:0:0:1".equals(ip) || "127.0.0.1".equals(ip) ){ return null; }
//从多级反向代理中获得第一个非unknown IP地址
if (ip.indexOf(",") <= 0) {
return Arrays.stream(ip.trim().split(","))
.filter(subIp -> !UNKNOWN.equalsIgnoreCase(subIp)).findFirst().orElse(ip);
}
return ip;
}
}
public static class LockIpLoadBalancerClientFactory extends LoadBalancerClientFactory{
/** 本网关服务 的内网IP */
private String localIp;
public LockIpLoadBalancerClientFactory setLocalIp(String localIp) { this.localIp = localIp; return this; }
public LockIpLoadBalancerClientFactory(LoadBalancerClientsProperties properties){ super(properties); }
@Override
public <T> T getInstance(String name, Class<T> type) {
AnnotationConfigApplicationContext context = getContext(name);
try {
T t = context.getBean(type);
if(t instanceof TestLocalLockIpLoadBalancer){ return t; }
if(t instanceof RoundRobinLoadBalancer){
System.out.println("自定义路由工厂-路由负载均衡策略: 默认轮询 "+t);
String[] beanName = context.getBeanNamesForType(type);
System.out.println("自定义路由工厂-路由负载均衡策略: 默认 删除第一个: "+ Arrays.toString(beanName));
context.removeBeanDefinition(beanName[0]);
System.out.println("自定义路由工厂-路由负载均衡策略: 自定义 创建");
TestLocalLockIpLoadBalancer balancer = new TestLocalLockIpLoadBalancer(
this.getLazyProvider(name, ServiceInstanceListSupplier.class), name).setLocalIp(localIp);
System.out.println("自定义路由工厂-路由负载均衡策略: 自定义 注入");
context.getBeanFactory().registerSingleton(beanName[0],balancer);
t = context.getBean(type);
System.out.println("自定义路由工厂-路由负载均衡策略: 注入后获取 " + t);
}
return t;
} catch (Exception e) {
log.error("自定义路由工厂", e);
return null;
}
}
}
}
配置文件-开关
通过配置,控制是否开启 自定义负载均衡
spring:
cloud:
lockIp: true
Spring网关源码分析
通过搜索和咨询AI,找到springCloudGateway处理服务负载均衡的拦截器org.springframework.cloud.gateway.filter.ReactiveLoadBalancerClientFilter
,核心代码是
private final LoadBalancerClientFactory clientFactory;
private final GatewayLoadBalancerProperties properties;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
URI url = exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR);
String schemePrefix = exchange.getAttribute(GATEWAY_SCHEME_PREFIX_ATTR);
//判断是不是注册中心的服务地址
if (url == null || (!"lb".equals(url.getScheme()) && !"lb".equals(schemePrefix))) {
return chain.filter(exchange);
}
//其他代码略...
URI requestUri = exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR);
//微服务模块ID
String serviceId = requestUri.getHost();
//存活的服务列表
Set<LoadBalancerLifecycle> supportedLifecycleProcessors = LoadBalancerLifecycleValidator
.getSupportedLifecycleProcessors(clientFactory.getInstances(serviceId, LoadBalancerLifecycle.class),
RequestDataContext.class, ResponseData.class, ServiceInstance.class);
//创建负载均衡函数choose入参的Request
DefaultRequest<RequestDataContext> lbRequest = new DefaultRequest<>(
new RequestDataContext(new RequestData(exchange.getRequest()), getHint(serviceId)));
//通过 负载均衡客户端工厂 获取 负载均衡配置
LoadBalancerProperties loadBalancerProperties = clientFactory.getProperties(serviceId);
return choose(lbRequest, serviceId, supportedLifecycleProcessors).doOnNext(response -> {
//通过负载均衡获取到服务后的处理代码略...
}).then(chain.filter(exchange)).doOnError(/*异常处理略*/).doOnSuccess(/*成功处理略*/);
}
private Mono<Response<ServiceInstance>> choose(Request<RequestDataContext> lbRequest, String serviceId,
Set<LoadBalancerLifecycle> supportedLifecycleProcessors) {
//本次核心代码,通过 负载均衡客户端工厂 获取 负载均衡
ReactorLoadBalancer<ServiceInstance> loadBalancer = this.clientFactory.getInstance(serviceId,
ReactorServiceInstanceLoadBalancer.class);
if (loadBalancer == null) {
throw new NotFoundException("No loadbalancer available for " + serviceId);
}
supportedLifecycleProcessors.forEach(lifecycle -> lifecycle.onStart(lbRequest));
return loadBalancer.choose(lbRequest);
}
拦截器获取 负载均衡ReactorLoadBalancer
,是通过LoadBalancerClientFactory
类的函数getInstance
,这个类是 ReactiveLoadBalancerClientFilter
构造入参。找到拦截器初始化的代码org.springframework.cloud.gateway.config.GatewayReactiveLoadBalancerClientAutoConfiguration
,是spring的自动注入配置类
@Bean
@ConditionalOnBean(LoadBalancerClientFactory.class)
@ConditionalOnMissingBean(ReactiveLoadBalancerClientFilter.class)
@ConditionalOnEnabledGlobalFilter
public ReactiveLoadBalancerClientFilter gatewayLoadBalancerClientFilter(LoadBalancerClientFactory factory,
GatewayLoadBalancerProperties properties) {
return new ReactiveLoadBalancerClientFilter(factory, properties);
}
那就找到注入LoadBalancerClientFactory
类的代码。也没看到初始化负载均衡ReactorLoadBalancer<ServiceInstance>
的代码
@ConditionalOnMissingBean
@Bean
public LoadBalancerClientFactory loadBalancerClientFactory(LoadBalancerClientsProperties properties) {
LoadBalancerClientFactory factory = new LoadBalancerClientFactory(properties);
factory.setConfigurations(this.configurations.getIfAvailable(Collections::emptyList));
return factory;
}
下面就看到,挠头的配置类初始化方式
进入LoadBalancerClientFactory
的构造函数,发现LoadBalancerClientConfiguration
负载均衡客户端配置类。
public LoadBalancerClientFactory(LoadBalancerClientsProperties properties) {
super(LoadBalancerClientConfiguration.class, NAMESPACE, PROPERTY_NAME);
this.properties = properties;
}
LoadBalancerClientConfiguration
负载均衡客户端配置类,请留意这里,这个配置类不是项目启动自动注入。也就是说,参考reactorServiceInstanceLoadBalancer
函数自定义注入spring是不对的。就因为这个情况,卡了好几天,因为它是运行时初始化的。
//当设置为 false 时,Spring 不会为配置类生成代理对象。也就是不会自动注入配置类
@Configuration(proxyBeanMethods = false)
@ConditionalOnDiscoveryEnabled
public class LoadBalancerClientConfiguration {
@Bean
@ConditionalOnMissingBean
public ReactorLoadBalancer<ServiceInstance> reactorServiceInstanceLoadBalancer(Environment ev,
LoadBalancerClientFactory factory) {
//自定义ReactorLoadBalancer注入spring,name获取为null。实际这个应该是serviceId
//获取配置参数 loadbalancer.client.name
String name = ev.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
return new RoundRobinLoadBalancer( factory.getLazyProvider(name, ServiceInstanceListSupplier.class), name);
}
//其他略
}
换个方向,分析函数getInstance
,实现是在org.springframework.cloud.context.named.NamedContextFactory
类里。看到这里,就能发现有一个map字段contexts
,存着每个 微服务模块 的上下文AnnotationConfigApplicationContext
。只有请求到对应 微服务模块 才初始化对应的 负载均衡类。
代码做了简化处理
public <T> T getInstance(String serviceId, Class<T> type) {
return (T)this.getContext(serviceId).getBean(type);
}
protected AnnotationConfigApplicationContext getContext(String serviceId) {
if (!this.contexts.containsKey(serviceId)) {
this.contexts.put(serviceId, this.createContext(serviceId));
}
return (AnnotationConfigApplicationContext)this.contexts.get(serviceId);
}
protected AnnotationConfigApplicationContext createContext(String serviceId) {
AnnotationConfigApplicationContext context /*上下文获取 代码略...*/;
//上下文配置注册 代码略...
//-----核心代码,注入配置类 LoadBalancerClientConfiguration
context.register(new Class[]{PropertyPlaceholderAutoConfiguration.class, this.defaultConfigType});
//-----核心代码,注入配置类 需要的 Environment 配置参数 loadbalancer.client.name
context.getEnvironment().getPropertySources().addFirst(new MapPropertySource(this.propertySourceName, Collections.singletonMap(this.propertyName, serviceId)));
//其他 代码略...
//上下文 刷新并启动,类似初始化-只能调用一次
context.refresh();
return context;
}
通过上面的代码分析,spring通过运行时注入负载均衡,可能是考虑 网关无法感知到有多少微服务模块。
如果自定义负载均衡,正常的启动注入 每个微服务模块的负载均衡器,代码开发是比较麻烦的,需要明确列出所有的微服务,还需要考虑让负载均衡获取到存活的 服务列表。
所以最后 通过 覆盖函数getInstance
,替换为自定义 负载均衡,这种代码量最简单粗暴(在不考虑性能的情况下)
测试日志
>>>>>>>>>自定义全局过滤器-注册
>>>>>>>>>自定义路由工厂-注册
------->自定义全局过滤器: /a/login
自定义全局过滤器-获取IP: null
自定义路由工厂-路由负载均衡策略: 默认轮询 org.springframework.cloud.loadbalancer.core.RoundRobinLoadBalancer@1fc654d2
自定义路由工厂-路由负载均衡策略: 默认 删除第一个: [reactorServiceInstanceLoadBalancer]
自定义路由工厂-路由负载均衡策略: 自定义 创建
自定义路由负载均衡->初始化,负载均衡策略: 先找请求IP查询IP相同的服务、再找和网关IP相同的服务、最后轮询其他IP服务
自定义路由工厂-路由负载均衡策略: 自定义 注入
自定义路由工厂-路由负载均衡策略: 注入后获取 *.*.*.config.TestLocalLockIpLoadBalancer@72e4d1fe
自定义路由负载均衡---->
自定义路由负载均衡--- 服务数量:1
自定义路由负载均衡--- 服务只有一个: 192.168.1.10
------->自定义全局过滤器: /a/login
自定义全局过滤器-获取IP: null
自定义路由负载均衡---->
自定义路由负载均衡--- 服务数量:2
自定义路由负载均衡--- 策略2: 测试处理 192.168.1.17
------->自定义全局过滤器: /a/login
自定义全局过滤器-获取IP: 192.168.1.10
自定义路由负载均衡---->
自定义路由负载均衡--- 服务数量:2
自定义路由负载均衡--- 策略1: 自己处理 192.168.1.10
测试截图
END
用的公司项目测试,抱歉内容做了脱敏处理。
仅供参考-请结合实际情况使用
原文地址:https://blog.csdn.net/privateobject/article/details/145143269
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!