Ribbon的負載均衡策略及原理

Load Balance負載均衡是用於解決一臺機器(一個進程)無法解決所有請求而產生的一種算法。像nginx可以使用負載均衡分配流量,ribbon為客戶端提供負載均衡,dubbo服務調用裡的負載均衡等等,很多地方都使用到了負載均衡。

使用負載均衡帶來的好處很明顯:

  1. 當集群裡的1臺或者多臺服務器down的時候,剩餘的沒有down的服務器可以保證服務的繼續使用
  2. 使用了更多的機器保證了機器的良性使用,不會由於某一高峰時刻導致系統cpu急劇上升

負載均衡有好幾種實現策略,常見的有:

  1. 隨機 (Random)
  2. 輪詢 (RoundRobin)
  3. 一致性哈希 (ConsistentHash)
  4. 哈希 (Hash)
  5. 加權(Weighted)

ILoadBalance 負載均衡器

ribbon是一個為客戶端提供負載均衡功能的服務,它內部提供了一個叫做ILoadBalance的接口代表負載均衡器的操作,比如有添加服務器操作、選擇服務器操作、獲取所有的服務器列表、獲取可用的服務器列表等等。ILoadBalance的繼承關係如下:


Ribbon的負載均衡策略及原理

負載均衡器是從EurekaClient(EurekaClient的實現類為DiscoveryClient)獲取服務信息,根據IRule去路由,並且根據IPing判斷服務的可用性。

負載均衡器多久一次去獲取一次從Eureka Client獲取註冊信息呢?在BaseLoadBalancer類下,BaseLoadBalancer的構造函數,該構造函數開啟了一個PingTask任務setupPingTask();,代碼如下:

<code> public BaseLoadBalancer(String name, IRule rule, LoadBalancerStats stats,
IPing ping, IPingStrategy pingStrategy) {
if (logger.isDebugEnabled()) {
logger.debug("LoadBalancer: initialized");
}
this.name = name;
this.ping = ping;
this.pingStrategy = pingStrategy;
setRule(rule);
setupPingTask();
lbStats = stats;
init();
}/<code>

setupPingTask()的具體代碼邏輯,它開啟了ShutdownEnabledTimer執行PingTask任務,在默認情況下pingIntervalSeconds為10,即每10秒鐘,向EurekaClient發送一次”ping”。

<code>void setupPingTask() {
if (canSkipPing()) {
return;
}
if (lbTimer != null) {
lbTimer.cancel();
}
lbTimer = new ShutdownEnabledTimer("NFLoadBalancer-PingTimer-" + name,
true);
lbTimer.schedule(new PingTask(), 0, pingIntervalSeconds * 1000);
forceQuickPing();
}/<code>

PingTask源碼,即new一個Pinger對象,並執行runPinger()方法。

查看Pinger的runPinger()方法,最終根據 pingerStrategy.pingServers(ping, allServers)來獲取服務的可用性,如果該返回結果,如之前相同,則不去向EurekaClient獲取註冊列表,如果不同則通知ServerStatusChangeListener或者changeListeners發生了改變,進行更新或者重新拉取。

完整過程是:

LoadBalancerClient(RibbonLoadBalancerClient是實現類)在初始化的時候(execute方法),會通過ILoadBalance(BaseLoadBalancer是實現類)向Eureka註冊中心獲取服務註冊列表,並且每10s一次向EurekaClient發送“ping”,來判斷服務的可用性,如果服務的可用性發生了改變或者服務數量和之前的不一致,則從註冊中心更新或者重新拉取。LoadBalancerClient有了這些服務註冊列表,就可以根據具體的IRule來進行負載均衡。

IRule 路由

IRule接口代表負載均衡策略:

<code>public interface IRule{
public Server choose(Object key);
public void setLoadBalancer(ILoadBalancer lb);
public ILoadBalancer getLoadBalancer();
}/<code>

IRule接口的實現類有以下幾種:


Ribbon的負載均衡策略及原理


Ribbon的負載均衡策略及原理

其中RandomRule表示隨機策略、RoundRobinRule表示輪詢策略、WeightedResponseTimeRule表示加權策略、BestAvailableRule表示請求數最少策略等等。

隨機策略很簡單,就是從服務器中隨機選擇一個服務器,RandomRule的實現代碼如下:

<code>lic Server choose(ILoadBalancer lb, Object key) {
if (lb == null) {
return null;
}
Server server = null;


while (server == null) {
if (Thread.interrupted()) {
return null;
}
List<server> upList = lb.getReachableServers();
List<server> allList = lb.getAllServers();
int serverCount = allList.size();
if (serverCount == 0) {
return null;
}
int index = rand.nextInt(serverCount); // 使用jdk內部的Random類隨機獲取索引值index
server = upList.get(index); // 得到服務器實例

if (server == null) {
Thread.yield();
continue;
}

if (server.isAlive()) {
return (server);
}

server = null;
Thread.yield();
}
return server;
}/<server>/<server>/<code>

RoundRobinRule輪詢策略表示每次都取下一個服務器,比如一共有5臺服務器,第1次取第1臺,第2次取第2臺,第3次取第3臺,以此類推:

<code> public Server choose(ILoadBalancer lb, Object key) {
if (lb == null) {
log.warn("no load balancer");
return null;
}

Server server = null;
int count = 0;
while (server == null && count++ < 10) {
List<server> reachableServers = lb.getReachableServers();
List<server> allServers = lb.getAllServers();
int upCount = reachableServers.size();

int serverCount = allServers.size();

if ((upCount == 0) || (serverCount == 0)) {
log.warn("No up servers available from load balancer: " + lb);
return null;
}

int nextServerIndex = incrementAndGetModulo(serverCount);
server = allServers.get(nextServerIndex);

if (server == null) {
/* Transient. */
Thread.yield();
continue;
}

if (server.isAlive() && (server.isReadyToServe())) {
return (server);
}

// Next.
server = null;
}

if (count >= 10) {
log.warn("No available alive servers after 10 tries from load balancer: "
+ lb);
}
return server;
}

/**
* Inspired by the implementation of {@link AtomicInteger#incrementAndGet()}.
*
* @param modulo The modulo to bound the value of the counter.
* @return The next value.
*/
private int incrementAndGetModulo(int modulo) {
for (;;) {
int current = nextServerCyclicCounter.get();
int next = (current + 1) % modulo;
if (nextServerCyclicCounter.compareAndSet(current, next))
return next;
}
}/<server>/<server>/<code>

WeightedResponseTimeRule繼承了RoundRobinRule,開始的時候還沒有權重列表,採用父類的輪詢方式,有一個默認每30秒更新一次權重列表的定時任務,該定時任務會根據實例的響應時間來更新權重列表,choose方法做的事情就是,用一個(0,1)的隨機double數乘以最大的權重得到randomWeight,然後遍歷權重列表,找出第一個比randomWeight大的實例下標,然後返回該實例,代碼略。

BestAvailableRule策略用來選取最少併發量請求的服務器:

<code>public Server choose(Object key) {
if (loadBalancerStats == null) {
return super.choose(key);
}
List<server> serverList = getLoadBalancer().getAllServers(); // 獲取所有的服務器列表
int minimalConcurrentConnections = Integer.MAX_VALUE;
long currentTime = System.currentTimeMillis();
Server chosen = null;
for (Server server: serverList) { // 遍歷每個服務器
ServerStats serverStats = loadBalancerStats.getSingleServerStat(server); // 獲取各個服務器的狀態
if (!serverStats.isCircuitBreakerTripped(currentTime)) { // 沒有觸發斷路器的話繼續執行
int concurrentConnections = serverStats.getActiveRequestsCount(currentTime); // 獲取當前服務器的請求個數
if (concurrentConnections < minimalConcurrentConnections) { // 比較各個服務器之間的請求數,然後選取請求數最少的服務器並放到chosen變量中
minimalConcurrentConnections = concurrentConnections;
chosen = server;
}
}
}
if (chosen == null) { // 如果沒有選上,調用父類ClientConfigEnabledRoundRobinRule的choose方法,也就是使用RoundRobinRule輪詢的方式進行負載均衡
return super.choose(key);
} else {
return chosen;
}
}/<server>/<code>

使用Ribbon提供的負載均衡策略很簡單,只需以下幾部:

1、創建具有負載均衡功能的RestTemplate實例

<code>@Bean 

@LoadBalanced
RestTemplate restTemplate() {
return new RestTemplate();
}/<code>

使用RestTemplate進行rest操作的時候,會自動使用負載均衡策略,它內部會在RestTemplate中加入LoadBalancerInterceptor這個攔截器,這個攔截器的作用就是使用負載均衡。

默認情況下會採用輪詢策略,如果希望採用其它策略,則指定IRule實現,如:

<code>@Bean
public IRule ribbonRule() {
return new BestAvailableRule();
}/<code>

這種方式對Feign也有效。

我們也可以參考ribbon,自己寫一個負載均衡實現類。

可以通過下面方法獲取負載均衡策略最終選擇了哪個服務實例:

<code>@Autowired
\tLoadBalancerClient loadBalancerClient;
\t
\t//測試負載均衡最終選中哪個實例
\tpublic String getChoosedService() {
\t ServiceInstance serviceInstance = loadBalancerClient.choose("USERINFO-SERVICE");
\t StringBuilder sb = new StringBuilder();
\t sb.append("host: ").append(serviceInstance.getHost()).append(", ");
\t sb.append("port: ").append(serviceInstance.getPort()).append(", ");
\t sb.append("uri: ").append(serviceInstance.getUri());
\t return sb.toString();
\t}/<code>

JAVA進階架構程序員福利:我這裡還總結整理了比較全面的JAVA相關的面試資料,都已經整理成了

PDF版,這些都可以分享給大家,關注私信我:【806】,免費領取!


分享到:


相關文章: