Nacos多人协同开发解决方案

一、存在问题

同一台Nacos服务多个开发人员在本地开发,就会出现一个问题:也就是你本来想在本地调试你修改的代码,发现服务调到别的同事的服务上去了,导致调试很麻烦。例如:有甲和乙两个开发人员,分别启动了 A1、A2和B1、B2 服务,然后 甲就有可能调用调 B1 服务上,我预期的是甲调用在 A1 服务上 优先使用同IP服务(本地服务优先调用)

二、原生负载策略

对于原生子自带的负载均衡有以下几种,想要自定义负载均衡逻辑只需修改getInstanceResponsegetClusterinstanceResponse方法

负载均衡器 实现
RandomLoadBalancer 基于随机访问的负载均衡策略随机地从候选服务实例中选择一个实例来处理请求
NacosLoadBalancer 基于Nacos权重的负载均衡策略:根据服务实例的权重来决定请求的分配比例。权重越高的实例将获得更多的请求
RoundRobinLoadBalancer 基于轮询的负载均衡策略按顺序轮询每一个实例

三、解决方案

区分开发环境(优先走本地IP)和生产环境(集群优先策略)走不同的负载均衡策略,生产环境也可用原生的负载均衡,当然除了以下的方案也可以参考命名空间以及克隆多分组的配置方案,但是比较不推荐比较繁琐重复,每新加一个人都得去配置一次

  • 新增本地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
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    package com.xxx.gateway.util;
    import org.springframework.util.ObjectUtils;
    import java.net.*;
    import java.util.ArrayList;
    import java.util.Enumeration;
    import java.util.List;
    import java.util.Optional;

    public class IpUtil {

    /**
    * 获取本机所有网卡信息,得到所有IP信息
    * @return
    * @throws SocketException
    */
    public static List<Inet4Address> getLocalIp4AddressFromNetworkInterface() throws SocketException {
    List<Inet4Address> addresses = new ArrayList<>(1);

    // 所有网络接口信息
    Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces();
    if (ObjectUtils.isEmpty(networkInterfaces)) {
    return addresses;
    }
    while (networkInterfaces.hasMoreElements()) {
    NetworkInterface networkInterface = networkInterfaces.nextElement();
    //滤回环网卡、点对点网卡、非活动网卡、虚拟网卡并要求网卡名字是eth或ens开头
    if (!isValidInterface(networkInterface)) {
    continue;
    }

    // 所有网络接口的IP地址信息
    Enumeration<InetAddress> inetAddresses = networkInterface.getInetAddresses();
    while (inetAddresses.hasMoreElements()) {
    InetAddress inetAddress = inetAddresses.nextElement();
    // 判断是否是IPv4,并且内网地址并过滤回环地址.
    if (isValidAddress(inetAddress)) {
    addresses.add((Inet4Address) inetAddress);
    }
    }
    }
    return addresses;
    }

    /**
    * 过滤回环网卡、点对点网卡、非活动网卡、虚拟网卡并要求网卡名字是eth或ens开头
    * @param ni 网卡
    * @return 如果满足要求则true,否则false
    */
    private static boolean isValidInterface(NetworkInterface ni) throws SocketException {
    return !ni.isLoopback() && !ni.isPointToPoint() && ni.isUp() && !ni.isVirtual()
    && (ni.getName().startsWith("eth") || ni.getName().startsWith("ens"));
    }

    /**
    * 判断是否是IPv4,并且内网地址并过滤回环地址.
    */
    private static boolean isValidAddress(InetAddress address) {
    return address instanceof Inet4Address && address.isSiteLocalAddress() && !address.isLoopbackAddress();
    }

    /**
    * 通过Socket 唯一确定一个IP
    * 当有多个网卡的时候,使用这种方式一般都可以得到想要的IP。甚至不要求外网地址8.8.8.8是可连通的
    * @return
    * @throws SocketException
    */
    private static Optional<Inet4Address> getIpBySocket() throws SocketException {
    try (final DatagramSocket socket = new DatagramSocket()) {
    socket.connect(InetAddress.getByName("8.8.8.8"), 10002);
    if (socket.getLocalAddress() instanceof Inet4Address) {
    return Optional.of((Inet4Address) socket.getLocalAddress());
    }
    } catch (UnknownHostException networkInterfaces) {
    throw new RuntimeException(networkInterfaces);
    }
    return Optional.empty();
    }

    /**
    * 获取本地IPv4地址
    * @return
    * @throws SocketException
    */
    public static Optional<Inet4Address> getLocalIp4Address() throws SocketException {
    final List<Inet4Address> inet4Addresses = getLocalIp4AddressFromNetworkInterface();
    if (inet4Addresses.size() != 1) {
    final Optional<Inet4Address> ipBySocketOpt = getIpBySocket();
    if (ipBySocketOpt.isPresent()) {
    return ipBySocketOpt;
    } else {
    return inet4Addresses.isEmpty() ? Optional.empty() : Optional.of(inet4Addresses.get(0));
    }
    }
    return Optional.of(inet4Addresses.get(0));
    }

    }
  • 新增本地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
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    package com.xxx.gateway.loadbalance;

    import com.alibaba.cloud.commons.lang.StringUtils;
    import com.alibaba.cloud.nacos.NacosDiscoveryProperties;
    import com.alibaba.cloud.nacos.balancer.NacosBalancer;
    import com.alibaba.nacos.client.naming.utils.CollectionUtils;
    import com.maiyawx.gateway.util.IpUtil;
    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.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;
    import reactor.core.publisher.Mono;

    import java.net.SocketException;
    import java.util.Collections;
    import java.util.List;
    import java.util.Set;
    import java.util.stream.Collectors;
    @Slf4j
    public class NacosLocalFirstLoadBalancer implements ReactorServiceInstanceLoadBalancer {

    private final String serviceId;

    private ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;

    private final NacosDiscoveryProperties nacosDiscoveryProperties;

    private Set<String> localIps;

    public NacosLocalFirstLoadBalancer(ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider, String serviceId, NacosDiscoveryProperties nacosDiscoveryProperties) {
    this.serviceId = serviceId;
    this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider;
    this.nacosDiscoveryProperties = nacosDiscoveryProperties;

    try {
    // 获取本地IPv4地址
    this.localIps = Collections.singleton(IpUtil.getLocalIp4Address().get().toString().replaceAll("/", ""));
    } catch (SocketException e) {
    throw new RuntimeException(e);
    }
    }

    @Override
    public Mono<Response<ServiceInstance>> choose(Request request) {
    ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider
    .getIfAvailable(NoopServiceInstanceListSupplier::new);
    return supplier.get().next().map(this::getInstanceResponse);
    }

    /**
    * 优先获取与本地IP一致的服务,否则获取同一集群服务
    *
    * @param serviceInstances
    * @return
    */
    private Response<ServiceInstance> getInstanceResponse(
    List<ServiceInstance> serviceInstances) {
    if (serviceInstances.isEmpty()) {
    log.warn("No servers available for service: " + this.serviceId);
    return new EmptyResponse();
    }
    // 过滤与本机IP地址一样的服务实例
    if (!CollectionUtils.isEmpty(this.localIps)) {
    for (ServiceInstance instance : serviceInstances) {
    String host = instance.getHost();
    if (this.localIps.contains(host)) {
    return new DefaultResponse(instance);
    }
    }
    }
    return this.getClusterInstanceResponse(serviceInstances);
    }

    /**
    * 同一集群下优先获取
    *
    * @param serviceInstances
    * @return
    */
    private Response<ServiceInstance> getClusterInstanceResponse(
    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);
    }

    ServiceInstance instance = NacosBalancer.getHostByRandomWeight3(instancesToChoose);
    return new DefaultResponse(instance);
    } catch (Exception e) {
    log.warn("NacosLoadBalancer error", e);
    return null;
    }
    }

    }
  • 新增集群优先策略

    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
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    package com.xxx.gateway.loadbalance;


    import com.alibaba.cloud.commons.lang.StringUtils;
    import com.alibaba.cloud.nacos.NacosDiscoveryProperties;
    import com.alibaba.cloud.nacos.balancer.NacosBalancer;
    import com.alibaba.nacos.client.naming.utils.CollectionUtils;
    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.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;
    import reactor.core.publisher.Mono;

    import java.util.List;
    import java.util.stream.Collectors;

    @Slf4j
    public class NacosClusterLoadBalancer implements ReactorServiceInstanceLoadBalancer {

    private final String serviceId;

    private ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;

    private final NacosDiscoveryProperties nacosDiscoveryProperties;

    public NacosClusterLoadBalance(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);
    }

    /**
    * 获取同一集群服务
    *
    * @param serviceInstances
    * @return
    */
    private Response<ServiceInstance> getInstanceResponse(
    List<ServiceInstance> serviceInstances) {
    if (serviceInstances.isEmpty()) {
    log.warn("No servers available for service: " + this.serviceId);
    return new EmptyResponse();
    }
    return this.getClusterInstanceResponse(serviceInstances);
    }

    /**
    * 同一集群下优先获取
    *
    * @param serviceInstances
    * @return
    */
    private Response<ServiceInstance> getClusterInstanceResponse(
    List<ServiceInstance> serviceInstances) {

    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);
    }

    ServiceInstance instance = NacosBalancer.getHostByRandomWeight3(instancesToChoose);
    return new DefaultResponse(instance);
    } catch (Exception e) {
    log.warn("NacosLoadBalancer error", e);
    return null;
    }
    }

    }
  • 新增loadBalancer自动配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    package com.xxx.gateway.loadbalance;


    import com.alibaba.cloud.nacos.ConditionalOnNacosDiscoveryEnabled;
    import org.springframework.boot.context.properties.EnableConfigurationProperties;
    import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClients;
    import org.springframework.context.annotation.Configuration;

    /**
    * {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration
    * Auto-configuration} that sets up LoadBalancer for Nacos.
    */
    @Configuration(proxyBeanMethods = false)
    @EnableConfigurationProperties
    @ConditionalOnNacosDiscoveryEnabled
    @LoadBalancerClients(defaultConfiguration = NacosLocalLoadBalancerClientConfiguration.class)
    public class LocalFirstLoadBalancerNacosAutoConfiguration {

    }
  • 新增负载均衡配置类

    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
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    package com.xxx.gateway.loadbalance;


    import com.alibaba.cloud.nacos.NacosDiscoveryProperties;
    import com.alibaba.cloud.nacos.loadbalancer.NacosLoadBalancerClientConfiguration;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
    import org.springframework.cloud.client.ConditionalOnDiscoveryEnabled;
    import org.springframework.cloud.client.ServiceInstance;
    import org.springframework.cloud.loadbalancer.core.RandomLoadBalancer;
    import org.springframework.cloud.loadbalancer.core.ReactorLoadBalancer;
    import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
    import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.Import;
    import org.springframework.core.env.Environment;

    /**
    * nacos 负载均衡同IP 同区域有限
    */
    @Configuration(proxyBeanMethods = false)
    @ConditionalOnDiscoveryEnabled
    // 这里引入 nacos 默认客户端配置,否则的话需要添加 配置 spring.cloud.loadbalancer.nacos.enabled = true
    @Import(NacosLoadBalancerClientConfiguration.class)
    public class NacosLocalLoadBalancerClientConfiguration {
    private static final Logger log = LoggerFactory.getLogger(NacosLocalLoadBalancerClientConfiguration.class);


    /**
    * 本地优先策略
    * @param environment 环境变量
    * @param loadBalancerClientFactory 工厂
    * @param nacosDiscoveryProperties 属性
    * @return ReactorLoadBalancer
    */
    @Bean
    @ConditionalOnProperty(value = "spring.cloud.loadbalancer.local-first", havingValue = "dev")
    public ReactorLoadBalancer<ServiceInstance> nacosLocalFirstLoadBalancer(Environment environment,
    LoadBalancerClientFactory loadBalancerClientFactory,
    NacosDiscoveryProperties nacosDiscoveryProperties){
    String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
    if(log.isDebugEnabled()) {
    log.debug("Use nacos local first load balancer for {} service", name);
    }
    return new NacosLocalFirstLoadBalancer(
    loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class),
    name, nacosDiscoveryProperties);
    }

    /**
    * 集群优先策略
    * @param environment 环境变量
    * @param loadBalancerClientFactory 工厂
    * @return NacosClusterLoadBalance
    */
    @Bean
    @ConditionalOnProperty(value = "spring.cloud.loadbalancer.local-first", havingValue = "prod")
    ReactorLoadBalancer<ServiceInstance> clusterLoadBalancer(Environment environment,
    LoadBalancerClientFactory loadBalancerClientFactory,
    NacosDiscoveryProperties nacosDiscoveryProperties) {
    String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
    // 集群轮询
    return new NacosClusterLoadBalance(
    loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class),
    name, nacosDiscoveryProperties);
    }

    /**
    * 所有ip轮询策略
    * @param environment 环境变量
    * @param loadBalancerClientFactory 工厂
    * @return RandomLoadBalancer
    */
    @Bean
    @ConditionalOnProperty(value = "spring.cloud.loadbalancer.local-first", havingValue = "all")
    ReactorLoadBalancer<ServiceInstance> randomLoadBalancer(Environment environment,
    LoadBalancerClientFactory loadBalancerClientFactory) {
    String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
    // 轮询

    return new RandomLoadBalancer(
    loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), name);
    }
    }
  • application.yml文件里新增配置

    通过local-first来区分不同环境使用不同的负载策略,cluster-name用来区别不同用户同一服务走哪个组,这样可以确保只走自己的模块,当开了VPN后除了本地IP地址外还会有额外的局域网适配器从而导致取得不是本地IP,就可以通过preferred-networks来指定网段

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    spring:
    cloud:
    loadbalancer:
    local-first: dev # 生产环境时为prod
    nacos:
    discovery:
    server-addr: xxx
    namespace: xxx
    group: DEFAULT_GROUP
    cluster-name: weix # 指定不同用户走自己服务,`weix`不与他人一样即可
    config:
    server-addr: ${spring.cloud.nacos.discovery.server-addr}
    namespace: ${spring.cloud.nacos.discovery.namespace}
    group: ${spring.cloud.nacos.discovery.group}
    username: xxx
    password: xxx
    inetutils:
    preferred-networks: 192.168.0 # 指定网段,用于开VPN后同局域网内联调代码
    config:
    import:
    - nacos:gateway.yml