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 具有負載均衡的能力的呢?
看下篇文章