負載均衡之ribbon


一、什么是負載均衡?

做web開發都會接觸到負載均衡,這里我們就不細說了。

(摘自百度百科)負載均衡,英文名稱為Load Balance,其含義就是指將負載(工作任務)進行平衡、分攤到多個操作單元上進行運行,例如FTP服務器、Web服務器、企業核心應用服務器和其它主要任務服務器等,從而協同完成工作任務。

負載均衡主要分為軟件負載和硬件負載,在微服務盛行的現在,軟件負載在微服務里成為主流,netflix的ribbon就是其中之一

 

二、負載均衡要干什么事情?

我們這里只關注軟件負載,硬件負載略過。軟件負載也分為:

服務端負載(服務端發現模式)、客戶端負載(客戶端發現模式)(概念回顧我們前文《服務發現之eureka》

服務端負載

需要做三個事情:

1.接收請求

2.選擇服務器地址

3.轉發/執行請求(轉發還是執行可以參考網關)

這里我們一筆帶過,因為不是本文的重點

 

客戶端負載

本文的重點,也是ribbon的實現方式:

如果讓我們自己做一個軟負載,試想一下怎么去做?

接收請求肯定是對應微服務自己的controller或者rpc服務接收請求

然后負載均衡這里負責:

1.選擇服務器地址

2.發請求

選擇服務器地址這里倒沒什么問題,但是發請求是用httpclient還是resttemplate?由誰定?服務消費者自己定,還是負載均衡器定,服務消費者除了使用負載均衡器請求,有可能也可以直接發請求給服務提供者吧?

作為一個中間件,ribbon其實給到的答案也是由服務消費者自己去定義。

 

負載均衡器

負載均衡其實是一個很抽象的說法,為了讓他更加形象化,我們抽象出一個可以量化的名詞,負載均衡器:

1.每個服務提供者一個負載均衡器,還是所有服務提供者公用一個負載均衡器?

2.每個服務提供者的每個接口能否使用不同的負載均衡器?(dubbo就可以?)

ribbon給到的答案是:每個服務提供者(多節點)一個負載均衡器,負載均衡器之間互相獨立且互不干擾

那么我們得出如下角色職責分工:

細心的觀眾會發現,為什么負載均衡器多了一個職責:記錄請求統計信息?

這是因為負載均衡有的負載規則需要根據請求統計信息來決定選擇哪個服務器,例如WeightedResponseTimeRule,根據平均請求響應時間來選擇合適的服務器

 

我們再來看看ribbon官方是怎么定義的:

Components of load balancer(摘自netflix ribbon wiki

Rule - a logic component to determine which server to return from a list

Ping - a component running in background to ensure liveness of servers

ServerList - this can be static or dynamic. If it is dynamic (as used by DynamicServerListLoadBalancer), a background thread will refresh and filter the list at certain interval

負載均衡器有三大組件:

1.負載規則  ,從服務器列表中決定用哪個服務器

2.ping任務  ,后台運行的任務,用來驗證服務器是否可用

3.服務器列表   ,可以是靜態也可以是動態,如果是動態,那么就要有一個后台線程定時去刷新和過濾列表。我們微服務基於服務發現的情況,服務器列表肯定都是動態增減的,而且ribbon都是配套eureka使用(也可以單獨使用,但我們這里不去研究場景)

發現跟我們的想發還是有出入的,服務器列表我們肯定是有的,不然沒法選擇,主要還是多了一個ping任務

 

把我們上面的想法整合一下,得到如下架構圖:

上圖大家肯定有很多疑惑,不要慌,下面我們再來一一分析:

1.定時獲取ServerList,去哪取?

2.定時執行ping任務,怎么ping

3.ServerList過濾,為什么要過濾,過濾什么?

 

其實結合我們使用ribbon都是結合eureka,單獨使用的場景其實基本沒有,所以我們這里主要關注結合eureka如何使用即可

那么結合eureka服務發現,上面的很多問題都迎刃而解

1.定時獲取ServerList,去哪取?【去eureka client取】

2.定時執行ping任務,怎么ping【eureka client有定時從server刷新服務列表(30s頻率),我們再去ping的話感覺沒太大必要,所以結合eureka的話,我們直接拿來用就行了】

3.ServerList過濾,為什么要過濾,過濾什么?【這個我們后面會揭曉】

 

三、如何將Http Client發送請求與ribbon負載均衡器進行融合?

如果不做融合這個事情,我們的代碼可能就是

1.獲取服務器地址【負載均衡器】

2.發送請求【服務消費者】

3.將請求耗時等信息記錄到負載均衡器【負載均衡器】

主要分為如上三步,這樣我們又會面臨操作jdbc類似的問題,每個開發寫出來的代碼都可能會不一樣,嚴重的可能還會有bug

所以類似spring jdbctemplate,ribbon也想到用類似模板來解決這個事情:

為什么負載均衡器對外提供方法只有get,沒有set?其實這里統計信息是引用類型,get到了之后做修改,地址不變值改變,所以沒有問題,這里我們不糾結

偽代碼如下:

AbstractLoadBalancerAwareClient{
  public void executeWithLoadBalancer(){
    selectServer()
    this.execute()//【交由子類實現】
    recordstats()
  }
}

這樣的話我們就能夠控制動作一致,結果不一致(執行請求的細節交給子類,父類只管選擇服務器、記錄請求結果信息,將地址交給子類自己去使用)

所以netflix ribbon-loadbalancer包也是主要分為兩塊:負載均衡器(loadbalancer包)、客戶端模板(client包)

feign結合ribbon

再進一步舉例,spring-cloud-openfeign是怎么將feign與ribbon結合使用的?(右鍵新標簽打開可查看大圖)

 其實就是我們剛剛說的,繼承模板,只需要實現execute方法決定怎么發送請求即可,其他的交由模板去處理

 

四、ribbon的懶加載策略

准確來說,是spring-cloud-netflix-ribbon的懶加載策略,是spring cloud基於ribbon進行包裝而來的,其最終目的還是為了給每個服務提供者提供各自獨立的個性化配置,懶加載只是其中的實現過程

每個ribbon負載均衡器可以個性化配置的內容有哪些: 

可參考spring-cloud-netflix-ribbon里

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

RibbonClientConfiguration

默認所有的負載均衡器都使用如下默認的實現

 ①DefaultClientConfigImpl里的屬性值如何換成自己的配置?具體有哪些配置?

service-hi.ribbon.ReadTimeout=339
<clientName>.<nameSpace>.<propertyName>=<value>

寫在配置文件里即可,這里就能讀到每個服務自己個性化的配置

如果想所有服務統一使用配置,則把服務名去掉即可

ribbon.MaxAutoRetries=100

如果想更換namespace也可以,將DefaultClientConfigImpl Bean換成自己的實現類即可

public class MyClientConfig extends DefaultClientConfigImpl {
    // ...
    public String getNameSpace() {
        return "foo";
    }
}

具體有哪些配置詳見DefaultClientConfigImpl

②ZonePreferenceServerListFilter里的屬性 zone

this.zone = ConfigurationManager.getDeploymentContext().getValue(ContextKey.zone);

這里的屬性值,由spring-cloud-netflix-eureka-client的 EurekaRibbonClientConfiguration

@PostConstruct set進去,先取

ConfigurationManager.getDeploymentContext().setValue(ContextKey.zone,availabilityZone);

優先順序如下

eureka.instance.metadataMap.zone = zone2 //不支持逗號分割
eureka.client.availability-zones.gz=zone2,zone1 //逗號分割后第一個,注意是只取當前配置region下的az

ribbon的配置有四種(優先級由高到低)

1.每個服務提供者自己個性化的配置 @RibbonClient

@RibbonClient(value="service-hi",configuration= ServiceHiRibbonConfiguration.class)

針對每個服務提供者自行配置,可配置的內容見上面的列表,舉例如下:

//不需要@Configution注解
public class ServiceHiRibbonConfiguration {
    @Bean
    public IRule iRule(){
        return new ZoneAvoidanceRule();
    }
}

2.全局配置 @RibbonsClient

如果有引用spring-cloud-netflix-eureka-client就是走的全局配置
org.springframework.cloud.netflix.ribbon.eureka.RibbonEurekaAutoConfiguration

@RibbonClients(defaultConfiguration = EurekaRibbonClientConfiguration.class)
public class RibbonEurekaAutoConfiguration {
}
@Configuration
public class EurekaRibbonClientConfiguration {
  @Bean
  @ConditionalOnMissingBean
  public IPing ribbonPing(IClientConfig config) {
  ...

自己也可以加全局配置,但是要注意添加@Order注解來控制bean的優先級,否則可能因為優先級低被覆蓋,舉例:
@RibbonClients(defaultConfiguration=MyRibbonDefaultConfiguration.class) ,全局配置,配置文件同上

//不需要@Configuration注解
public class MyRibbonDefaultConfiguration {
    @Bean
    public IRule iRule(){
        return new ZoneAvoidanceRule();
    }
}

另外需要注意@RibbonClients注解除了可以定義全局的配置,也可以給每個服務提供者配置

@RibbonClients(value = {
    @RibbonClient(value="service-hi",configuration = ServiceHiRibbonConfiguration.class),
    @RibbonClient(value="service-order",configuration = ServiceOrderRibbonConfiguration.class)
},defaultConfiguration = RibbonDefaultConfiration.class)

即沒有自己配置的走全局配置,自己配置了的走自己的配置

3.默認配置 RibbonClientConfiguration 

@Autowired(required = false)
private List<RibbonClientSpecification> configurations = new ArrayList<>();
@Bean
public SpringClientFactory springClientFactory() {
   SpringClientFactory factory = new SpringClientFactory();
   factory.setConfigurations(this.configurations);
   return factory;
}
public SpringClientFactory() {
   super(RibbonClientConfiguration.class, NAMESPACE, "ribbon.client.name");
}

@RibbonClient 和@RibbonClients都是注冊BeanClass=RibbonClientSpecification 的spring bean
然后springclientfactory最后統統收到自己的 configurations屬性里
最后獲取配置的時候,通過如上的優先級順序去注冊和取對應的bean即可
SpringClientFactory extends NamedContextFactory

public abstract class NamedContextFactory
implements DisposableBean, ApplicationContextAware{
@Override
public void setApplicationContext(ApplicationContext parent) throws BeansException {
   this.parent = parent;
}
protected AnnotationConfigApplicationContext createContext(String name) {
   AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
   if (this.configurations.containsKey(name)) {
      for (Class<?> configuration : this.configurations.get(name)
            .getConfiguration()) {
         context.register(configuration);
      }
   }
   for (Map.Entry<String, C> entry : this.configurations.entrySet()) {
      if (entry.getKey().startsWith("default.")) {
         for (Class<?> configuration : entry.getValue().getConfiguration()) {
            context.register(configuration);
         }
      }
   }
   context.register(PropertyPlaceholderAutoConfiguration.class,
         this.defaultConfigType);
   context.getEnvironment().getPropertySources().addFirst(new MapPropertySource(
         this.propertySourceName,
         Collections.<String, Object> singletonMap(this.propertyName, name)));
   if (this.parent != null) {
      // Uses Environment from parent as well as beans
      context.setParent(this.parent);
   }
   context.setDisplayName(generateDisplayName(name));
   context.refresh();
   return context;
}

4.spring context里的bean

如上代碼,NamedContextFactory繼承了ApplicationContextAware,取bean時最后會把ApplicationContext作為parent注冊進去,這樣spring 掃描到的所有bean都會被取到
context.setParent(this.parent);

這里需要注意(摘自spring cloud netflix ribbon
@RibbonClient(name = "custom", configuration = CustomConfiguration.class)
The CustomConfiguration clas must be a @Configuration class, but take care that it is not in a @ComponentScan for the main application context. Otherwise, it is shared by all the @RibbonClients. If you use @ComponentScan (or @SpringBootApplication), you need to take steps to avoid it being included (for instance, you can put it in a separate, non-overlapping package or specify the packages to scan explicitly in the @ComponentScan).

如果每個服務提供者自己個性化的配置,要注意configuration文件不要被spring掃描到,否則會被注冊到spring bean,這樣SpringClientFactory getBean時會拿到,這樣就會影響到其他服務提供者(如果優先級是最后倒也還好,但是如果有使用@Order把優先級提高的話,就會造成影響)

其實經過測試,spring cloud Finchley.RELEASE版本,spring-cloud-netflix-ribbon 2.0.0.RELEASE版本 下,CustomConfiguration.class 是不需要@Configuration注解也可以正常使用,這樣就可以避免上面的問題發生(spring-cloud-openfeign的個性化配置一樣可以,且官方文檔也已經明確提到CustomConfiguration可以不需要@Configuration注解)

ribbon也支持餓漢

不過好像沒什么應用場景?,詳見:
RibbonAutoConfiguration

@Bean
@ConditionalOnProperty(value = "ribbon.eager-load.enabled")
public RibbonApplicationContextInitializer ribbonApplicationContextInitializer() {
   return new RibbonApplicationContextInitializer(springClientFactory(),
         ribbonEagerLoadProperties.getClients());
}

通過如下配置即可,應用啟動就會自動注冊相關的bean到SpringClientFactory的contexts map里

ribbon.eager-load.clients=service-hi
ribbon.eager-load.clients=service-order

 

五、結合region AZ要干些什么事情?

region AZ的概念詳見我們前文《服務發現之eureka》,我們的決策肯定是:優先使用相同zone的服務器
所以ServerList過濾時,只保留當前AZ的服務器,其他zone的過濾掉即可(如果當前AZ沒有服務器地址,則保留其他AZ的來使用即可)

所以我們這里才需要ServerList過濾

  

六、將抽象出來的對象映射到類圖

連連看,將如下類圖與我們架構圖里對象進行對應(右鍵新標簽打開可查看大圖)

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM