1. 程式人生 > 程式設計 >Springcloud原始碼閱讀4-Ribbon負載均衡(下)

Springcloud原始碼閱讀4-Ribbon負載均衡(下)

關聯閱讀:

SpringCloud原始碼閱讀0-SpringCloud必備知識

SpringCloud原始碼閱讀1-EurekaServer原始碼的祕密

SpringCloud原始碼閱讀2-Eureka客戶端的祕密

SpringCloud原始碼閱讀3-Ribbon負載均衡(上)

配置檔案

同其他微服務元件與spring整合過程一樣,Ribbon也有一個自動配置檔案。 RibbonAutoConfiguration

@Configuration
@ConditionalOnClass({ IClient.class,RestTemplate.class,AsyncRestTemplate.class,Ribbon.class})
@RibbonClients
@AutoConfigureAfter(name = "org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration") @AutoConfigureBefore({LoadBalancerAutoConfiguration.class,AsyncLoadBalancerAutoConfiguration.class}) @EnableConfigurationProperties({RibbonEagerLoadProperties.class,ServerIntrospectorProperties.class}) public
class RibbonAutoConfiguration { //載入配置規範 @Autowired(required = false) private List<RibbonClientSpecification> configurations = new ArrayList<>(); //載入飢餓屬性。 @Autowired private RibbonEagerLoadProperties ribbonEagerLoadProperties; // Ribbon特徵類 @Bean public HasFeatures ribbonFeature() { return
HasFeatures.namedFeature("Ribbon",Ribbon.class); } // 客戶端生產工廠 @Bean public SpringClientFactory springClientFactory() { SpringClientFactory factory = new SpringClientFactory(); factory.setConfigurations(this.configurations); return factory; } //負載均衡客戶端 @Bean @ConditionalOnMissingBean(LoadBalancerClient.class) public LoadBalancerClient loadBalancerClient() { return new RibbonLoadBalancerClient(springClientFactory()); } ....... ....... } 複製程式碼

下面講講配置檔案中所包含的知識點。

RibbonClientSpecification:

RibbonClient規範,一個規範就對應一種型別的RibbonClient。 規範怎麼制訂呢?

  • @RibbonClients : 針對全部服務指定規範的。
  • @RibbonClient: 針對部分指定規範的。

此兩個註解都會引入一個RibbonClientConfigurationRegistrar類。 從其名字,我們也可以看出,這是一個用來註冊客戶端配置的註冊類。

RibbonClientConfigurationRegistrar會把 @RibbonClients 與 @RibbonClient 註解對應的配置類,註冊為一個RibbonClientSpecification類的Bean.

  • 對應的配置類作為建構函式的引數,傳入。
  • 針對的服務名,作為構造引數傳入。

這樣就得到了RibbonClientSpecification 規範列表。

private void registerClientConfiguration(BeanDefinitionRegistry registry,Object name,Object configuration) {
		BeanDefinitionBuilder builder = BeanDefinitionBuilder
				.genericBeanDefinition(RibbonClientSpecification.class);
		builder.addConstructorArgValue(name);//客戶端名稱
		builder.addConstructorArgValue(configuration);//對應的配置類。
		registry.registerBeanDefinition(name + ".RibbonClientSpecification",builder.getBeanDefinition());
	}
複製程式碼

Ribbon負載均衡(上)一節說過,@RibbonClients 和 @RibbonClient 用來自定義客戶端元件,替換預設的元件。

所以所謂規範的不同,其實就是表現在 個別元件的不同

注意: @RibbonClients 的value屬性,可以用來配置@RibbonClient的複數 @RibbonClients(value = {@RibbonClient(name = "xxx",configuration = XxxRibbonConfig.class),@RibbonClient(name = "demo",configuration = DemoRibbonConfig.class) })

@RibbonClients 的defaultConfiguration屬性,用來替換所有非自定義的客戶端的預設元件

SpringClientFactory:

每個微服務都在呼叫多個微服務。呼叫不同微服務的RibbonClient配置可能不同。SpringClientFactory根據不同的RibbonClient規範(RibbonClientSpecification),為不同的微服務建立子上下文。來儲存不同規範的RibbonClient 元件Bean。 以此達到個性化並存的目的。

從程式碼中,可以看出,SpringClientFactory 會傳入List configurations

@Bean
	public SpringClientFactory springClientFactory() {
		SpringClientFactory factory = new SpringClientFactory();
		factory.setConfigurations(this.configurations);
		return factory;
	}
複製程式碼

舉例說明: user服務需要呼叫, A B C三個微服務,使用@RibbonClient(name = "A",configuration = AConfiguration.class)針對A服務 自定義了IPing 為MyIPing。 那麼會建立三個上下文:

  • A的上下文,使用A.RibbonClientSpecification 規範建立,IPing 對應的Bean是 MyMyIPing
  • B的上下文,使用default.RibbonClientSpecification 規範建立,IPing 對應的Bean是DummyPing
  • C的上下文,使用default.RibbonClientSpecification 規範建立,IPing 對應的Bean是DummyPing
RibbonClientConfiguration

SpringClientFactory 初始化向其父類,傳遞RibbonClientConfiguration配置類做為RibbonClient預設的配置。

public SpringClientFactory() {
		super(RibbonClientConfiguration.class,NAMESPACE,"ribbon.client.name");
	}

public NamedContextFactory(Class<?> defaultConfigType,String propertySourceName,String propertyName) {
		this.defaultConfigType = defaultConfigType;
		this.propertySourceName = propertySourceName;
		this.propertyName = propertyName;
}
複製程式碼

RibbonClientConfiguration 配置類中註冊的就是Ribbon 預設的元件

在這裡插入圖片描述

EurekaRibbonClientConfiguration

在與Eureka一起使用的時候,RibbonEurekaAutoConfiguration 使用@RibbonClients註解引入EurekaRibbonClientConfiguration配置類對RibbonClient預設配置的部分元件進行覆蓋。

@Configuration
@EnableConfigurationProperties
@ConditionalOnRibbonAndEurekaEnabled
@AutoConfigureAfter(RibbonAutoConfiguration.class)
@RibbonClients(defaultConfiguration = EurekaRibbonClientConfiguration.class)
public class RibbonEurekaAutoConfiguration {
}
複製程式碼

EurekaRibbonClientConfiguration 配置類會覆蓋:

  • DiscoveryEnabledNIWSServerList 替換 ribbonServerList , 預設安裝一個DomainExtractingServerList代理DiscoveryEnabledNIWSServerList
  • NIWSDiscoveryPing 替換 (IPing) DummyPing

RibbonLoadBalancerClient

RibbonLoadBalancerClient 就是負載均衡客戶端了。

通過此客戶端,我們可以傳入服務id,從springClientFactory選擇出對應配置的上下文。使用適用於當前服務的負載均衡元件集,來實現負載均衡的目的。

@Bean
	@ConditionalOnMissingBean(LoadBalancerClient.class)
	public LoadBalancerClient loadBalancerClient() {
		return new RibbonLoadBalancerClient(springClientFactory());
	}
複製程式碼

負載均衡原理

路由與負載

LoadBalancerClient

RibbonLoadBalancerClient#choose方法。通過傳入服務名,從多個副本中找出一個服務,以達到負載均衡的目的。

@Override
	public ServiceInstance choose(String serviceId) {
		Server server = getServer(serviceId);
		if (server == null) {
			return null;
		}
		return new RibbonServer(serviceId,server,isSecure(server,serviceId),serverIntrospector(serviceId).getMetadata(server));
	}
protected Server getServer(String serviceId) {
		return getServer(getLoadBalancer(serviceId));
}
protected Server getServer(ILoadBalancer loadBalancer) {
		if (loadBalancer == null) {
			return null;
		}
		return loadBalancer.chooseServer("default"); // TODO: better handling of key
}
複製程式碼

RibbonLoadBalancerClient#choose 方法呼叫loadBalancer.chooseServer

ILoadBalancer: 負載均衡器

從工廠內獲取負載均衡器,上文配置類說過此處的Bean 是ZoneAwareLoadBalancer

protected ILoadBalancer getLoadBalancer(String serviceId) {
		return this.clientFactory.getLoadBalancer(serviceId);
}
複製程式碼

ZoneAwareLoadBalancer#chooseServer方法

ZoneAwareLoadBalancer
public Server chooseServer(Object key) {
。。。
//預設一個區域的情況下直接呼叫父類的chooseServer(key)
 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);
                }
 }
 。。。。
}
BaseLoadBalancer
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;
            }
        }
}
複製程式碼
IRule: 負載均衡策略

rule.choose(key) 根據策略從服務列表中選擇一個出來。

預設的IRule是ZoneAvoidanceRule。

choose 方法在其父類PredicateBasedRule中

 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;
        }       
    }
複製程式碼

可以看出先執行 ILoadBalancer#getAllServers 獲取所有服務,傳入的策略執行方法中,選擇一個服務。

獲取與更新服務

那麼ILoadBalancer.allServerList是如何儲存所有服務的呢?

ServerListUpdater: 服務更新

ZoneAvoidanceRule的直接父類DynamicServerListLoadBalancer:

在初始化屬性時,會初始化UpdateAction 屬性。UpdateAction 是一個ServerListUpdater的一個內部介面,此處初始化了一個匿名實現類。

 protected final ServerListUpdater.UpdateAction updateAction = new ServerListUpdater.UpdateAction() {
        @Override
        public void doUpdate() {
            updateListOfServers();//更新服務列表。
        }
    };
複製程式碼

在初始化構造方法時:預設建立(ServerListUpdater)PollingServerListUpdater()

並且呼叫restOfInit(clientConfig),接著呼叫enableAndInitLearnNewServersFeature(); 方法。

public void enableAndInitLearnNewServersFeature() {
        LOGGER.info("Using serverListUpdater {}",serverListUpdater.getClass().getSimpleName());
        serverListUpdater.start(updateAction);//執行updateAction動作。
}
複製程式碼

最終在PollingServerListUpdater中會有一個定時排程,此定時排程會定時執行UpdateAction 任務,來更新服務列表。預設會在任務建立後1秒後開始執行,並且上次執行完成與下次執行開始之間的間隔,預設30秒。

scheduledFuture = getRefreshExecutor().scheduleWithFixedDelay(
                    wrapperRunnable,initialDelayMs,refreshIntervalMs,TimeUnit.MILLISECONDS
);
複製程式碼
ServerList 服務列表

UpdateAction#doUpdate() 會呼叫updateListOfServers() 執行服務列表的更新。

@VisibleForTesting
    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: {}",servers);
            }
        }
        updateAllServerList(servers);
    }
複製程式碼

此時serverListImpl 實現類是DiscoveryEnabledNIWSServerList DiscoveryEnabledNIWSServerList#getUpdatedListOfServers 執行obtainServersViaDiscovery方法

private List<DiscoveryEnabledServer> obtainServersViaDiscovery() {
 //獲取 DiscoveryClient   
 EurekaClient eurekaClient = (EurekaClient)this.eurekaClientProvider.get();
 //獲取服務列表          
List<InstanceInfo> listOfInstanceInfo = eurekaClient.getInstancesByVipAddress(vipAddress,this.isSecure,this.targetRegion);
                    ....
}
複製程式碼

eurekaClientProvider其實就是對DiscoveryManager的一個代理。DiscoveryManager 在Eureka客戶端的祕密說過,就是對DiscoveryClient的管理者。

eurekaClient.getInstancesByVipAddress 最終呼叫DiscoveryClient.localRegionApps獲取服務列表。

DiscoveryClient.localRegionApps 在Eureka客戶端的祕密已經說過是客戶端服務列表的快取。

從此,我們也可以看出,Ribbon在與Eureka一起使用時,是從DiscoveryClient獲取服務列表的。

ServerListFilter 服務列表過濾

updateListOfServers 方法中獲取到服務列表後,並沒有直接返回,而是通過 ServerListFilter進行了過濾

此時預設的是ZonePreferenceServerListFilter ,會過濾出同區域的服務例項,也就是區域優先

servers = filter.getFilteredListOfServers(servers);
複製程式碼
IPing: 檢查服務狀態

updateListOfServers 方法中執行完過濾後,最後還做了一個操作updateAllServerList。

updateAllServerList(servers);

 protected void updateAllServerList(List<T> ls) {
        // other threads might be doing this - in which case,we pass
        if (serverListUpdateInProgress.compareAndSet(false,true)) {
            try {
                for (T s : ls) {
                    s.setAlive(true); // set so that clients can start using these
                }
                setServersList(ls);
                super.forceQuickPing();
            } finally {
                serverListUpdateInProgress.set(false);
            }
        }
    }
複製程式碼

updateAllServerList 中最終的一步,就是ping操作,用於檢測服務時候存活。此時預設是DummyPing ,

 public boolean isAlive(Server server) {
        return true;//預設永遠是存活狀態。
    }
複製程式碼

Ping任務其實是有一個定時任務存在的:

BaseLoadBalancer 負載均衡器,在初始化時會建立一個定時任務NFLoadBalancer-PingTimer-以10秒的間隔定時去執行Ping任務

public BaseLoadBalancer() {
        this.name = DEFAULT_NAME;
        this.ping = null;
        setRule(DEFAULT_RULE);
        setupPingTask();
        lbStats = new LoadBalancerStats(DEFAULT_NAME);
    }
複製程式碼

至此: Ribbon負載均衡的工作原理輪廓就展現出來了, 因為本文的目的在於闡述Ribbon的工作原理。具體向IRule 的具體策略細節,不在本文範圍內,以後找機會再說。

總結

當Ribbon與Eureka一起使用時,Ribbon會從Eureka客戶端的快取中取服務列表。

我們在使用Ribbon的時候,並沒有直接使用RibbonLoadBalancerClient ,而是常用Resttemplate+@LoadBalanced來傳送請求,那@LoadBalanced是如何讓Resttemplate 具有負載均衡的能力的呢?

看下篇文章