Ribbon 客戶端負載均衡
負載均衡
負載均衡 建立在現有網絡結構之上,它提供了一種廉價有效透明的方法擴展網絡設備和服務器的帶寬、增加吞吐量、加強網絡數據處理能力、提高網絡的靈活性和可用性。
分類
分類 | 實現方式 | 優點 | 缺點 |
---|---|---|---|
軟件 | 在一臺或多臺服務器相應的操作系統上安裝一個或多個附加軟件 | 基於特定環境,配置簡單,使用靈活,成本低廉 | 軟件本身耗費資源、可擴展性不好、受操作系統限制、安全問題 |
硬件 | 直接在服務器和外部網絡間安裝負載均衡設備 | 獨立於操作系統、多樣化的負載均衡策略、智能化的流量管理、性能高 | 成本昂貴 |
本地 | 對本地的服務器羣做負載均衡 | 能有效地解決數據流量過大、網絡負荷過重的問題、不需要購置昂貴的服務器 | |
全局 | 對分別放置在不同的地理位置、有不同網絡結構的服務器羣間作負載均衡 | 使全球用戶只以一個IP地址或域名就能訪問到離自己最近的服務器 |
客戶端與服務端級別的負載均衡
- 服務器端負載均衡:例如Nginx,通過Nginx進行負載均衡過程如下:先發送請求給nginx服務器,然後通過負載均衡算法,在多個業務服務器之間選擇一個進行訪問;即在服務器端再進行負載均衡算法分配。
- 客戶端負載均衡:客戶端會有一個服務器地址列表,在發送請求前通過負載均衡算法選擇一個服務器,然後進行訪問,即在客戶端就進行負載均衡算法分配。
Ribbon
Netflix Ribbon 以及 被SpringCloud 封裝過的Ribbon,本質上都是客戶端負載均衡的方式。
原理概述
但凡客戶端負載均衡,大抵可以分爲兩個過程:
- 獲取服務註冊列表的信息
- 根據均衡策略進行負載均衡
在SpringCloud中,服務註冊列表的信息來自 EurekaClient。
源碼分析
在Ribbon的整個調度過程中,LoadBalancerClient,是負責整個過程的執行者。
獲取服務註冊列表
拋開SpringCloud中一大堆的框架代碼,直接找到 DynamicServerListLoadBalancer 類中的 updateListOfServers() 方法:
@VisibleForTesting
public void updateListOfServers() {
List<T> servers = new ArrayList();
if (this.serverListImpl != null) {
servers = this.serverListImpl.getUpdatedListOfServers();
LOGGER.debug("List of Servers for {} obtained from Discovery client: {}", this.getIdentifier(), servers);
if (this.filter != null) {
servers = this.filter.getFilteredListOfServers((List)servers);
LOGGER.debug("Filtered List of Servers for {} obtained from Discovery client: {}", this.getIdentifier(), servers);
}
}
this.updateAllServerList((List)servers);
}
繼續根據 serverListImpl.getUpdatedListOfServers() 順藤摸瓜,找到類 DiscoveryEnabledNIWSServerList ,其中有 getInitialListOfServers() 和 getUpdatedListOfServers() 方法:
@Override
public List<DiscoveryEnabledServer> getInitialListOfServers(){
return obtainServersViaDiscovery();
}
@Override
public List<DiscoveryEnabledServer> getUpdatedListOfServers(){
return obtainServersViaDiscovery();
}
這裏定義了兩個方法,一個是獲取初始服務註冊列表的方法,一個是獲取更新後的服務註冊列表的方法,其實是同一個方法……具體實現:
private List<DiscoveryEnabledServer> obtainServersViaDiscovery() {
List<DiscoveryEnabledServer> serverList = new ArrayList();
if (this.eurekaClientProvider != null && this.eurekaClientProvider.get() != null) {
EurekaClient eurekaClient = (EurekaClient)this.eurekaClientProvider.get();
if (this.vipAddresses != null) {
String[] var3 = this.vipAddresses.split(",");
int var4 = var3.length;
for(int var5 = 0; var5 < var4; ++var5) {
String vipAddress = var3[var5];
List<InstanceInfo> listOfInstanceInfo = eurekaClient.getInstancesByVipAddress(vipAddress, this.isSecure, this.targetRegion);
//省略部分代碼
}
}
}
}
上述方法分兩個部分:
- 通過 eurekaClientProvider.get() 獲取 EurekaClient
- 通過 EurekaClient 來獲取服務註冊列表信息
其中,eurekaClientProvider 的實現類是 LegacyEurekaClientProvider,它是一個獲取 eurekaClient 類,通過靜態的方法去獲取 eurekaClient ,其代碼如下:
class LegacyEurekaClientProvider implements Provider<EurekaClient> {
private volatile EurekaClient eurekaClient;
@Override
public synchronized EurekaClient get() {
if (eurekaClient == null) {
eurekaClient = DiscoveryManager.getInstance().getDiscoveryClient();
}
return eurekaClient;
}
}
獲取服務註冊列表的時間間隔
在BaseLoadBalancer類下,BaseLoadBalancer的構造函數,該構造函數開啓了一個PingTask任務:
public BaseLoadBalancer(String name, IRule rule, LoadBalancerStats stats,
IPing ping, IPingStrategy pingStrategy) {
//省略部分代碼
setupPingTask();
//省略部分代碼
}
void setupPingTask() {
//省略部分代碼
lbTimer.schedule(new PingTask(), 0, pingIntervalSeconds * 1000);
//省略部分代碼
}
判斷服務可用性
在BaseLoadBalancer的構造函數中有一個參數IPing ping,它定義了服務的可用性:
public interface IPing {
boolean isAlive(Server var1);
}
這裏僅僅設置定義了一個布爾值,真正使用的地方,在Pinger的runPinger()方法中:
public void runPinger() throws Exception {
//省略部分代碼
boolean[] resultsx = this.pingerStrategy.pingServers(BaseLoadBalancer.this.ping, allServers);
//省略部分代碼
}
簡單來說,pingerStrategy.pingServers()方法會返回一個布爾值,代表服務註冊列表是否有變化
public interface IPingStrategy {
boolean[] pingServers(IPing var1, Server[] var2);
}
實現其實很簡單,獲取新的服務註冊列表的 Ping 列表,與已經獲取的進行比較:
public boolean[] pingServers(IPing ping, Server[] servers) {
int numCandidates = servers.length;
boolean[] results = new boolean[numCandidates];
//省略部分代碼
for(int i = 0; i < numCandidates; ++i) {
results[i] = false;
try {
if (ping != null) {
results[i] = ping.isAlive(servers[i]);
}
} //省略部分代碼
return results;
}
由此可見,LoadBalancerClient 會向Eureka 獲取服務註冊列表,並且通過10s一次向EurekaClient發送“ping”,來判斷服務的可用性,如果服務的可用性發生了改變或者服務數量和之前的不一致,則更新或者重新拉取。LoadBalancerClient有了這些服務註冊列表,就可以進行負載均衡。
負載均衡
簡而言之,真正的負載均衡是委託給 IRule 實現的:
public Server chooseServer(Object key) {
//省略部分代碼
this.counter.increment();
//省略部分代碼
return this.rule.choose(key);
//省略部分代碼
}
IRule 通過 AbstractLoadBalancerRule 加載過濾規則:
public abstract class AbstractLoadBalancerRule implements IRule, IClientConfigAware {
private ILoadBalancer lb;
public AbstractLoadBalancerRule() {
}
public void setLoadBalancer(ILoadBalancer lb) {
this.lb = lb;
}
public ILoadBalancer getLoadBalancer() {
return this.lb;
}
}
RandomRule
本質上通過隨機數隨機獲取一個服務實例,在index上隨機,選擇index對應位置的server,但如果根據隨機數獲取不到實例,則會出現BUG
RoundRobinRule
按照線性輪詢的方式依次選擇每個服務的實例,如果超過10次獲取不到服務,則嘗試結束,並拋出警告。
RetryRule
在 RoundRobinRule 基礎上增加了重試機制,在一個配置時間段內當選擇server不成功,則一直嘗試使用subRule的方式選擇一個可用的server
WeightedResponseTimeRule
該方式是 RoundRobinRule 的擴展,增加了根據實例的運行情況來計算權值,並根據權值來選擇實例,其實現主要分3個步驟:
-
定時任務
- WeightedResponseTimeRule 策略在初始化時會啓動一個定時任務,每隔30s會執行權值更新的操作
-
權值計算
-
權值的值是總響應時間與實例自身平均響應時間的差所累加而得
-
舉例:假設有四個實例A、B、C、D,他們的平均響應時間爲10、40、80、100,所以總響應時間是230,每個實例的權值如下:
- A:230-10=220
- B:220+(230-40)=410
- C:410+(230-80)=560
- D:560+(230-100)=690
這些權值代表的一個區間,總區間是 [0 ,690)
- A:[0,220]
- B:(220,410]
- C:(410,560]
- B:(560,690)
-
-
實例選擇
- 實例的選擇是隨機一個數字,然後將這個數字所在的權值區間對應的服務實例作爲被選擇的實例
- 根據上述權值計算髮現,平均響應時間越短,權重區間寬度越大,因此被選中的概率越大
BestAvailableRule
逐個考察實例,如果實例被過濾掉了,則忽略,再選擇其中併發請求數最小的實例。
AvailabilityFilteringRule
該策略有兩個過程,先以線性方式選擇一個實例,再判斷是否滿足,如果滿足就選擇,不滿足就繼續下一個
- 過濾:
- 過濾掉那些因爲一直連接失敗而被標記爲circuit tripped的服務實例
- 過濾掉那些高併發的的服務實例(active connections 超過配置的閾值),閥值默認爲2^32-1,可通過參數
<clientName>.<nameSpace>.ActiveConnectionsLimit
來修改
ZoneAvoidanceRule
和 AvailabilityFilteringRule 不一樣的是,ZoneAvoidanceRule是先進行過濾,再輪詢選擇,過濾的條件和 AvailabilityFilteringRule 一樣,不過是先通過過濾把所有的服務找出來,然後再去輪詢選擇。
- 使用主過濾條件過濾所有實例並返回過濾的清單
- 依次使用次過濾條件對返回的過濾清單進行過濾
- 每次過濾完成都需要判斷下面兩個條件,只要有一個符合就不再過濾,並將當前結果返回供線性輪詢選擇:
- 過濾後是實例總數 >= 最小過濾實例數(默認爲1)
- 過濾後的實例比例 > 最小過濾百分百(默認爲0)
SpringCloud 對 Ribbon 的封裝
與RestTemplate的結合
使用,增加一個註解 @LoadBalance
@LoadBalanced
RestTemplate restTemplate() {
return new RestTemplate();
}
源碼分析,SpringBoot的一個核心功能就是自動配置,其中以 XXXAutoConfiguration 類爲最關鍵:
找到 LoadBalanceAutoConfiguration 類定義如下:
@Configuration
@ConditionalOnClass(RestTemplate.class)
@ConditionalOnBean(LoadBalancerClient.class)
@EnableConfigurationProperties(LoadBalancerRetryProperties.class)
public class LoadBalancerAutoConfiguration {
@LoadBalanced
@Autowired(required = false)
private List<RestTemplate> restTemplates = Collections.emptyList();
@Bean
public SmartInitializingSingleton loadBalancedRestTemplateInitializer(
final List<RestTemplateCustomizer> customizers) {
return new SmartInitializingSingleton() {
@Override
public void afterSingletonsInstantiated() {
for (RestTemplate restTemplate : LoadBalancerAutoConfiguration.this.restTemplates) {
for (RestTemplateCustomizer customizer : customizers) {
customizer.customize(restTemplate);
}
}
}
};
}
}
在該類中,首先維護了一個被@LoadBalanced修飾的RestTemplate對象的List,在初始化的過程中,通過調用customizer.customize(restTemplate)方法來給RestTemplate增加攔截器LoadBalancerInterceptor。
而LoadBalancerInterceptor,用於實時攔截,在LoadBalancerInterceptor實現來負載均衡。LoadBalancerInterceptor的攔截方法如下:
@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));
}
總結
綜上所述,Ribbon的負載均衡,主要通過LoadBalancerClient來實現的,而LoadBalancerClient具體交給了ILoadBalancer來處理,ILoadBalancer通過配置IRule、IPing等信息,並向EurekaClient獲取註冊列表的信息,並默認10秒一次向EurekaClient發送“ping”,進而檢查是否更新服務列表,最後,得到註冊列表後,ILoadBalancer根據IRule的策略進行負載均衡。
而RestTemplate 被@LoadBalance註解後,能過用負載均衡,主要是維護了一個被@LoadBalance註解的RestTemplate列表,並給列表中的RestTemplate添加攔截器,進而交給負載均衡器去處理。