spring cloud eureka 服务分组隔离

  • spring cloud eureka 作为注册中心,feign 服务之间调用,原生不支持服务的隔离,
    比如以下场景: 服务A 调用服务 B(b1,b2),某些情况下只想让A请求到b1,实现服务之间的分组隔离
    或者 共用注册中心,配置中心,公共的服务模块,开发人员本机调试的时候不用启动大量的服务,导致开发机运行缓慢

实现思路

  • 其实要实现很简单,参考MQ消息分组消费的原理,只需要在服务注册的时候,将每个服务实例分组标示好,然后自定义实现负载均衡的策略,根据服务消费者的分组名找到对应分组的服务提供者,选择性的请求就可以

负载均衡

  • 客户端负载均衡,客户端负载均衡策略,由客户端实现,客户端自己决定需要调用那一个服务 ribbon就是属于客户端负载模式
  • 服务端负载模式,服务端收到请求,由服务端决定调用那一个服务,spring cloud getway就是属于服务端负载均衡模式,另外 还有典型的ngxin

Ribbon 负载均衡规则策略

  • IRule 接口
public interface IRule{
    /*
     * choose one alive server from lb.allServers or
     * lb.upServers according to key
     * 
     * @return choosen Server object. NULL is returned if none
     *  server is available 
     */

    public Server choose(Object key);
    
    public void setLoadBalancer(ILoadBalancer lb);
    
    public ILoadBalancer getLoadBalancer();    
}
  • AbstractLoadBalancerRule 抽象实现
public abstract class AbstractLoadBalancerRule implements IRule, IClientConfigAware {

    private ILoadBalancer lb;
        
    @Override
    public void setLoadBalancer(ILoadBalancer lb){
        this.lb = lb;
    }
     /**
	*ILoadBalancer 维护了所有服务实例的相关信息, com.netflix.loadbalancer.ILoadBalancer#getAllServers
    */
    @Override
    public ILoadBalancer  getLoadBalancer(){
        return lb;
    }      
}
  • RandomRule 随机选择一个
public Server choose(ILoadBalancer lb, Object key) {
        if (lb == null) {
            return null;
        }
        Server server = null;

        while (server == null) {
            if (Thread.interrupted()) {
                return null;
            }
            List<Server> upList = lb.getReachableServers();
            List<Server> allList = lb.getAllServers();

            int serverCount = allList.size();
            if (serverCount == 0) {
                /*
                 * No servers. End regardless of pass, because subsequent passes
                 * only get more restrictive.
                 */
                return null;
            }
			// 随机选择一个
            int index = rand.nextInt(serverCount);
            server = upList.get(index);

            if (server == null) {
                /*
                 * The only time this should happen is if the server list were
                 * somehow trimmed. This is a transient condition. Retry after
                 * yielding.
                 */
                Thread.yield();
                continue;
            }

            if (server.isAlive()) {
                return (server);
            }

            // Shouldn't actually happen.. but must be transient or a bug.
            server = null;
            Thread.yield();
        }

        return server;

    }
  • RoundRobinRule 照线性轮询
public Server choose(ILoadBalancer lb, Object key) {
        if (lb == null) {
            log.warn("no load balancer");
            return null;
        }

        Server server = null;
        int count = 0;
        while (server == null && count++ < 10) {
            List<Server> reachableServers = lb.getReachableServers();
            List<Server> allServers = lb.getAllServers();
            int upCount = reachableServers.size();
            int serverCount = allServers.size();

            if ((upCount == 0) || (serverCount == 0)) {
                log.warn("No up servers available from load balancer: " + lb);
                return null;
            }
			// 照线性轮询
            int nextServerIndex = incrementAndGetModulo(serverCount);
            server = allServers.get(nextServerIndex);

            if (server == null) {
                /* Transient. */
                Thread.yield();
                continue;
            }

            if (server.isAlive() && (server.isReadyToServe())) {
                return (server);
            }

            // Next.
            server = null;
        }

        if (count >= 10) {
            log.warn("No available alive servers after 10 tries from load balancer: "
                    + lb);
        }
        return server;
    }
	
    private int incrementAndGetModulo(int modulo) {
        for (;;) {
            int current = nextServerCyclicCounter.get();
            int next = (current + 1) % modulo;
            if (nextServerCyclicCounter.compareAndSet(current, next))
                return next;
        }
    }
  • RetryRule 重试机制的选择器
public Server choose(ILoadBalancer lb, Object key) {
		long requestTime = System.currentTimeMillis();
		long deadline = requestTime + maxRetryMillis;

		Server answer = null;

		answer = subRule.choose(key);

		if (((answer == null) || (!answer.isAlive()))
				&& (System.currentTimeMillis() < deadline)) {

			InterruptTask task = new InterruptTask(deadline
					- System.currentTimeMillis());

			while (!Thread.interrupted()) {
				answer = subRule.choose(key);

				if (((answer == null) || (!answer.isAlive()))
						&& (System.currentTimeMillis() < deadline)) {
					/* pause and retry hoping it's transient */
					Thread.yield();
				} else {
					break;
				}
			}

			task.cancel();
		}

		if ((answer == null) || (!answer.isAlive())) {
			return null;
		} else {
			return answer;
		}
	}
  • WeightedResponseTimeRule 根据权重计算
public Server choose(ILoadBalancer lb, Object key) {
        if (lb == null) {
            return null;
        }
        Server server = null;

        while (server == null) {
            // get hold of the current reference in case it is changed from the other thread
            List<Double> currentWeights = accumulatedWeights;
            if (Thread.interrupted()) {
                return null;
            }
            List<Server> allList = lb.getAllServers();

            int serverCount = allList.size();

            if (serverCount == 0) {
                return null;
            }

            int serverIndex = 0;

            // last one in the list is the sum of all weights
            double maxTotalWeight = currentWeights.size() == 0 ? 0 : currentWeights.get(currentWeights.size() - 1); 
            // No server has been hit yet and total weight is not initialized
            // fallback to use round robin
            if (maxTotalWeight < 0.001d) {
                server =  super.choose(getLoadBalancer(), key);
                if(server == null) {
                    return server;
                }
            } else {
                // generate a random weight between 0 (inclusive) to maxTotalWeight (exclusive)
                double randomWeight = random.nextDouble() * maxTotalWeight;
                // pick the server index based on the randomIndex
                int n = 0;
                for (Double d : currentWeights) {
                    if (d >= randomWeight) {
                        serverIndex = n;
                        break;
                    } else {
                        n++;
                    }
                }

                server = allList.get(serverIndex);
            }

            if (server == null) {
                /* Transient. */
                Thread.yield();
                continue;
            }

            if (server.isAlive()) {
                return (server);
            }

            // Next.
            server = null;
        }
        return server;
    }
  • ZoneAvoidanceRule 复合判断server所在区域的性能和server的可用性选择服务器 (默认规则)
public Server choose(Object key) {
        ILoadBalancer lb = getLoadBalancer();
        Optional<Server> server = getPredicate().chooseRoundRobinAfterFiltering(lb.getAllServers(), key);
        if (server.isPresent()) {
            return server.get();
        } else {
            return null;
        }       
    }
  • BestAvailableRule 会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,然后选择-一个并发 量最小的服务
public Server choose(Object key) {
        if (loadBalancerStats == null) {
            return super.choose(key);
        }
        List<Server> serverList = getLoadBalancer().getAllServers();
        int minimalConcurrentConnections = Integer.MAX_VALUE;
        long currentTime = System.currentTimeMillis();
        Server chosen = null;
        for (Server server: serverList) {
            ServerStats serverStats = loadBalancerStats.getSingleServerStat(server);
            if (!serverStats.isCircuitBreakerTripped(currentTime)) {
                int concurrentConnections = serverStats.getActiveRequestsCount(currentTime);
                if (concurrentConnections < minimalConcurrentConnections) {
                    minimalConcurrentConnections = concurrentConnections;
                    chosen = server;
                }
            }
        }
        if (chosen == null) {
            return super.choose(key);
        } else {
            return chosen;
        }
    }

自定义规则 ZoneAvoidanceRuleSupport

  • 自定义实现 IRule , 这里我们继承 ZoneAvoidanceRule,
public class ZoneAvoidanceRuleSupport extends ZoneAvoidanceRule {

    @Autowired
    private Environment env;
    @Override
    public Server choose(Object key) {
        //获取服务调用者的groupName
        String appGroup = env.getProperty("eureka.instance.app-group-name");
		
        ILoadBalancer lb = getLoadBalancer();
        //获取所有的服务实例
        List<Server> allServers = lb.getAllServers();
        // 根据分组groupName 过滤,找到对应分组的服务
        List<Server> collect = allServers.stream().filter(server -> {
            DiscoveryEnabledServer discoveryEnabledServer=(DiscoveryEnabledServer)server;
            String serverGroup = discoveryEnabledServer.getInstanceInfo().getAppGroupName();
            if (StrUtil.isBlank(appGroup)){
                return StrUtil.isBlank(serverGroup);
            }else {
                return appGroup.equals(serverGroup);
            }
        }).collect(Collectors.toList());
        // 调用父类的实现,找到一个可用的服务
        Optional<Server> server = getPredicate().chooseRoundRobinAfterFiltering(collect, key);
        if (server.isPresent()) {
            return server.get();
        } else {
            return null;
        }

    }
}

配置分组名

  • 服务提供者和服务消费者同时配置相同分组
  • 服务消费者也可以在启动参数中传递分组名
eureka:
  client:
    serviceUrl:
      defaultZone: http://172.16.1.248:11001/eureka/
# 配置实例的分组名
  instance:
    app-group-name: LUJIA

指定 ribbon 负载规则

  • 服务消费者指定
queenOA-service:
  ribbon:
    NFLoadBalancerRuleClassName: com.queen.oa.api.feign.youpin.ZoneAvoidanceRuleSupport

ok,启动服务,就可以根据不同的分组之间的隔离调用。