目标:根据当前运行时环境,调用不同环境的服务实例,实现环境的隔离。比如本地开发环境,服务间调用时,只会调用本地环境中的服务,而不是调用测试环境中的服务。或者做一个保底处理,当本地开发环境进行代码调试时,优先获取本地环境中的服务节点,若本地没有运行该服务,则调用测试环境中的该服务。这样可以便利我们进行代码自测,只需要将需要测试的服务在本地启动,其余的服务依旧调用测试环境。
实现步骤:
- 在测试环境的 nacos 配置文件中添加 test.server.ips 配置项,指定测试环境的服务地址
- 关闭网关默认的 ribbon 负载均衡器,注册我们自己实习的负载均衡器进行替换
- 负载均衡器中实现环境的切换:根据当前环境,过滤调用注册中心中当前环境下的服务实例
第一步:准备配置文件
准备 nacos 配置文件,分别在 dev & test 环境创建 gateway-service 配置文件:
server:
port: 5000
logging:
level:
root: info
spring:
mvc:
path-match:
matching-strategy: ANT_PATH_MATCHER
application:
name: gateway-service
cloud:
# 切换 LocalRoundRobbinLoadBalancer
loadbalancer:
ribbon:
enabled: false
config:
override-none: true
allow-override: true
override-system-properties: false
# 网关配置
gateway:
discovery:
locator:
enabled: true
routes:
- id: order_route # 路由的唯一标识
uri: lb://nacos-order-service # lb: load-balance 启用负载均衡,并根据服务名称自动转发
# uri: http://nacos-order-service # lb: load-balance 启用负载均衡,并根据服务名称自动转发
predicates: # 断言规则,用于路由匹配
- Path=/route-order-service/**
- MyPredicate=key1,key2
filters:
- StripPrefix=1 # 剔除 url 中的第一层路径: /order-service/
# - AddRequestHeader=my-header,mh
- MyFilter=my-header,mh
globalcors: # 全局的跨域处理
add-to-simple-url-handler-mapping: true # 解决 options 请求被拦截问题
corsConfigurations:
'[/**]':
allowedOrigins: # 允许哪些网站的可以跨域请求, '*' 表示所有
- "*"
# - "https://localhost:8001"
# - "https://localhost:8002"
# - "https://localhost:8003"
allowedMethods: # 允许的跨域的请求方式, '*' 表示所有
- "GET"
- "POST"
- "DELETE"
- "PUT"
- "OPTIONS"
allowedHeaders: "*" # 允许在请求中携带的请求头, '*' 表示所有
allowCredentials: true # 是否允许携带 cookie
maxAge: 360000 # 跨域检测的有效期
# 测试环境服务地址
test:
server:
ips: 124.70.69.96
关键配置在于:
spring:
cloud:
# 关闭默认的负载均衡,切换 LocalRoundRobbinLoadBalancer
loadbalancer:
ribbon:
enabled: false
# 测试环境服务地址
test:
server:
ips: 124.70.69.96
在网关服务中添加两个配置文件:
内容如下:
spring:
main:
allow-bean-definition-overriding: true
cloud:
nacos:
# 通用配置
# server-addr: 127.0.0.1:8000 # 1. nacos 集群服务地址
server-addr: 127.0.0.1:8100 # 1. nacos 服务地址
username: nacos
password: nacos
# 服务治理
discovery:
namespace: 0298b122-a60d-47f5-9be3-9ea149f17185
group: DEFAULT_GROUP
service: nacos-gateway-service
cluster-name: gateway-service-cluster
weight: 1
# 服务配置
config:
namespace: c8cc59da-dea3-44ef-9ff7-055879477406
group: DEFAULT_GROUP
name: gateway-service
file-extension: yaml
refresh-enabled: true
spring:
main:
allow-bean-definition-overriding: true
cloud:
nacos:
# 通用配置
# server-addr: 127.0.0.1:8000 # 1. nacos 集群服务地址
server-addr: 127.0.0.1:8100 # 1. nacos 服务地址
username: nacos
password: nacos
# 服务治理
discovery:
namespace: 0298b122-a60d-47f5-9be3-9ea149f17185
group: DEFAULT_GROUP
service: nacos-gateway-service
cluster-name: gateway-service-cluster
weight: 1
# 服务配置
config:
namespace: 0298b122-a60d-47f5-9be3-9ea149f17185
group: DEFAULT_GROUP
name: gateway-service
file-extension: yaml
refresh-enabled: true
唯一的区别在于配置文件的命名空间不同(dev & test):
# 服务配置
config:
namespace: 0298b122-a60d-47f5-9be3-9ea149f17185
# 服务配置
config:
namespace: c8cc59da-dea3-44ef-9ff7-055879477406
第二步:关闭默认的负载均衡器,并切换我们自己的负载均衡器
关闭默认的负载均衡器已经在上面配置了:
spring:
cloud:
# 关闭默认的负载均衡,切换 LocalRoundRobbinLoadBalancer
loadbalancer:
ribbon:
enabled: false
注册自实现的负载均衡器:
引入 Spring 官方的原生负载均衡依赖(其中整合 Sentinel 依赖可以根据需要删除掉)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>gateway-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>gateway-service</name>
<parent>
<groupId>priv.cqq</groupId>
<artifactId>my-mall</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<dependencies>
<!-- nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!-- gateway -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!-- loadbalancer -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-loadbalancer</artifactId>
</dependency>
<!-- other -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<mainClass>priv.cqq.gateway.GatewayServiceApplication</mainClass>
<skip>true</skip>
</configuration>
</plugin>
</plugins>
</build>
</project>
在启动类中指定负载均衡配置:
@EnableDiscoveryClient
@LoadBalancerClients(defaultConfiguration = DefaultServiceLoadBalancerConfig.class)
@SpringBootApplication
public class GatewayServiceApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayServiceApplication.class, args);
}
}
// @Configuration // 注意,此处不能增加 @Configuration 注解,这样反而会使得下方的 @Bean 方法形参中注入不了 LoadBalancerClientFactory,说明该参数是负载均衡配置类的处理类传入的。
public class DefaultServiceLoadBalancerConfig {
@Bean
public ReactorLoadBalancer<ServiceInstance> myLoadBalancer(Environment environment,
LoadBalancerClientFactory loadBalancerClientFactory,
NacosDiscoveryProperties nacosDiscoveryProperties,
@Value("${spring.profiles.active}") String profile,
@Value("${test.server.ips}") String testServerIps) {
String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
ObjectProvider<ServiceInstanceListSupplier> provider = loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class);
return new LocalRoundRobbinLoadBalancer(provider, name, nacosDiscoveryProperties, profile, Arrays.stream(testServerIps.split(",")).collect(Collectors.toSet()));
}
}
第三步:实现环境隔离的负载均衡器
public class LocalRoundRobbinLoadBalancer implements ReactorServiceInstanceLoadBalancer {
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private final AtomicInteger position;
private final ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;
private final String serviceId;
// ==================================================================
private final NacosDiscoveryProperties nacosDiscoveryProperties;
private final String profile;
private final Set<String> testServerIpSet;
public LocalRoundRobbinLoadBalancer(ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider,
String serviceId,
NacosDiscoveryProperties nacosDiscoveryProperties,
String profile,
Set<String> testServerIpSet) {
this.serviceId = serviceId;
this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider;
this.position = new AtomicInteger(0);
this.nacosDiscoveryProperties = nacosDiscoveryProperties;
this.profile = profile;
this.testServerIpSet = testServerIpSet;
}
@Override
public Mono<Response<ServiceInstance>> choose(Request request) {
// TODO: move supplier to Request?
// Temporary conditional logic till deprecated members are removed.
ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider.getIfAvailable(NoopServiceInstanceListSupplier::new);
return supplier
.get()
.next()
.map(serviceInstances -> processInstanceResponse(supplier, serviceInstances));
}
private Response<ServiceInstance> processInstanceResponse(ServiceInstanceListSupplier supplier, List<ServiceInstance> serviceInstances) {
Response<ServiceInstance> serviceInstanceResponse = getInstanceResponse(serviceInstances);
if (supplier instanceof SelectedInstanceCallback && serviceInstanceResponse.hasServer()) {
((SelectedInstanceCallback) supplier).selectedServiceInstance(serviceInstanceResponse.getServer());
}
return serviceInstanceResponse;
}
private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> allServerInstances) {
if (allServerInstances.isEmpty()) {
log.warn("No servers available for service: " + this.serviceId);
return new EmptyResponse();
}
// 0. 默认非测试、本地开发环境,使用 serviceId 所有的可用服务实例
List<ServiceInstance> serviceInstances = allServerInstances;
// 1. 测试环境: 搜索测试环境中的所有服务节点
if ("test".equals(profile)) {
serviceInstances = findTestInstances(allServerInstances);
}
// 2. 本地开发环境: 搜索本地开发环境中的所有服务节点, 若为空则使用测试环境的所有服务节点
else if ("dev".equals(profile)) {
String ip = nacosDiscoveryProperties.getIp();
serviceInstances = findDevInstances(allServerInstances, ip);
if (CollectionUtils.isEmpty(serviceInstances)) {
serviceInstances = findTestInstances(allServerInstances);
}
}
if (CollectionUtils.isEmpty(serviceInstances)) {
log.warn("No servers available for service: " + this.serviceId);
return new EmptyResponse();
}
// 3. 轮询访问服务节点
Response<ServiceInstance> response = roundRobbin(serviceInstances);
ServiceInstance choose = response.getServer();
log.info("Request {}, servers={}, choose {}",
this.serviceId,
serviceInstances.stream().map(item -> item.getHost() + ":" + item.getPort()).collect(Collectors.toList()),
choose.getHost() + ":" + choose.getPort());
return response;
}
/**
* 搜索本地开发环境中的所有服务节点
*/
private static List<ServiceInstance> findDevInstances(List<ServiceInstance> instances, String ip) {
return instances
.stream()
.filter(instance -> Objects.equals(instance.getHost(), ip))
.collect(Collectors.toList());
}
/**
* 搜索测试环境中的所有服务节点
*/
private List<ServiceInstance> findTestInstances(List<ServiceInstance> instances) {
return instances
.stream()
.filter(instance -> testServerIpSet.contains(instance.getHost()))
.collect(Collectors.toList());
}
/**
* 轮询访问服务实例
*/
private Response<ServiceInstance> roundRobbin(List<ServiceInstance> instances) {
// 1. position 溢出后继续自增会进行递减, 只要保证数值变化是有序的, 就可以保证取模结果是有序的.
int pos = Math.abs(this.position.incrementAndGet());
ServiceInstance instance = instances.get(pos % instances.size());
return new DefaultResponse(instance);
// 2. CAS 自旋修改下次调用服务的索引, 因为取模所以 position 的值不会溢出
// int allServerCount = instances.size();
// while (true) {
// int current = position.get();
// int next = (current + 1) % allServerCount;
// if (position.compareAndSet(current, next)) {
// return new DefaultResponse(instances.get(next));
// }
// }
}
}
核心方法:getInstanceResponse
,实现逻辑已经在代码注释中写的很清楚了。
最后在总结一下:根据当前的 active.profile 过滤不同 ip 的服务实例
- 若 dev 环境,则以当前服务运行所在主机的 ip 进行过滤(本机上启动的其他服务在 nacos 注册时使用的一定也是本机 ip)
- 若 test 环境,则以配置文件中的测试环境 ip 进行过滤