提示:本文侧重使用,原理解析部分较少
前言
目前大部分系统在生产环境部署使用双中心(同城双活)策略来提高系统的可用性,从而也产生了本文面临的问题:两个中心的集群的服务,需要优先调用本中心内的子服务,如果本中心子服务不可用,则调用另一个中心的子服务;
系统使用Openfeign + LoadBalancer + Nacos,让我们看一下基于此技术栈如何使用基于loadbalancer或基于nacos,实现以上需求。
版本信息:
spring-boot:2.6.13
spring-cloud:2021.0.5
spring-cloud-alibaba:2021.0.5.0
nacos-client:2.2.0
提示:以下是本篇文章正文内容,下面案例可供参考
一、基于loadbalancer
1.原理浅析
基于loadbalancer的ZonePreferenceServiceInstanceListSupplier类可以实现,基本原理如下:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package org.springframework.cloud.loadbalancer.core;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.loadbalancer.config.LoadBalancerZoneConfig;
import reactor.core.publisher.Flux;
public class ZonePreferenceServiceInstanceListSupplier extends DelegatingServiceInstanceListSupplier {
private final String ZONE = "zone";
private final LoadBalancerZoneConfig zoneConfig;
private String zone;
public ZonePreferenceServiceInstanceListSupplier(ServiceInstanceListSupplier delegate, LoadBalancerZoneConfig zoneConfig) {
super(delegate);
this.zoneConfig = zoneConfig;
}
public Flux<List<ServiceInstance>> get() {
return ((Flux)this.getDelegate().get()).map(this::filteredByZone);
}
private List<ServiceInstance> filteredByZone(List<ServiceInstance> serviceInstances) {
if (this.zone == null) {
this.zone = this.zoneConfig.getZone();
}
if (this.zone != null) {
List<ServiceInstance> filteredInstances = new ArrayList();
Iterator var3 = serviceInstances.iterator();
while(var3.hasNext()) {
ServiceInstance serviceInstance = (ServiceInstance)var3.next();
String instanceZone = this.getZone(serviceInstance);
if (this.zone.equalsIgnoreCase(instanceZone)) {
filteredInstances.add(serviceInstance);
}
}
if (filteredInstances.size() > 0) {
return filteredInstances;
}
}
return serviceInstances;
}
private String getZone(ServiceInstance serviceInstance) {
Map<String, String> metadata = serviceInstance.getMetadata();
return metadata != null ? (String)metadata.get("zone") : null;
}
}
ZonePreferenceServiceInstanceListSupplier类中会获取注册列表中所有服务实例,获取每个服务的元数据中的“zone”配置项,和当前服务的“zone”配置项进行“equalsIgnoreCase”对比,如果一致则加入过滤列表,最终如果过滤列表不为空,则返回过滤列表,否则返回原始的注册列表。
2.具体使用
基于以上原理,我们可以使用yaml配置来指定我们的区域偏好策略,以及我们所属的区域
1)服务提供方
# 方式一(基于loadbalancer):
spring:
application:
name: app2
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
namespace: demo
# 设置元数据
metadata:
# 设置区域名称-机房1
zone: jifang1
2)服务消费方
# 方式一(基于loadbalancer):
spring:
application:
name: app1
cloud:
loadbalancer:
# 指定负载均衡模式为区域偏好
configurations: zone-preference
# 指定区域偏好
zone: jifang1
nacos:
discovery:
server-addr: 127.0.0.1:8848
namespace: demo
# 设置元数据
metadata:
# 设置区域名称-机房1
zone: jifang1
二、基于nacos
1.原理浅析
/*
* Copyright 2013-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.cloud.nacos.loadbalancer;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javax.annotation.PostConstruct;
import com.alibaba.cloud.commons.lang.StringUtils;
import com.alibaba.cloud.nacos.NacosDiscoveryProperties;
import com.alibaba.cloud.nacos.balancer.NacosBalancer;
import com.alibaba.cloud.nacos.util.InetIPv6Utils;
import com.alibaba.nacos.client.naming.utils.CollectionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Mono;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.DefaultResponse;
import org.springframework.cloud.client.loadbalancer.EmptyResponse;
import org.springframework.cloud.client.loadbalancer.Request;
import org.springframework.cloud.client.loadbalancer.Response;
import org.springframework.cloud.loadbalancer.core.NoopServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.core.ReactorServiceInstanceLoadBalancer;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
/**
* see original.
* {@link org.springframework.cloud.loadbalancer.core.RoundRobinLoadBalancer}
*
* @author XuDaojie
* @since 2021.1
*/
public class NacosLoadBalancer implements ReactorServiceInstanceLoadBalancer {
private static final Logger log = LoggerFactory.getLogger(NacosLoadBalancer.class);
private final String serviceId;
private ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;
private final NacosDiscoveryProperties nacosDiscoveryProperties;
private static final String IPV4_REGEX = "((2(5[0-5]|[0-4]\\d))|[0-1]?\\d{1,2})(.((2(5[0-5]|[0-4]\\d))|[0-1]?\\d{1,2})){3}";
private static final String IPV6_KEY = "IPv6";
/**
* Storage local valid IPv6 address, it's a flag whether local machine support IPv6 address stack.
*/
public static String ipv6;
@Autowired
private InetIPv6Utils inetIPv6Utils;
@PostConstruct
public void init() {
String ip = nacosDiscoveryProperties.getIp();
if (StringUtils.isNotEmpty(ip)) {
ipv6 = Pattern.matches(IPV4_REGEX, ip) ? nacosDiscoveryProperties.getMetadata().get(IPV6_KEY) : ip;
}
else {
ipv6 = inetIPv6Utils.findIPv6Address();
}
}
private List<ServiceInstance> filterInstanceByIpType(List<ServiceInstance> instances) {
if (StringUtils.isNotEmpty(ipv6)) {
List<ServiceInstance> ipv6InstanceList = new ArrayList<>();
for (ServiceInstance instance : instances) {
if (Pattern.matches(IPV4_REGEX, instance.getHost())) {
if (StringUtils.isNotEmpty(instance.getMetadata().get(IPV6_KEY))) {
ipv6InstanceList.add(instance);
}
}
else {
ipv6InstanceList.add(instance);
}
}
// Provider has no IPv6, should use IPv4.
if (ipv6InstanceList.size() == 0) {
return instances.stream()
.filter(instance -> Pattern.matches(IPV4_REGEX, instance.getHost()))
.collect(Collectors.toList());
}
else {
return ipv6InstanceList;
}
}
return instances.stream()
.filter(instance -> Pattern.matches(IPV4_REGEX, instance.getHost()))
.collect(Collectors.toList());
}
public NacosLoadBalancer(
ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider,
String serviceId, NacosDiscoveryProperties nacosDiscoveryProperties) {
this.serviceId = serviceId;
this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider;
this.nacosDiscoveryProperties = nacosDiscoveryProperties;
}
@Override
public Mono<Response<ServiceInstance>> choose(Request request) {
ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider
.getIfAvailable(NoopServiceInstanceListSupplier::new);
return supplier.get().next().map(this::getInstanceResponse);
}
private Response<ServiceInstance> getInstanceResponse(
List<ServiceInstance> serviceInstances) {
if (serviceInstances.isEmpty()) {
log.warn("No servers available for service: " + this.serviceId);
return new EmptyResponse();
}
try {
String clusterName = this.nacosDiscoveryProperties.getClusterName();
List<ServiceInstance> instancesToChoose = serviceInstances;
if (StringUtils.isNotBlank(clusterName)) {
List<ServiceInstance> sameClusterInstances = serviceInstances.stream()
.filter(serviceInstance -> {
String cluster = serviceInstance.getMetadata()
.get("nacos.cluster");
return StringUtils.equals(cluster, clusterName);
}).collect(Collectors.toList());
if (!CollectionUtils.isEmpty(sameClusterInstances)) {
instancesToChoose = sameClusterInstances;
}
}
else {
log.warn(
"A cross-cluster call occurs,name = {}, clusterName = {}, instance = {}",
serviceId, clusterName, serviceInstances);
}
instancesToChoose = this.filterInstanceByIpType(instancesToChoose);
ServiceInstance instance = NacosBalancer
.getHostByRandomWeight3(instancesToChoose);
return new DefaultResponse(instance);
}
catch (Exception e) {
log.warn("NacosLoadBalancer error", e);
return null;
}
}
}
NacosLoadBalancer类中会获取注册列表中所有服务实例,获取每个服务的元数据中的“nacos.cluster”配置项,和当前服务的“spring.cloud.nacos.discovery.cluster-name”配置项进行“equals”对比,如果一致则加入同集群列表,最终如果同集群列表不为空,则返回同集群列表,否则返回原始的注册列表。
2.具体使用
基于以上原理,我们可以使用yaml配置来指定我们的集群名称,进而实现同集群优先调用
1)服务提供方
# 方式二(基于nacos):
spring:
application:
name: app2
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
namespace: demo
# 指定集群名-机房1
cluster-name: jifang1
2)服务消费方
# 方式二(基于nacos):
spring:
application:
name: app1
cloud:
loadbalancer:
# 指定负载均衡模式为nacos
nacos:
enabled: true
nacos:
discovery:
server-addr: 127.0.0.1:8848
namespace: demo
# 指定集群名-机房1
cluster-name: jifang1
总结
以上就是同机房优先调用的实现方式。