源碼分析——客戶端負載Netflix Ribbon


原文: 源碼分析——客戶端負載Netflix Ribbon
date: 2019-04-23 11:23:22


[TOC]

前言

Ribbon是由Netflix OSS開源的負載均衡組件

Spring Cloud將其整合作為客戶端側的(client-side)負載均衡組件, 以類庫的形式集成于消費者客戶端內

你在Spring Cloud整合Eureka, Feign, Zuul的組件中都可以見到它的影子

關于負載的作用與分類模式可以參考: 服務發現——需求與模式

在Netflix的場景中, Ribbon與Eureka都是作用在中間層的服務, 在終端接入側的Edge Service負載仍然由亞馬遜ELB服務提供

AWS 彈性負載均衡服務是邊界服務的負載均衡解決方案,邊界服務是向終端用戶訪問 Web 而開放的。
Eureka 填補了中間層負載均衡的空缺。
雖然,理論上可以將中間層服務直接掛在 AWS 彈性負載均衡器后面,但這樣會將它們直接開放給外部世界,從而失去了 AWS 安全組的所有好處。
—— 摘自Netflix


目的

閱讀源碼的目為了解決以下幾個疑問

  • Ribbon獲取到服務列表后是如何負載? 如何路由到目標服務的?
    • Ribbon中的負載均衡策略有哪些? —— 網上很多說明
  • Ribbon是怎么獲取/更新服務列表的?
  • Ribbon的工作流程
  • Spring RestTemplate是如何具備LB能力的, 它與Ribbon有什么聯系


說明

版本:

Netflix Ribbon的源碼依然是Servlet應用, 為了調試方便我還是在Spring Cloud的集成工程中, 源碼僅作為參照方便查找

所以準確講, 并不是單獨分析, 而是Spring Cloud Netflix Ribbon

  • Spring Cloud: Finchley.SR2
  • 對應Netflix Ribbon: v2.2.5
$ git checkout -b v2.2.5 v2.2.5

名詞:

  • NIWS: Netflix Internal Web Service Framework


核心組件

Ribbon核心組件

the beans that Spring Cloud Netflix provides by default for Ribbon:

Bean Type Bean Name Class Name (default)
IClientConfig ribbonClientConfig DefaultClientConfigImpl
IRule ribbonRule ZoneAvoidanceRule
IPing ribbonPing DummyPing
ServerList<Server> ribbonServerList ConfigurationBasedServerList
ServerListFilter<Server> ribbonServerListFilter ZonePreferenceServerListFilter
ILoadBalancer ribbonLoadBalancer ZoneAwareLoadBalancer
ServerListUpdater ribbonServerListUpdater PollingServerListUpdater

以上幾個組件在源碼分析過程會提到


Ribbon核心源碼

調試代碼

為了在方便調試源碼, 我準備一個很簡單的代碼

@Autowired
private LoadBalancerClient loadBalancerClient;

public String getHello() {
    RestTemplate restTemplate = new RestTemplate();
    ServiceInstance serviceInstance = loadBalancerClient.choose("HELLO-SERVICE");
    String url = "http://" + serviceInstance.getHost() + ":" + serviceInstance.getPort();
    return restTemplate.getForObject(url, String.class);
}


路由 & 負載

LoadBalancerClient

RibbonLoadBalancerClient作為Ribbon負載邏輯的重要實現類, 看它之前, 先來看看它的接口聲明

  • 繼承結構
繼承結構圖
  • 接口定義

    public interface LoadBalancerClient extends ServiceInstanceChooser {
    
      <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException;
    
      <T> T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest<T> request) throws IOException;
    
      URI reconstructURI(ServiceInstance instance, URI original);
    }
    
    public interface ServiceInstanceChooser {
      
        ServiceInstance choose(String serviceId);
    }
    

解釋:

LoadBalancerClient

  • execute(): 兩個重載方法根據服務實例(serviceInstance)執行請求

  • reconstructURI(): 重構URI

    例如http://cloudlink-user/priUser/getByIds重構為http://SC-201707142304:8802/priUser/getByIds

ServiceInstanceChooser

  • choose(String serviceId): 即依據LoadBalancer選擇并返回服務ID對應的服務實例

源碼中有比較詳細的注釋

顯而易見RibbonLoadBalancerClient#choose是應該重點分析的方法

它調用了getServer()方法, 經過幾個函數重載, 最終調用了ILoadBalancer中的實現

默認的實現其實是com.netflix.loadbalancer.ZoneAwareLoadBalancer#chooseServer

—— 參考配置類: RibbonClientConfiguration#ribbonLoadBalancer

它是BaseLoadBalancer的子類, 因為我的環境只有一個可用區zone, 所以不會走區域相關的負載邏輯, 直接調用父類super.chooseServer(key), 即BaseLoadBalancer#chooseServer

public Server chooseServer(Object key) {
    if (counter == null) {
        counter = createCounter();
    }
    counter.increment();
    if (rule == null) {
        return null;
    } else {
        try {
            return rule.choose(key); // look me !
        } catch (Exception e) {
            logger.warn("LoadBalancer [{}]:  Error choosing server for key {}", name, key, e);
            return null;
        }
    }
}

IRule

接著上面的, 略過非重點代碼, 看rule.choose(key)

其中的rule就是Ribbon中負載均衡策略的核心接口IRule , 對應前面核心組件中提到的Rule—負載均衡策略

接口定義如下:

public interface IRule{

    public Server choose(Object key);
    
    public void setLoadBalancer(ILoadBalancer lb);
    
    public ILoadBalancer getLoadBalancer();    
}

除了choose(), 接口ILoadBalancer中定義了負載均衡器中的操作方法

public interface ILoadBalancer {

    public void addServers(List<Server> newServers);
    
    public Server chooseServer(Object key);
    
    public void markServerDown(Server server);
    
    @Deprecated
    public List<Server> getServerList(boolean availableOnly);

    public List<Server> getReachableServers();

    public List<Server> getAllServers();
}

IRule的抽象子類AbstractLoadBalancerRule中完成了ILoadBalancer的setter&getter

回到chooseServer()中, 作為BaseLoadBalancer類的成員變量, 默認值定義如下:

private final static IRule DEFAULT_RULE = new RoundRobinRule();
protected IRule rule = DEFAULT_RULE;

也就是默認的策略為輪詢(RoundRobin)

看看其實現: com.netflix.loadbalancer.RoundRobinRule#choose(ILoadBalancer, Object), 代碼省略...

可以看到在該方法中調用了ILoadBalancer#getAllServers獲取到服務列表, 通過一個線程安全的計數算法從中取出一個活著的Server返回

  • 繼承結構

    RoundRobinRule繼承結構

從源碼中可以看到IRule的實現有很多很多, 對應多種負載均衡策略的種類

網上有針對每種策略的解釋, 不說了

至此, 已經分析了Ribbon從多個服務中依據某種負載策略選擇一個進行調用

回答了開頭問題1: Ribbon獲取到服務列表后是如何負載? 如何路由到目標服務的?


獲取 & 更新服務

下面來分析問題2: Ribbon是怎么獲取/更新服務列表的?

在consumer客戶端進行第一次調用provider服務時, 能看到這樣一行日志:

INFO c.n.l.DynamicServerListLoadBalancer      : DynamicServerListLoadBalancer for client cloudlink-user initialized: DynamicServerListLoadBalancer:{
    NFLoadBalancer:name=cloudlink-user,current list of Servers=[SC-201707142304:8801, SC-201707142304:8802],
    ...
}

日志中打印了服務提供者的信息

在源碼中查看, 來自于com.netflix.loadbalancer.DynamicServerListLoadBalancer#restOfInit, 調用棧如下:

org.springframework.cloud.netflix.ribbon.RibbonClientConfiguration#ribbonLoadBalancer
        ↓
com.netflix.loadbalancer.ZoneAwareLoadBalancer#ZoneAwareLoadBalancer
        ↓
com.netflix.loadbalancer.DynamicServerListLoadBalancer#DynamicServerListLoadBalancer
        ↓
com.netflix.loadbalancer.DynamicServerListLoadBalancer#restOfInit

最后在restOfInit()中調用了updateListOfServers(), 從字面意思看, 它與獲取服務實例有關

  • com.netflix.loadbalancer.DynamicServerListLoadBalancer#updateListOfServers

    @VisibleForTesting
    public void updateListOfServers() {
      List<T> servers = new ArrayList<T>();
      if (serverListImpl != null) {
          servers = serverListImpl.getUpdatedListOfServers(); // look me !
          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: {}",
                      getIdentifier(), servers);
          }
      }
      updateAllServerList(servers);
    }
    

從這段代碼能夠推斷出兩點:

  • 獲取到的服務集合信息servers來自于ServerList的實現類
  • ServerList接口中聲明了獲取服務列表的方法以及更新服務列表(30秒)的方法 —— 下面

其實從上面那行debug日志已經能夠看出服務列表信息來自于服務發現客戶端

我們只需要進入ServerList的實現類證明即可

ServerList

這里看到了Ribbon中的另一個核心組件: ServerList 即服務列表, 用于獲取地址列表

在與Spring Cloud Eureka集成的服務中, 它是從注冊中心拉取的并能夠動態更新的服務列表清單

當然, 在捕魚Eureka集成的情況下可以配制成靜態的地址列表

public interface ServerList<T extends Server> {

    public List<T> getInitialListOfServers();
    
    /**
     * Return updated list of servers. This is called say every 30 secs
     * (configurable) by the Loadbalancer's Ping cycle
     * 
     */
    public List<T> getUpdatedListOfServers();   

}

找到其實現: com.netflix.niws.loadbalancer.DiscoveryEnabledNIWSServerList

可以看到以上兩個方法的實現都調用了同一個方法obtainServersViaDiscovery()

代碼很長, 簡化一下大致如下:

List<DiscoveryEnabledServer> serverList = new ArrayList<DiscoveryEnabledServer>();
EurekaClient eurekaClient = eurekaClientProvider.get();
List<InstanceInfo> instanceInfo = eurekaClient.getInstancesByVipAddress(vipAddress, isSecure, targetRegion);
serverList.add(new DiscoveryEnabledServer(instanceInfo););

還能看到在調用getUpdatedListOfServers()得到servers后, 并沒有立即返回

而是又去執行了filter.getFilteredListOfServers(servers);過濾后返回

至此, 已經能夠證明Ribbon獲取服務提供者的信息來自于Eureka Server

ServerListFilter

Ribbon中另一個組件: ServerListFilter, 負責服務列表過濾

僅當使用動態ServerList時使用, 用于在原始的服務列表中使用一定策略過慮掉一部分地址

public interface ServerListFilter<T extends Server> {

    public List<T> getFilteredListOfServers(List<T> servers);

}

默認的過濾器為ZonePreferenceServerListFilter, 會過濾出同區域的服務實例, 也就是區域優先

從ServerListFilter的實現類可以找到, Netflix Ribbon中還提供了其它的過濾規則, 不說了

ServerListUpdater

在DynamicServerListLoadBalancer中可以看到ServerListUpdater的定義

protected final ServerListUpdater.UpdateAction updateAction = 
    new ServerListUpdater.UpdateAction() {
        @Override
        public void doUpdate() {
            updateListOfServers();
        }
};

protected volatile ServerListUpdater serverListUpdater;

它定義了服務列表ServerList的動態更新, 相當于一個服務更新器

  • Implement 1(default): PollingServerListUpdater
  • Implement 2: EurekaNotificationServerListUpdater

默認的動態更新策略為PollingServerListUpdater, 會執行一個定時任務, 源碼中定義了默認執行周期為30s

IPing

接口定義中只有一個方法, 檢測服務是否活著

public interface IPing {
    
    public boolean isAlive(Server server);
}

在Ribbon中的默認實現為DummyPing, 也就是假Ping, 一直返回True

在與Eureka集成使用中, 默認實現為NIWSDiscoveryPing, 會根據服務實例的狀態判斷該實例是否活著:

// ...
InstanceStatus status = instanceInfo.getStatus();
if (status!=null){
    isAlive = status.equals(InstanceStatus.UP);
}
return isAlive;

也是以定時任務周期執行, 源碼可參考: com.netflix.loadbalancer.BaseLoadBalancer.PingTask


RestTemplate & LB

介紹

在Spring Cloud中的服務間通信場景, 除了可以使用聲明式的REST客戶端Feign, 也可以使用從Spring 3.0引入的RestTemplate

RestTemplate中封裝了簡單易用的API, 其實現默認是封裝JDK原生的HTTP客戶端URLConnection

如果你想替換實現為HttpClient, OkHttpClient, 加入對應的依賴和顯示的配置即可, Spring Cloud已對其做好了自動配置

參考: org.springframework.cloud.commons.httpclient.HttpClientConfiguration

例如替換為OkHttp3Client

  • pom依賴

    <dependency>
      <groupId>com.squareup.okhttp3</groupId>
      <artifactId>okhttp</artifactId>
      <version>3.9.1</version>
    </dependency>
    
  • 顯示配置

    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
      RestTemplate restTemplate = new RestTemplate();
        restTemplate.setRequestFactory(new OkHttp3ClientHttpRequestFactory());
      return restTemplate;
    }
    

那么問題來了, 服務間的調用和正常HttpClient遠程調用還是有區別的

RestTemplate是如何具備從多個服務中挑選一個進行路由的能力? 它和Ribbon有什么關系?

我們知道, 有了@LoadBalanced注釋, RestTemplate才能通過邏輯服務名的方式進行調用, 否則會是UnknowHostException

所以猜想RestTemplate在某一步會被賦予客戶端負載的能力


源碼分析

在LoadBalanced同路徑下, 可以看到如下兩個類:

  • LoadBalancerAutoConfiguration
  • LoadBalancerInterceptor

關注如下代碼:

public class LoadBalancerAutoConfiguration {

    @LoadBalanced
    @Autowired(required = false)
    private List<RestTemplate> restTemplates = Collections.emptyList();

    @Bean
    public SmartInitializingSingleton loadBalancedRestTemplateInitializerDeprecated(
            final ObjectProvider<List<RestTemplateCustomizer>> restTemplateCustomizers) {
        return () -> restTemplateCustomizers.ifAvailable(customizers -> {
            for (RestTemplate restTemplate : LoadBalancerAutoConfiguration.this.restTemplates) {
                for (RestTemplateCustomizer customizer : customizers) {
                    customizer.customize(restTemplate); // look me !
                }
            }
        });
    }

    //...

    static class LoadBalancerInterceptorConfig {
        //...

        @Bean
        @ConditionalOnMissingBean
        public RestTemplateCustomizer restTemplateCustomizer(final LoadBalancerInterceptor loadBalancerInterceptor) {
            return restTemplate -> {
                List<ClientHttpRequestInterceptor> list = new ArrayList<>(restTemplate.getInterceptors());
                list.add(loadBalancerInterceptor); // look me !
                restTemplate.setInterceptors(list);
            };
        }
    }
}

其中最關鍵的就是restTemplate.setInterceptors(list)了, 為restTemplate添加了一個攔截器, 在發起HTTP請求時進行攔截

RestTemplate#interceptors

而攔截后要實現的功能肯定就是完成客戶端負載, 進入該攔截器:

public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor {

    private LoadBalancerClient loadBalancer;
    //... 

    @Override
    public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
            final ClientHttpRequestExecution execution) throws IOException {
        final URI originalUri = request.getURI();
        String serviceName = originalUri.getHost();
        Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri);
        return this.loadBalancer.execute(serviceName, requestFactory.createRequest(request, body, execution));
    }
}

可以看到intercept方法內傳入了serviceName, request, 最終調用了LoadBalancerClient#execute()

  • 其中host取到的是URL中填寫的服務名

    e.g. http://cloudlink-user/priUser/getByIds中服務名為cloudlink-user

  • 服務名serviceName取的是host

那接下來就是根據serviceName, 根據負載找到一個服務實例進行路由, 又回到上面的 路由&負載 過程啦

我們使用中都要使用@Bean對其初始化, 確保在啟動時就初始化, 如果new一個, 用一段代碼來簡化手動模擬, 就是這個樣子

@Autowired
private LoadBalancerClient loadBalancerClient;

public List method() {
    RestTemplate restTemplate = new RestTemplate();
    List<ClientHttpRequestInterceptor> interceptors = restTemplate.getInterceptors();
    interceptors.add(new LoadBalancerInterceptor(loadBalancerClient));
    restTemplate.setInterceptors(interceptors);
    return restTemplate.postForObject("http://cloudlink-user/priUser/getByIds", userIds, List.class);
}


總結

源碼沒有很深入, 僅僅為了了解Ribbon工作流程和其中一些細節

整個流程還是比較清晰, 按照順序大致分為:

  • 獲取服務列表信息
  • 執行定時任務動態更新/檢查/剔除服務
  • 對獲取到的服務列表進行過濾
  • 按照負載均衡策略在多個服務中選擇一個服務進行調用

從中也能看出, 在服務實例發生變化時, Consumer端并不能立刻感知到, 而是有一定的延遲, 可能會繼續調用而報錯

與Eureka相同, 再次看出CAP中保證了AP而適當犧牲C, 所以無論是服務網關路由到我們的服務, 還是服務消費者調用提供者.

作為Eureka Client調用側, 盡可能的加入一定重試. 通過配置可以實現這一點

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,156評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,401評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,069評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,873評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,635評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,128評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,203評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,365評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,881評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,733評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,935評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,475評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,172評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,582評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,821評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,595評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,908評論 2 372

推薦閱讀更多精彩內容