1. 程式人生 > >負載均衡之ribbon

負載均衡之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過濾,為什麼要過濾,過濾什麼?【這個我們後面會揭曉】

 

三、如何將傳送請求與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包)

 

再進一步舉例,openfeign結合ribbon是怎麼使用的?(右鍵新標籤開啟可檢視大圖)

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

 

四、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
2.全域性配置 @RibbonsClient
3.預設配置 RibbonClientConfiguration
4.spring context裡的bean

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

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

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

@Configuration
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把優先順序提高的話,就會造成影響)

 

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過濾

  

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

連連看,將如下類圖與我們架構圖裡物件進行對應(右鍵新標籤開啟可檢視大圖)

&n