区域权衡策略(ZoneAvoidanceRule)也称为区域规避规则,主要用于在多个区域(zone)的服务器环境中进行负载均衡,并在某些区域出现问题时,能够自动规避这些问题区域,将请求分发到其他正常的区域和服务器上,以确保系统的高可用性和稳定性。
Ribbon 可以根据服务器的物理位置、网络拓扑等因素将服务器划分为不同的区域。例如,可以将服务器分为不同的数据中心、不同的机房等区域。
区域权衡策略会持续监测各个区域以及区域内服务器的健康状况。通过定期发送心跳请求或其他方式来判断服务器是否可用、响应时间是否过长等。
根据区域内服务器的健康状况和性能指标,计算每个区域的权重。如果一个区域内有较多服务器不可用或者响应时间过长,该区域的权重就会降低。
当有新的请求到来时,优先选择权重较高的区域中的服务器来处理请求。如果某个区域的权重过低,会尽量避免将请求分发到该区域的服务器上。
在 application.properties 中,使用如下配置:
user.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.ZoneAvoidanceRule
将在名为 user 的客户端上开启区域权衡策略。
Ribbon 区域权衡策略通过 com.netflix.loadbalancer.ZoneAvoidanceRule 类实现。代码如下:
package com.netflix.loadbalancer; import com.netflix.client.config.IClientConfig; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Random; import java.util.Set; // 源码分析 public class ZoneAvoidanceRule extends PredicateBasedRule { private static final Random random = new Random(); private CompositePredicate compositePredicate; public ZoneAvoidanceRule() { // ZoneAvoidancePredicate 和 AvailabilityPredicate 谓词后续分析…… ZoneAvoidancePredicate zonePredicate = new ZoneAvoidancePredicate(this); AvailabilityPredicate availabilityPredicate = new AvailabilityPredicate(this); // 初始化compositePredicate为包含ZoneAvoidancePredicate和AvailabilityPredicate的复合谓词 this.compositePredicate = this.createCompositePredicate(zonePredicate, availabilityPredicate); } // 创建复杂谓词 private CompositePredicate createCompositePredicate(ZoneAvoidancePredicate p1, AvailabilityPredicate p2) { return CompositePredicate.withPredicates(new AbstractServerPredicate[]{p1, p2}) .addFallbackPredicate(p2) .addFallbackPredicate(AbstractServerPredicate.alwaysTrue()).build(); } // 使用给定的客户端配置重新初始化compositePredicate 复合谓词 // 创建新的 ZoneAvoidancePredicate 和 AvailabilityPredicate 并构建新的复合谓词 public void initWithNiwsConfig(IClientConfig clientConfig) { ZoneAvoidancePredicate zonePredicate = new ZoneAvoidancePredicate(this, clientConfig); AvailabilityPredicate availabilityPredicate = new AvailabilityPredicate(this, clientConfig); this.compositePredicate = this.createCompositePredicate(zonePredicate, availabilityPredicate); } // 根据负载均衡统计信息创建一个区域快照的映射 static Map<String, ZoneSnapshot> createSnapshot(LoadBalancerStats lbStats) { Map<String, ZoneSnapshot> map = new HashMap(); // 遍历可用区域,获取每个区域的快照并放入映射中 Iterator var2 = lbStats.getAvailableZones().iterator(); while(var2.hasNext()) { String zone = (String)var2.next(); // 获取区域 ZoneSnapshot 对象,建立 Map 关系 ZoneSnapshot snapshot = lbStats.getZoneSnapshot(zone); map.put(zone, snapshot); } return map; } // 从给定的区域快照映射和可选区域集合中随机选择一个区域 static String randomChooseZone(Map<String, ZoneSnapshot> snapshot, Set<String> chooseFrom) { if (chooseFrom != null && chooseFrom.size() != 0) { String selectedZone = (String)chooseFrom.iterator().next(); if (chooseFrom.size() == 1) { // 如果集合大小为 1,则直接返回该区域; return selectedZone; } else { // 如果可选区域集合不为空且大小不为 0,则根据区域中的实例数量进行随机选择 // 服务器总数 int totalServerCount = 0; // 区域 String zone; for(Iterator var4 = chooseFrom.iterator(); var4.hasNext(); totalServerCount += ((ZoneSnapshot)snapshot.get(zone)).getInstanceCount()) { zone = (String)var4.next(); } // 获取一个位于 0~totalServerCount 之间的随机数 int index = random.nextInt(totalServerCount) + 1; int sum = 0; // 迭代每个区域 Iterator var6 = chooseFrom.iterator(); while(var6.hasNext()) { // 获取该区域内的服务器数量,并累计当前已经获取数量的和 String zone = (String)var6.next(); sum += ((ZoneSnapshot)snapshot.get(zone)).getInstanceCount(); // 如果随机数大于了服务器熟练累计和,则选择该区域 if (index <= sum) { selectedZone = zone; break; } } return selectedZone; } } else { // 如果为空,则返回 null return null; } } // 根据给定的区域快照(Map<String, ZoneSnapshot>)以及触发负载(triggeringLoad) // 和触发故障百分比(triggeringBlackoutPercentage)来确定可用的区域集合。 // 它通过遍历区域快照,根据每个区域的实例数量、负载情况和电路跳闸计数来判断哪些 // 区域是可用的,同时避免选择负载过高或故障较多的区域,以实现区域规避的负载均衡策略 // 参数说明: // Map<String, ZoneSnapshot> snapshot:区域快照的映射,其中键是区域名称,值是ZoneSnapshot对象,包含了该区域的各种统计信息。 // double triggeringLoad:触发负载,用于判断区域的负载是否过高。 // double triggeringBlackoutPercentage:触发故障百分比,用于判断区域的故障情况是否严重。 public static Set<String> getAvailableZones(Map<String, ZoneSnapshot> snapshot, double triggeringLoad, double triggeringBlackoutPercentage) { if (snapshot.isEmpty()) { // 如果映射为空,则返回 null; return null; } else { Set<String> availableZones = new HashSet(snapshot.keySet()); if (availableZones.size() == 1) { // 如果只有一个区域,则直接返回该区域集合; return availableZones; } else { // 创建一个名为worstZones的集合,用于存储负载最高的区域,即最差的区域 Set<String> worstZones = new HashSet(); // 初始化最大负载maxLoadPerServer为 0.0 double maxLoadPerServer = 0.0; // 设置标志 limitedZoneAvailability 表示是否存在区域可用性限制 boolean limitedZoneAvailability = false; Iterator var10 = snapshot.entrySet().iterator(); while(true) { while(var10.hasNext()) { Map.Entry<String, ZoneSnapshot> zoneEntry = (Map.Entry)var10.next(); String zone = (String)zoneEntry.getKey(); ZoneSnapshot zoneSnapshot = (ZoneSnapshot)zoneEntry.getValue(); int instanceCount = zoneSnapshot.getInstanceCount(); if (instanceCount == 0) { // 如果某个区域中的服务器数量为0 // 则将该区域从可用区域集合中移除,并且将 limitedZoneAvailability // 设置为 true,避免后续最大负载小于触发负载返回整个 availableZones 集合 availableZones.remove(zone); limitedZoneAvailability = true; } else { // 当前区域服务器负载 double loadPerServer = zoneSnapshot.getLoadPerServer(); // getCircuitTrippedCount() 获取电路跳闸计数 // instanceCount 服务器数量 // triggeringBlackoutPercentage 触发停电百分比 // 如果该区域的电路跳闸计数与实例数量的比例小于触发故障百分比,并且负载不小于 0.0 if (!((double)zoneSnapshot.getCircuitTrippedCount() / (double)instanceCount >= triggeringBlackoutPercentage) && !(loadPerServer < 0.0)) { // 比较当前区域的负载与最大负载 // 如果负载与最大负载之差的绝对值小于一个极小值(1.0E-6),将该区域添加到worstZones集合中 if (Math.abs(loadPerServer - maxLoadPerServer) < 1.0E-6) { // 表示非常接近(正向/负向)最大负载,添加到最差区域 // 这也可能是由于浮点数表示不精准,才有这个写法 // 例如 当前 0.30000000000000004 负载,最大负载为 0.3 // 0.30000000000000004 > 0.3 成立的,其实我们不想让他成立 worstZones.add(zone); } else if (loadPerServer > maxLoadPerServer) { // 表示已经超过最大负载,优先踢出去,比前面 if 条件下的区域负载更糟糕 // 如果负载大于最大负载,更新最大负载为当前负载, // 清空worstZones集合并将该区域添加到其中 maxLoadPerServer = loadPerServer; worstZones.clear(); worstZones.add(zone); } } else { // 从可用区域中移除当前区域 availableZones.remove(zone); limitedZoneAvailability = true; } } } // 如果最大负载小于触发负载且没有区域可用性限制,则返回所有可用区域; if (maxLoadPerServer < triggeringLoad && !limitedZoneAvailability) { return availableZones; } // 从 worstZones 集合中随机选择一个区域(如果存在的话) String zoneToAvoid = randomChooseZone(snapshot, worstZones); if (zoneToAvoid != null) { availableZones.remove(zoneToAvoid); } return availableZones; } } } } // 获取可用区域集合 public static Set<String> getAvailableZones(LoadBalancerStats lbStats, double triggeringLoad, double triggeringBlackoutPercentage) { if (lbStats == null) { return null; } else { // 创建区域 ZoneSnapshot 快照 Map<String, ZoneSnapshot> snapshot = createSnapshot(lbStats); // 获取可用区域 return getAvailableZones(snapshot, triggeringLoad, triggeringBlackoutPercentage); } } // 【重点】 // 由父类调用,获取一个复杂的谓词,该谓词用于进行区域选择 // 上面分析的 getAvailableZones 和 randomChooseZone 均是静态方法,是工具方法 // 供 ZoneAvoidancePredicate 谓词调用 public AbstractServerPredicate getPredicate() { return this.compositePredicate; } }
接着看看 ZoneAvoidancePredicate(区域回避谓词)谓词的源码:
package com.netflix.loadbalancer; import com.netflix.client.config.IClientConfig; import com.netflix.config.DynamicBooleanProperty; import com.netflix.config.DynamicDoubleProperty; import com.netflix.config.DynamicPropertyFactory; import java.util.Map; import java.util.Set; import javax.annotation.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; // 该类用于实现区域规避策略。它继承自AbstractServerPredicate, // 并根据负载均衡统计信息和动态配置参数来判断给定的服务器是否应该被选择。 // 该类主要用于在负载均衡过程中避免选择负载过高或故障较多的区域中的服务器。 public class ZoneAvoidancePredicate extends AbstractServerPredicate { // 表示触发负载阈值。 // 默认值为 0.2,可以通过配置文件进行动态调整。用于判断区域的负载是否过高。 private volatile DynamicDoubleProperty triggeringLoad = new DynamicDoubleProperty("ZoneAwareNIWSDiscoveryLoadBalancer.triggeringLoadPerServerThreshold", 0.2); // 表示触发故障百分比阈值。默认值为 0.99999,可以通过配置文件进行动态调整。 // 用于判断区域的故障情况是否严重。 private volatile DynamicDoubleProperty triggeringBlackoutPercentage = new DynamicDoubleProperty("ZoneAwareNIWSDiscoveryLoadBalancer.avoidZoneWithBlackoutPercetage", 0.99999); private static final Logger logger = LoggerFactory.getLogger(ZoneAvoidancePredicate.class); // 一个动态的布尔属性,表示该谓词是否启用。默认值为true,可以通过配置文件进行动态调整。 private static final DynamicBooleanProperty ENABLED = DynamicPropertyFactory.getInstance().getBooleanProperty("niws.loadbalancer.zoneAvoidanceRule.enabled", true); // 接受一个负载均衡规则IRule和一个客户端配置IClientConfig作为参数 public ZoneAvoidancePredicate(IRule rule, IClientConfig clientConfig) { super(rule, clientConfig); // 初始化动态属性 this.initDynamicProperties(clientConfig); } // 接受一个负载均衡统计信息LoadBalancerStats和一个客户端配置IClientConfig作为参数 public ZoneAvoidancePredicate(LoadBalancerStats lbStats, IClientConfig clientConfig) { super(lbStats, clientConfig); // 初始化动态属性 this.initDynamicProperties(clientConfig); } ZoneAvoidancePredicate(IRule rule) { super(rule); } // 根据客户端配置初始化动态属性 private void initDynamicProperties(IClientConfig clientConfig) { if (clientConfig != null) { this.triggeringLoad = DynamicPropertyFactory.getInstance().getDoubleProperty("ZoneAwareNIWSDiscoveryLoadBalancer." + clientConfig.getClientName() + ".triggeringLoadPerServerThreshold", 0.2); this.triggeringBlackoutPercentage = DynamicPropertyFactory.getInstance().getDoubleProperty("ZoneAwareNIWSDiscoveryLoadBalancer." + clientConfig.getClientName() + ".avoidZoneWithBlackoutPercetage", 0.99999); } } // 该方法是谓词的核心方法,用于判断给定的输入是否满足谓词条件 public boolean apply(@Nullable PredicateKey input) { if (!ENABLED.get()) { // 如果谓词未启用(ENABLED.get()为false),则返回true,表示服务器可以被选择 return true; } else { String serverZone = input.getServer().getZone(); if (serverZone == null) { // 如果输入的服务器所在区域为null,表示服务器可以被选择 return true; } else { LoadBalancerStats lbStats = this.getLBStats(); if (lbStats == null) { // 如果负载均衡统计信息为null,表示服务器可以被选择 return true; } else if (lbStats.getAvailableZones().size() <= 1) { // 如果可用区域数量小于等于 1,表示服务器可以被选择 return true; } else { // 调用 ZoneAvoidanceRule 的工具方法 Map<String, ZoneSnapshot> zoneSnapshot = ZoneAvoidanceRule.createSnapshot(lbStats); if (!zoneSnapshot.keySet().contains(serverZone)) { // 如果区域快照中不包含服务器所在的区域,表示服务器可以被选择 return true; } else { logger.debug("Zone snapshots: {}", zoneSnapshot); // 获取可用区域集合,并判断服务器所在的区域是否在可用区域集合中 Set<String> availableZones = ZoneAvoidanceRule.getAvailableZones(zoneSnapshot, this.triggeringLoad.get(), this.triggeringBlackoutPercentage.get()); logger.debug("Available zones: {}", availableZones); // 如果可用区域集合不为null且包含服务器所在的区域,则返回true;否则,返回false。 return availableZones != null ? availableZones.contains(input.getServer().getZone()) : false; } } } } } }
AvailabilityPredicate(可用性谓词)谓词源码:
package com.netflix.loadbalancer; import com.netflix.client.config.IClientConfig; import com.netflix.config.ChainedDynamicProperty; import com.netflix.config.DynamicBooleanProperty; import com.netflix.config.DynamicIntProperty; import com.netflix.config.DynamicPropertyFactory; import javax.annotation.Nullable; // 该类用于判断服务器的可用性。 // 它主要根据服务器的连接状态和断路器状态来决定是否选择该服务器进行负载均衡。 public class AvailabilityPredicate extends AbstractServerPredicate { // 表示是否根据断路器状态进行过滤。默认值为true,可以通过配置文件进行动态调整。 private static final DynamicBooleanProperty CIRCUIT_BREAKER_FILTERING = DynamicPropertyFactory.getInstance().getBooleanProperty("niws.loadbalancer.availabilityFilteringRule.filterCircuitTripped", true); // 表示活动连接的限制数量。默认值为Integer.MAX_VALUE,可以通过配置文件进行动态调整。 private static final DynamicIntProperty ACTIVE_CONNECTIONS_LIMIT = DynamicPropertyFactory.getInstance().getIntProperty("niws.loadbalancer.availabilityFilteringRule.activeConnectionsLimit", Integer.MAX_VALUE); // 用于获取活动连接限制数量 private ChainedDynamicProperty.IntProperty activeConnectionsLimit; // 接受一个负载均衡规则IRule和一个客户端配置IClientConfig作为参数 // 初始化activeConnectionsLimit属性和动态属性。 public AvailabilityPredicate(IRule rule, IClientConfig clientConfig) { super(rule, clientConfig); this.activeConnectionsLimit = new ChainedDynamicProperty.IntProperty(ACTIVE_CONNECTIONS_LIMIT); this.initDynamicProperty(clientConfig); } // 接受一个负载均衡统计信息LoadBalancerStats和一个客户端配置IClientConfig作为参数 // 初始化activeConnectionsLimit属性和动态属性 public AvailabilityPredicate(LoadBalancerStats lbStats, IClientConfig clientConfig) { super(lbStats, clientConfig); this.activeConnectionsLimit = new ChainedDynamicProperty.IntProperty(ACTIVE_CONNECTIONS_LIMIT); this.initDynamicProperty(clientConfig); } // 接受一个负载均衡规则IRule作为参数,初始化activeConnectionsLimit属性 AvailabilityPredicate(IRule rule) { super(rule); this.activeConnectionsLimit = new ChainedDynamicProperty.IntProperty(ACTIVE_CONNECTIONS_LIMIT); } // 根据客户端配置初始化动态属性 private void initDynamicProperty(IClientConfig clientConfig) { String id = "default"; if (clientConfig != null) { id = clientConfig.getClientName(); this.activeConnectionsLimit = new ChainedDynamicProperty.IntProperty(id + "." + clientConfig.getNameSpace() + ".ActiveConnectionsLimit", ACTIVE_CONNECTIONS_LIMIT); } } // 该方法是谓词的核心方法,用于判断给定的输入是否满足谓词条件 public boolean apply(@Nullable PredicateKey input) { LoadBalancerStats stats = this.getLBStats(); if (stats == null) { // 如果负载均衡统计信息为null,则返回true,表示服务器可以被选择。 return true; } else { // 调用shouldSkipServer方法判断是否应该跳过该服务器 return !this.shouldSkipServer(stats.getSingleServerStat(input.getServer())); } } // 根据断路器状态和活动连接数量判断是否应该跳过该服务器 private boolean shouldSkipServer(ServerStats stats) { // 如果CIRCUIT_BREAKER_FILTERING为true且服务器的断路器跳闸,则返回true // 如果服务器的活动请求数量大于等于活动连接限制数量,则返回true // 否则,返回false return CIRCUIT_BREAKER_FILTERING.get() && stats.isCircuitBreakerTripped() || stats.getActiveRequestsCount() >= (Integer)this.activeConnectionsLimit.get(); } }
上面已经分析了 ZoneAvoidanceRule、ZoneAvoidancePredicate 和 AvailabilityPredicate 类的源码,接着继续查看到底是如何运作的。先查看父类 PredicateBasedRule,代码如下:
public abstract class PredicateBasedRule extends ClientConfigEnabledRoundRobinRule { public PredicateBasedRule() { } public abstract AbstractServerPredicate getPredicate(); // 关注这里的方法 public Server choose(Object key) { ILoadBalancer lb = this.getLoadBalancer(); // this.getPredicate() 方法在子类已经实现了,提供了一个复杂谓词 // 即 CompositePredicate compositePredicate; // 内部使用 ZoneAvoidancePredicate 和 AvailabilityPredicate 谓词 Optional<Server> server = this.getPredicate().chooseRoundRobinAfterFiltering(lb.getAllServers(), key); return server.isPresent() ? (Server)server.get() : null; } }
跟进查看 chooseRoundRobinAfterFiltering() 方法源码:
public Optional<Server> chooseRoundRobinAfterFiltering(List<Server> servers, Object loadBalancerKey) { // 获得符合条件的服务器 List<Server> eligible = this.getEligibleServers(servers, loadBalancerKey); // 轮训的方式获取服务器 return eligible.size() == 0 ? Optional.absent() : Optional.of(eligible.get(this.incrementAndGetModulo(eligible.size()))); } // 该方法以原子方式的形式返回一个位于 eligible 列表内的下标值 private int incrementAndGetModulo(int modulo) { int current; int next; do { current = this.nextIndex.get(); next = (current + 1) % modulo; } while(!this.nextIndex.compareAndSet(current, next) || current >= modulo); return current; }
查看 getEligibleServers() 方法源码:
public List<Server> getEligibleServers(List<Server> servers, Object loadBalancerKey) { if (loadBalancerKey == null) { // 将筛选后的结果使用ImmutableList.copyOf方法转换为不可变列表并返回 return ImmutableList.copyOf(Iterables.filter(servers, this.getServerOnlyPredicate())); } else { List<Server> results = Lists.newArrayList(); Iterator var4 = servers.iterator(); // 遍历输入的服务器列表 while(var4.hasNext()) { Server server = (Server)var4.next(); // 【关注点】 // 对于每个服务器,使用apply方法结合PredicateKey对象来判断该服务器是否符合条件。 // PredicateKey对象包含了负载均衡键和服务器对象 if (this.apply(new PredicateKey(loadBalancerKey, server))) { results.add(server); } } return results; } }
该策略能够在多个区域(zone)之间进行权衡,有效避免将所有请求集中在一个区域的问题,从而实现更均衡的负载分布。这有助于提高整个系统的性能和稳定性,防止单个区域因负载过高而出现响应缓慢或故障。
例如,在一个分布式系统中,可能存在多个数据中心,每个数据中心可以被视为一个区域。ZoneAvoidanceRule 可以根据各个区域的负载情况和健康状态,智能地将请求分发到不同的区域,确保每个区域的资源得到合理利用。
它能够检测区域的健康状态,并在某个区域出现故障时,自动减少向该区域发送请求的概率。这样可以快速地从故障区域中恢复,提高系统的可用性。
比如,如果一个数据中心发生网络故障或服务器宕机,ZoneAvoidanceRule 可以及时感知到,并将请求引导到其他正常运行的区域,避免用户请求受到故障区域的影响。
可以根据实际需求进行配置和调整。用户可以根据系统的特点和业务需求,设置不同的参数来控制区域权衡的策略。
例如,可以调整对不同区域的权重分配,或者设置触发故障规避的阈值等,以满足特定场景下的负载均衡要求。
由于该策略涉及多个区域的权衡和配置,因此配置起来可能相对复杂。需要对系统的架构和区域划分有清晰的了解,并进行适当的参数调整,才能达到最佳的效果。
对于不熟悉 Ribbon 和负载均衡机制的用户来说,可能需要花费一定的时间和精力来理解和配置 ZoneAvoidanceRule。
为了实现区域权衡和故障检测,该策略可能会引入一定的性能开销。它需要不断地监测各个区域的状态,并进行计算和决策,这可能会消耗一定的系统资源。
在高并发的情况下,这种性能开销可能会对系统的响应时间产生一定的影响。因此,在使用该策略时,需要权衡负载均衡的效果和性能开销之间的关系。
ZoneAvoidanceRule 的效果很大程度上依赖于区域划分的准确性。如果区域划分不合理,可能会导致负载均衡效果不佳,甚至出现错误的决策。
例如,如果将不同性能的服务器错误地划分到同一个区域,或者将具有强关联性的服务划分到不同的区域,都可能影响该策略的有效性。因此,在使用该策略时,需要仔细考虑区域划分的原则和方法。
在企业级应用中,常常会将系统部署在多个数据中心以提高可用性和容灾能力。区域权衡策略 ZoneAvoidanceRule 可以在这种多数据中心的环境中,根据各个数据中心的实际情况进行智能的请求分发。
例如,如果一个数据中心出现网络故障或者服务器负载过高,该规则可以自动将请求引导到其他正常的数据中心。
对于大型分布式系统,不同的服务器可能分布在不同的地理位置或网络环境中。区域权衡策略 ZoneAvoidanceRule 可以帮助系统在复杂的环境中实现有效的负载均衡和故障规避。
当某个区域的服务器出现硬件故障、软件故障或者网络问题时,系统可以快速地调整请求分发策略,避免将请求发送到有问题的区域,从而提高系统的整体稳定性和可靠性。