Next, let’s take a look at how to obtain the service instance, what processing has been done after obtaining, and how to select the service instance after processing.
Divide into three parts:
Configuration
In the configuration section of the previous article "The Principles of Spring Cloud Ribbon", you can see that the default load balancer is ZoneAwareLoadBalancer.
Take a look at the configuration class.
Location:
spring-cloud-netflix-core-1.3.5.RELEASE.jarorg.springframework.cloud.netflix.ribbonRibbonClientConfiguration.class
@SuppressWarnings("deprecation")@Configuration@EnableConfigurationProperties//Order is important here, last should be the default, first should be optional// see https://github.com/spring-cloud/spring-cloud-netflix/issues/2086#issuecomment-316281653@Import({OkHttpRibbonConfiguration.class, RestClientRibbonConfiguration.class, HttpClientRibbonConfiguration.class})public class RibbonClientConfiguration {// omit @Bean @ConditionalOnMissingBean public ILoadBalancer ribbonLoadBalancer(IClientConfig config, ServerList<Server> serverList, ServerListFilter<Server> serverListFilter, IRule rule, IPing ping, ServerListUpdater serverListUpdater) { if (this.propertiesFactory.isSet(ILoadBalancer.class, name)) { return this.propertiesFactory.get(ILoadBalancer.class, config, name); } return new ZoneAwareLoadBalancer<>(config, rule, ping, serverList, serverListFilter, serverListUpdater); }// omitted}Injected in the instance of config, rule, ping, serverList, serverListFilter, serverListUpdater.
config: configure the instance.
rule: Load balancing policy instance.
ping: ping instance.
serverList: Gets and updates an instance of the service.
serverListFilter: Service filtering instance.
serverListUpdater: Service list information update instance.
@SuppressWarnings("deprecation")@Configuration@EnableConfigurationProperties//Order is important here, last should be the default, first should be optional// see https://github.com/spring-cloud/spring-cloud-netflix/issues/2086#issuecomment-316281653@Import({OkHttpRibbonConfiguration.class, RestClientRibbonConfiguration.class, HttpClientRibbonConfiguration.class})public class RibbonClientConfiguration { // omit @Bean @ConditionalOnMissingBean public IClientConfig ribbonClientConfig() { DefaultClientConfigImpl config = new DefaultClientConfigImpl(); config.loadProperties(this.name); return config; } @Bean @ConditionalOnMissingBean public IRule ribbonRule(IClientConfig config) { if (this.propertiesFactory.isSet(IRule.class, name)) { return this.propertiesFactory.get(IRule.class, config, name); } ZoneAvoidanceRule rule = new ZoneAvoidanceRule(); rule.initWithNiwsConfig(config); return rule; } @Bean @ConditionalOnMissingBean public IPing ribbonPing(IClientConfig config) { if (this.propertiesFactory.isSet(IPing.class, name)) { return this.propertiesFactory.get(IPing.class, config, name); } return new DummyPing(); } @Bean @ConditionalOnMissingBean @SuppressWarnings("unchecked") public ServerList<Server> ribbonServerList(IClientConfig config) { if (this.propertiesFactory.isSet(ServerList.class, name)) { return this.propertiesFactory.get(ServerList.class, config, name); } ConfigurationBasedServerList serverList = new ConfigurationBasedServerList(); serverList.initWithNiwsConfig(config); return serverList; } @Bean @ConditionalOnMissingBean public ServerListUpdater ribbonServerListUpdater(IClientConfig config) { return new PollingServerListUpdater(config); } @Bean @ConditionalOnMissingBean public ILoadBalancer ribbonLoadBalancer(IClientConfig config, ServerList<Server> serverListFilter<Server> serverListFilter, IRule rule, IPing ping, ServerListUpdater serverListUpdater) { if (this.propertiesFactory.isSet(ILoadBalancer.class, name)) { return this.propertiesFactory.get(ILoadBalancer.class, config, name); } return new ZoneAwareLoadBalancer<>(config, rule, ping, serverList, serverListFilter, serverListUpdater); } @Bean @ConditionalOnMissingBean @SuppressWarnings("unchecked") public ServerListFilter<Server> ribbonServerListFilter(IClientConfig config) { if (this.propertiesFactory.isSet(ServerListFilter.class, name)) { return this.propertiesFactory.get(ServerListFilter.class, config, name); } ZonePreferenceServerListFilter filter = new ZonePreferenceServerListFilter(); filter.initWithNiwsConfig(config); return filter; } @Bean @ConditionalOnMissingBean public RibbonLoadBalancerContext ribbonLoadBalancerContext( ILoadBalancer loadBalancer, IClientConfig config, RetryHandler retryHandler) { return new RibbonLoadBalancerContext(loadBalancer, config, retryHandler); } // omitted}Configure relevant examples here
config: DefaultClientConfigImpl.
rule: ZoneAvoidanceRule.
ping: DummyPing.
serverList: ConfigurationBasedServerList, configuration-based service list instance.
serverListFilter: ZonePreferenceServerListFilter.
serverListUpdater: PollingServerListUpdater.
It should be noted that the instance of serverList here is ConfigurationBasedServerList, which is an instance that gets service information when Eureka is not used, and is obtained from the configuration file.
So when using Eureka, you need to obtain service information from Eureka Server. Which instance should be used to do this?
When enabling Eureka service discovery, the EurekaRibbonClientConfiguration configuration class will be first adopted.
Location:
spring-cloud-netflix-eureka-client-1.3.5.RELEASE.jarorg.springframework.cloud.netflix.ribbon.eurekaEurekaRibbonClientConfiguration.class
@Configuration@CommonsLogpublic class EurekaRibbonClientConfiguration { // omit @Bean @ConditionalOnMissingBean public IPing ribbonPing(IClientConfig config) { if (this.propertiesFactory.isSet(IPing.class, serviceId)) { return this.propertiesFactory.get(IPing.class, config, serviceId); } NIWSDiscoveryPing ping = new NIWSDiscoveryPing(); ping.initWithNiwsConfig(config); return ping; } @Bean @ConditionalOnMissingBean public ServerList<?> ribbonServerList(IClientConfig config, Provider<EurekaClient> eurekaClientProvider) { if (this.propertiesFactory.isSet(ServerList.class, serviceId)) { return this.propertiesFactory.get(ServerList.class, config, serviceId); } DiscoveryEnabledNIWSServerList discoveryServerList = new DiscoveryEnabledNIWSServerList( config, eurekaClientProvider); DomainExtractingServerList serverList = new DomainExtractingServerList( discoveryServerList, config, this.approximateZoneFromHostname); return serverList; } // omitted}After first using the EurekaRibbonClientConfiguration configuration, the instances actually become
config: DefaultClientConfigImpl.
rule: ZoneAvoidanceRule.
ping: NIWSDiscoveryPing.
serverList: DomainExtractingServerList, internally DiscoveryEnabledNIWSServerList, actually obtaining a list of service information through service discovery.
serverListFilter: ZonePreferenceServerListFilter.
serverListUpdater: PollingServerListUpdater.
Get services
Before finding the access to obtain service information, first edit the class inheritance relationship of the load balancer.
The parent class DynamicServerListLoadBalancer construct is called in the construct of ZoneAwareLoadBalancer.
Location:
ribbon-loadbalancer-2.2.2.jar
com.netflix.loadbalancer
ZoneAwareLoadBalancer.class
In the construction of DynamicServerListLoadBalancer, the restOfInit function is called.
ribbon-loadbalancer-2.2.2.jar
com.netflix.loadbalancer
DynamicServerListLoadBalancer.class
void restOfInit(IClientConfig clientConfig) { boolean primeConnection = this.isEnablePrimingConnections(); // turn this off to avoid duplicated asynchronous priming done in BaseLoadBalancer.setServerList() this.setEnablePrimingConnections(false); enableAndInitLearnNewServersFeature(); updateListOfServers(); if (primeConnection && this.getPrimeConnections() != null) { this.getPrimeConnections() .primeConnections(getReachableServers()); } this.setEnablePrimingConnections(primeConnection); LOGGER.info("DynamicServerListLoadBalancer for client {} initialized: {}", clientConfig.getClientName(), this.toString()); }First, the service list is updated regularly by calling the enableAndInitLearnNewServersFeature method, and then immediately call the updateListOfServers function to obtain and update the service list information immediately.
Let’s first look at the enableAndInitLearnNewServersFeature method. In fact, the start method of the service list information update instance is called to start the timed update function.
/** * Feature that lets us add new instances (from AMIs) to the list of * existing servers that the LB will use Call this method if you want this * feature enabled */ public void enableAndInitLearnNewServersFeature() { LOGGER.info("Using serverListUpdater {}", serverListUpdater.getClass().getSimpleName()); serverListUpdater.start(updateAction); } The service list information update example here is the PollingServerListUpdater instance configured in the configuration stage. Take a look at the construction and start method of this class.
public class PollingServerListUpdater implements ServerListUpdater { // private static long LISTOFSERVERS_CACHE_UPDATE_DELAY = 1000; // msecs; private static int LISTOFSERVERS_CACHE_REPEAT_INTERVAL = 30 * 1000; // msecs; // private final AtomicBoolean isActive = new AtomicBoolean(false); private volatile long lastUpdated = System.currentTimeMillis(); private final long initialDelayMs; private final long refreshIntervalMs; // Slightly public PollingServerListUpdater(IClientConfig clientConfig) { this(LISTOFSERVERS_CACHE_UPDATE_DELAY, getRefreshIntervalMs(clientConfig)); } public PollingServerListUpdater(final long initialDelayMs, final long refreshIntervalMs) { this.initialDelayMs = initialDelayMs; this.refreshIntervalMs = refreshIntervalMs; } @Override public synchronized void start(final UpdateAction updateAction) { if (isActive.compareAndSet(false, true)) { final Runnable wrapperRunnable = new Runnable() { @Override public void run() { if (!isActive.get()) { if (scheduledFuture != null) { scheduledFuture.cancel(true); } return; } try { updateAction.doUpdate(); lastUpdated = System.currentTimeMillis(); } catch (Exception e) { logger.warn("Failed one update cycle", e); } } } }; scheduledFuture = getRefreshExecutor().scheduleWithFixedDelay( wrapperRunnable, initialDelayMs, refreshIntervalMs, TimeUnit.MILLISECONDS ); } else { logger.info("Already active, no-op"); } } // omit}From the construction and constant definition, the execution is delayed by one second, and the update is performed every 30 seconds by default. You can modify the update time between the configuration.
From the start method, it is to open a schedule that executes regularly and execute updateAction.doUpdate() regularly.
Go back to the start method caller DynamicServerListLoadBalancer class and take a look at the definition of the UpdateAction instance.
protected final ServerListUpdater.UpdateAction updateAction = new ServerListUpdater.UpdateAction() { @Override public void doUpdate() { updateListOfServers(); } };In fact, the updateListOfServers method of DynamicServerListLoadBalancer class is called, which is consistent with the path to update the service information list immediately after the startup time update.
Continue to look at the updateListOfServers method.
public void updateListOfServers() { List<T> servers = new ArrayList<T>(); if (serverListImpl != null) { servers = serverListImpl.getUpdatedListOfServers(); LOGGER.debug("List of Servers for {} obtained from Discovery client: {}", getIdentifier(), servers); if (filter != null) { servers = filter.getFilteredListOfServers(servers); LOGGER.debug("Filtered List of Servers for {} obtained from Discovery client: {}", getIdentifier(), servers); } } updateAllServerList(servers); }1. Get the service information list through the ServerList instance.
2. Filter the obtained service information list through the ServerListFilter instance.
3. Save the filtered service information list to LoadBalancerStats as state hold.
Let’s take a look at it separately next.
1. Get the service information list through the ServerList instance.
The ServerList instance is the DomainExtractingServerList generated in the configuration stage. The service information is delegated to the DiscoveryEnabledNIWSServerList.
public class DiscoveryEnabledNIWSServerList extends AbstractServerList<DiscoveryEnabledServer>{ // omit @Override public List<DiscoveryEnabledServer> getInitialListOfServers(){ return obtainedServersViaDiscovery(); } @Override public List<DiscoveryEnabledServer> getUpdatedListOfServers(){ return obtainedServersViaDiscovery(); } private List<DiscoveryEnabledServer> obtainedServersViaDiscovery() { List<DiscoveryEnabledServer> serverList = new ArrayList<DiscoveryEnabledServer>(); if (eurekaClientProvider == null || eurekaClientProvider.get() == null) { logger.warn("EurekaClient has not been initialized yet, returning an empty list"); return new ArrayList<DiscoveryEnabledServer>(); } EurekaClient eurekaClient = eurekaClientProvider.get(); if (vipAddresses!=null){ for (String vipAddress: vipAddresses.split(",")) { // if targetRegion is null, it will be interpreted as the same region of client List<InstanceInfo> listOfInstanceInfo = eurekaClient.getInstancesByVipAddress(vipAddress, isSecure, targetRegion); for (InstanceInfo ii : listOfInstanceInfo) { if (ii.getStatus().equals(InstanceStatus.UP)) { if(shouldUseOverridePort){ if(logger.isDebugEnabled()){ logger.debug("Overriding port on client name: " + clientName + " to " + overridePort); } // copy is necessary since the InstanceInfo builder just uses the original reference, // and we don't want to corrupt the global eureka copy of the object which may be // used by other clients in our system InstanceInfo copy = new InstanceInfo(ii); if(isSecure){ ii = new InstanceInfo.Builder(copy).setSecurePort(overridePort).build(); }else{ ii = new InstanceInfo.Builder(copy).setPort(overridePort).build(); } } DiscoveryEnabledServer des = new DiscoveryEnabledServer(ii, isSecure, shouldUseIpAddr); des.setZone(DiscoveryClient.getZone(ii)); serverList.add(des); } } if (serverList.size()>0 && prioritizeVipAddressBasedServers){ break; // if the current vipAddress has servers, we don't use subsequent vipAddress based servers } } } return serverList; } // omitted}It can be seen that it is actually to obtain all service instance information from the Eureka server through the Eureka client and package the online ones into DiscoveryEnabledServer instances, with zone information, and put them in the service list.
2. Filter the obtained service information list through the ServerListFilter instance.
The serverListFilte instance is the ZonePreferenceServerListFilter generated in the configuration stage, and is filtered by calling the getFilteredListOfServers method of the instance.
@Data@EqualsAndHashCode(callSuper = false)public class ZonePreferenceServerListFilter extends ZoneAffinityServerListFilter<Server> { private String zone; @Override public void initWithNiwsConfig(IClientConfig niwsClientConfig) { super.initWithNiwsConfig(niwsClientConfig); if (ConfigurationManager.getDeploymentContext() != null) { this.zone = ConfigurationManager.getDeploymentContext().getValue( ContextKey.zone); } } @Override public List<Server> getFilteredListOfServers(List<Server> servers) { List<Server> output = super.getFilteredListOfServers(servers); if (this.zone != null && output.size() == servers.size()) { List<Server> local = new ArrayList<Server>(); for (Server server: output) { if (this.zone.equalsIgnoreCase(server.getZone())) { local.add(server); } } if (!local.isEmpty()) { return local; } } return output; }}In the getFilteredListOfServers method, the first thing you want is to call the parent class's same name to filter first. In fact, the parent class also filters out services in the same area as the consumer side. Not only that, it adds some intelligent judgments to ensure that filtering in the same area is not performed when the failure/load is high or when there are few available instances.
However, in ZonePreferenceServerListFilter.getFilteredListOfServers, even if the parent class has not filtered, the services of the same zone still need to be filtered out and used. Who said that the class here is ZonePreference?
This is a rather weird thing, and it feels that the intelligent judgment of the parent class has no effect.
Let’s take a look at the hard work done by ZoneAffinityServerListFilter.getFilteredListOfServers.
public class ZoneAffinityServerListFilter<T extends Server> extends AbstractServerListFilter<T> implements IClientConfigAware { // private boolean shouldEnableZoneAffinity(List<T> filtered) { if (!zoneAffinity && !zoneExclusive) { return false; } if (zoneExclusive) { return true; } LoadBalancerStats stats = getLoadBalancerStats(); if (stats == null) { return zoneAffinity; } else { logger.debug("Determining if zone affinity should be enabled with given server list: {}", filtered); ZoneSnapshot snapshot = stats.getZoneSnapshot(filtered); double loadPerServer = snapshot.getLoadPerServer(); int instanceCount = snapshot.getInstanceCount(); int circuitBreakerTrippedCount = snapshot.getCircuitTrippedCount(); if ((((double) circuitBreakerTrippedCount) / instanceCount >= blackOutServerPercentageThreshold.get() || loadPerServer >= activeReqeustsPerServerThreshold.get() || (instanceCount - circuitBreakerTrippedCount) < availableServersThreshold.get()) { logger.debug("zoneAffinity is overriden. blackOutServerPercentage: {}, activeReqeustsPerServer: {}, availableServers: {}", new Object[] {(double) circuitBreakerTrippedCount / instanceCount, loadPerServer, instanceCount - circuitBreakerTrippedCount}); return false; } else { return true; } } } @Override public List<T> getFilteredListOfServers(List<T> servers) { if (zone != null && (zoneAffinity || zoneExclusive) && servers !=null && servers.size() > 0){ List<T> filteredServers = Lists.newArrayList(Iterables.filter( servers, this.zoneAffinityPredicate.getServerOnlyPredicate())); if (shouldEnableZoneAffinity(filteredServers)) { return filteredServers; } else if (zoneAffinity) { overrideCounter.increment(); } } return servers; } // omitted}First, the services of the same zone as the consumer side will be filtered out, and then the services of the same zone will be determined by shouldEnableZoneAffinity (filteredServers) to determine whether the services of the same zone can be adopted or all services can be adopted.
In the shouldEnableZoneAffinity method, a snapshot is done for services of the same zone, and the number of instances, average load, and number of instances of these services are obtained for calculation and judgment.
You can take a look at the values of the key indicators in the initWithNiwsConfig method.
Decision conditions:
Percentage of circuit breakers >=0.8 (number of circuit breakers/number of instances of service)
Average load>=0.6
Number of available instances < 2 (number of instances - number of instances that are broken)
If the judgment conditions are met, then all services will be used to ensure availability.
However, as mentioned above, because ZonePreferenceServerListFilter itself always chooses services that are consistent with consumer zones, the intelligent operations done in ZoneAffinityServerListFilter.getFilteredListOfServers are of no use.
However, of course, you can use ZoneAffinityServerListFilter instances through custom configurations.
3. Save the filtered service information list to LoadBalancerStats as state hold.
Follow up updateAllServerList(servers); and go deeper step by step, you will find that it is actually saved in LoadBalancerStats , and the services at this time are saved in HashMap<String, List<Server>> structure according to the zone grouping, and the key is zone.
Select a Service
The load balancer that implements the ILoadBalancer interface is used to select services by implementing the choiceServer method, and the selected service is used as the target request service.
Take a look at the ZoneAwareLoadBalancer.chooseServer method.
@Override public Server chooseServer(Object key) { if (!ENABLED.get() || getLoadBalancerStats().getAvailableZones().size() <= 1) { logger.debug("Zone aware logic disabled or there is only one zone"); return super.chooseServer(key); } Server server = null; try { LoadBalancerStats lbStats = getLoadBalancerStats(); Map<String, ZoneSnapshot> zoneSnapshot = ZoneAvoidanceRule.createSnapshot(lbStats); logger.debug("Zone snapshots: {}", zoneSnapshot); if (triggeringLoad == null) { triggeringLoad = DynamicPropertyFactory.getInstance().getDoubleProperty( "ZoneAwareNIWSDiscoveryLoadBalancer." + this.getName() + ".triggeringLoadPerServerThreshold", 0.2d); } if (triggeringBlackoutPercentage == null) { triggeringBlackoutPercentage = DynamicPropertyFactory.getInstance().getDoubleProperty( "ZoneAwareNIWSDiscoveryLoadBalancer." + this.getName() + ".avoidZoneWithBlackoutPercetage", 0.99999d); } Set<String> availableZones = ZoneAvoidanceRule.getAvailableZones(zoneSnapshot, triggeringLoad.get(), triggeringBlackoutPercentage.get()); logger.debug("Available zones: {}", availableZones); if (availableZones != null && availableZones.size() < zoneSnapshot.keySet().size()) { String zone = ZoneAvoidanceRule.randomChooseZone(zoneSnapshot, availableZones); logger.debug("Zone chosen: {}", zone); if (zone != null) { BaseLoadBalancer zoneLoadBalancer = getLoadBalancer(zone); server = zoneLoadBalancer.chooseServer(key); } } } catch (Exception e) { logger.error("Error choosing server using zone aware logic for load balancer={}", name, e); } if (server != null) { return server; } else { logger.debug("Zone avoidance logic is not invoked."); return super.chooseServer(key); } }Note that there are two usages here:
1. Turn off zone-aware load balancing by configuring ZoneAwareNIWSDiscoveryLoadBalancer.enabled=false, or the number of zones <= 1.
2. Use zone perception, or the number of zones is >1.
Let's take a look one by one
1. Turn off zone-aware load balancing by configuring ZoneAwareNIWSDiscoveryLoadBalancer.enabled=false, or the number of zones <= 1.
In this case, the parent class BaseLoadBalancer.chooseServer method is called.
public Server chooseServer(Object key) { if (counter == null) { counter = createCounter(); } counter.increment(); if (rule == null) { return null; } else { try { return rule.choose(key); } catch (Exception e) { logger.warn("LoadBalancer [{}]: Error choosing server for key {}", name, key, e); return null; } } }The load balancing policy rule used here is actually the ZoneAvoidanceRule policy instance generated during the configuration phase that was transmitted in when constructing the ZoneAwareLoadBalancer.
public void setRule(IRule rule) { if (rule != null) { this.rule = rule; } else { /* default rule */ this.rule = new RoundRobinRule(); } if (this.rule.getLoadBalancer() != this) { this.rule.setLoadBalancer(this); } }Assume that if there is no configuration, the default RoundRobinRule policy instance is used.
2. Use zone perception, or the number of zones is >1.
public Server chooseServer(Object key) { if (!ENABLED.get() || getLoadBalancerStats().getAvailableZones().size() <= 1) { logger.debug("Zone aware logic disabled or there is only one zone"); return super.chooseServer(key); } Server server = null; try { LoadBalancerStats lbStats = getLoadBalancerStats(); Map<String, ZoneSnapshot> zoneSnapshot = ZoneAvoidanceRule.createSnapshot(lbStats); logger.debug("Zone snapshots: {}", zoneSnapshot); if (triggeringLoad == null) { triggeringLoad = DynamicPropertyFactory.getInstance().getDoubleProperty( "ZoneAwareNIWSDiscoveryLoadBalancer." + this.getName() + ".triggeringLoadPerServerThreshold", 0.2d); } if (triggeringBlackoutPercentage == null) { triggeringBlackoutPercentage = DynamicPropertyFactory.getInstance().getDoubleProperty( "ZoneAwareNIWSDiscoveryLoadBalancer." + this.getName() + ".avoidZoneWithBlackoutPercetage", 0.99999d); } Set<String> availableZones = ZoneAvoidanceRule.getAvailableZones(zoneSnapshot, triggeringLoad.get(), triggeringBlackoutPercentage.get()); logger.debug("Available zones: {}", availableZones); if (availableZones != null && availableZones.size() < zoneSnapshot.keySet().size()) { String zone = ZoneAvoidanceRule.randomChooseZone(zoneSnapshot, availableZones); logger.debug("Zone chosen: {}", zone); if (zone != null) { BaseLoadBalancer zoneLoadBalancer = getLoadBalancer(zone); server = zoneLoadBalancer.chooseServer(key); } } } catch (Exception e) { logger.error("Error choosing server using zone aware logic for load balancer={}", name, e); } if (server != null) { return server; } else { logger.debug("Zone avoidance logic is not invoked."); return super.chooseServer(key); } }In this case, the ZoneAvoidanceRule load balancing policy is used by default.
Get the snapshot information of the zone.
Get the available zone, by observing ZoneAvoidanceRule.getAvailableZones definition, the condition that not the available zone is:
After all available zones are obtained, choose one randomly.
And from this zone, select the service through the parent class BaseLoadBalancer of ZoneAwareLoadBalancer.chooseServer. As compiled above, if no rule is passed in BaseLoadBalancer, then the RoundRobinRule policy is used by default to find a service.
In fact, it is still the problem of obtaining the ZonePreferenceServerListFilter filter above. In fact, only one service with the same zone as the consumer side is filtered out. Therefore, the function of selecting services from available zones in Part 2 cannot be achieved. To reach it, the filter must be replaced.
Summarize:
The configured load balancer will start the schedule to obtain service information. When using the Eureka client, all service instance information will be obtained from the Eureka service, and the services that can be used will be filtered out through the filter. By default, the filter will only filter out services that are the same as the consumer side. If you want to ensure high availability, you can configure the ZoneAffinityServerListFilter filter, and the filtered service list will select the corresponding service by implementing the load balancing policy of the IRule interface. If you use the zone-aware policy, you can select the appropriate service from the zone with good load conditions.