原文: 源碼分析——客戶端負載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
核心組件
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請求時進行攔截
而攔截后要實現的功能肯定就是完成客戶端負載, 進入該攔截器:
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調用側, 盡可能的加入一定重試. 通過配置可以實現這一點