“不积跬步,无以至千里。”
之前我们看到了ribbon负载均衡组件里的服务列表实际上是从eureka client自己本地的缓存中获取的
eureka client自己本身,是不断的去从eureka server每隔30秒更新一次注册表,拉取增量注册表
所以ribbon和eureka整合的机制里,肯定得有一个组件,负责每隔一定的时间,从本地的eureka client里刷新一下服务的注册表到LoadBalancer中
继续回到 DynamicServerListLoadBalancer
组件的 restOfInit()
方法
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());
}
enableAndInitLearnNewServersFeature();
这一行代码比较关键,实际上就包含了定时去拉取eureka client本地注册表的逻辑,不妨进去看看
/**
* 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);
}
先看这个注释,大概就知道点意思了,允许我们将新实例添加到已存在的servers… …
这里调用了 serverListUpdaterd
的start方法
这个 serverListUpdaterd 实际上就是一个 PollingServerListUpdater
的实例Bean,同样在 RibbonClientConfiguration
这个配置类中自动配置的
@Bean
@ConditionalOnMissingBean
public ServerListUpdater ribbonServerListUpdater(IClientConfig config) {
return new PollingServerListUpdater(config);
}
start方法传入了一个 updateAction
protected final ServerListUpdater.UpdateAction updateAction = new ServerListUpdater.UpdateAction() {
@Override
public void doUpdate() {
updateListOfServers();
}
};
这个action里面有一个 doUpdate()
方法,调用了 updateListOfServers()
这个方法正是上一篇文章分析的,ribbon从eureka client那里获取server list的方法!!!
到这里,我们可以大胆猜想,这个所谓的 enableAndInitLearnNewServersFeature ,
很可能就是通过 PollingServerListUpdater 这个组件定时去eureka client拉取本地的服务注册表到ribbon的负载均衡组件,那么一旦有服务实例上线或者故障,eureka client就会通过定时拉取增量注册表的机制更新本地缓存,进而ribbon的负载均衡组件里就是最新的服务实例了
现在只需要看看 PollingServerListUpdater 的start方法就可以验证我们的猜想
@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");
}
}
updateAction.doUpdate();
这里会把上面的方法封装到一个 wrapperRunnable
的线程里,然后通过一个 scheduledFuture
调度器定时执行,果然我们又猜对了
initialDelayMs
,第一次执行的延迟,默认1s
refreshIntervalMs
,后续的执行间隔时间,默认30s
private static long LISTOFSERVERS_CACHE_UPDATE_DELAY = 1000; // msecs;
private static int LISTOFSERVERS_CACHE_REPEAT_INTERVAL = 30 * 1000; // msecs;
ok,到这里,已经很明确了,定时更新ribbon负载均衡组件里的server list是通过 enableAndInitLearnNewServersFeature 方法完成的,是通过 PollingServerListUpdater 组件内部的ScheduledFuture
定时调用 updateListOfServers()
实现的
接着,用剩下的篇幅说说负载均衡吧,这个也是挺重要的
Server server = getServer(loadBalancer);
protected Server getServer(ILoadBalancer loadBalancer) {
if (loadBalancer == null) {
return null;
}
return loadBalancer.chooseServer("default"); // TODO: better handling of key
}
loadBalancer.chooseServer(“default”);
可以看到,调用得是 ZoneAwareLoadBalancer
的 chooseServer
方法
@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;
... ...
}
这个方法,真正会调用就上面的super.chooseServer(key);
下面的涉及到多机房的概念,我就给省略了,因为我们这里暂时没有Zone(机房)的概念
如果是多机房的话,在这里是可以感知到多机房的,将一个机房里的请求,转发给自己这个机房里部署的其他的服务实例
除非是一些大公司,或者是基于阿里云的环境,否则一般都是单机房部署的,上了一些规模了,有些中小型的公司系统比较成熟了,也会做双机房的环境,所以在这里我们不需要关注
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;
}
}
}
return rule.choose(key);
这个rule是一个ZoneAvoidanceRule
类型的Bean,是在RibbonClientConfiguraiton
中自动配置的
@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;
}
调用了他的choose()方法来选择一个server,其实是用的父类,PredicateBasedRule.choose() 方法
@Override
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;
}
}
List eligible = getEligibleServers(servers, loadBalancerKey);
先执行过滤规则,过滤掉一批server,根据你自己指定的filter规则,然后用round robin
轮询算法,依次获取下一个server
public Optional<Server> chooseRoundRobinAfterFiltering(List<Server> servers, Object loadBalancerKey) {
List<Server> eligible = getEligibleServers(servers, loadBalancerKey);
if (eligible.size() == 0) {
return Optional.absent();
}
return Optional.of(eligible.get(incrementAndGetModulo(eligible.size())));
}
incrementAndGetModulo(eligible.size())
这个代码,是轮询的方法从server list中挑选一个index返回,即挑选一个server
private int incrementAndGetModulo(int modulo) {
for (;;) {
int current = nextIndex.get();
int next = (current + 1) % modulo;
if (nextIndex.compareAndSet(current, next) && current < modulo)
return current;
}
}
int current = nextIndex.get();
获取当前的index,比如是0
int next = (current + 1) % modulo;
把当前的index+1,和server总数取模,假如总数是2,0+1%2,1
nextIndex.compareAndSet(current, next)
把next设置到nextIndex中,那么下次来就是1,也就是说,本次总第一台机器,下次就走第二台,依次循环
核心算法,round robin轮询算法。
到这里位置,ribbon的整合eureka和choose server已经写完了,可以发现,整体上来看,还是比较简单的,从架构设计到具体的代码实现,核心流程也是比较的清晰
最后,附上一张ribbon处理流程图(手绘)。