Spring Cloud 進階--Ribbon核心組件IRule的使用及自定義負載均衡算法

                              《 Ribbon核心組件IRule的使用及自定義負載均衡算法 》

 

前言

在上一篇文章中,主要完成了 《 Rest微服務加入Ribbon負載均衡客戶端組件實現負載均衡 》,本篇將帶領讀者一步一步認識Ribbon的核心插件 “ IRule ”的常用 API 以及自定義算法規則詳細說明,本篇博文涉及的服務模塊包括:

  • 修改消費者模塊 “ microservice-consumer-80 ”,新增具體的算法配置類,實現指定規則的調用;

 

Ribbon核心組件IRule的使用及自定義負載均衡算法

1、IRule 常見的API

這兒先細列哈Ribbon核心組件IRule中常見的幾個算法API,一般情況下,這幾個API已經可以滿足實際的業務需求了,如下:

API 說明
RoundRobinRule 廣爲人知和最基本的負載平衡策略,即輪詢算法。(是Ribbon默認的負載均衡機制)
RandomRule 一種隨機分配現有流量的負載平衡策略,即隨機訪問算法
RetryRule 先按照 RoundRobinRule 的策略訪問服務,如果訪問的服務宕機或者出現異常的情況,則在指定時間內會進行重試,訪問其它可用的服務
BestAvailableRule 首先會先過濾掉由於多次訪問故障而處於斷路器跳閘狀態的服務,然後選擇一個併發量最小的服務訪問
ZoneAvoidanceRule 默認規則,複合判斷server所在區域的性能和server的可用性選擇服務器
AvailabilityFilteringRule 首先會先過濾掉由於多次訪問故障而處於斷路器跳閘狀態的服務,還有併發的連接數量超過閾值的服務,然後對剩餘的服務列表按照輪詢策略進行訪問
WeightedResponseTimeRule 根據平均響應時間計算所有服務的權重,響應時間越快服務權重越大被選中的概率越高。
剛啓動時如果統計信息不足,則使用RoundRobinRule策略,等統計信息足夠,
會切換到WeightedResponseTimeRule

 

接下將列舉前三個API的源碼,只爲展示其表面的算法,當然更多的內部實現細節,請轉到Ribbon官網咯。

RoundRobinRule:

package com.netflix.loadbalancer;

import com.netflix.client.config.IClientConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * The most well known and basic load balancing strategy, i.e. Round Robin Rule.
 *
 * @author stonse
 * @author Nikos Michalakis <[email protected]>
 *
 */
public class RoundRobinRule extends AbstractLoadBalancerRule
{

	private AtomicInteger nextServerCyclicCounter;
	private static final boolean AVAILABLE_ONLY_SERVERS = true;
	private static final boolean ALL_SERVERS = false;

	private static Logger log = LoggerFactory.getLogger(RoundRobinRule.class);

	public RoundRobinRule()
	{
		nextServerCyclicCounter = new AtomicInteger(0);
	}

	public RoundRobinRule(ILoadBalancer lb)
	{
		this();
		setLoadBalancer(lb);
	}

	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;
		}
	}

	@Override
	public Server choose(Object key)
	{
		return choose(getLoadBalancer(), key);
	}
}

 

RandomRule(這個博主在實際的業務中也常常遇到:首先獲取服務列表,再獲取服務總的個數,獲取這個總數內的隨機數,得到的這個隨機數即服務的下標,拿到下標後,再去服務列表中取指定的服務名稱即可,不知道你們有沒有遇到過呢!):

package com.netflix.loadbalancer;

import com.netflix.client.config.IClientConfig;

import java.util.List;
import java.util.concurrent.ThreadLocalRandom;

/**
 * A loadbalacing strategy that randomly distributes traffic amongst existing
 * servers.
 * 
 * @author stonse
 * 
 */
public class RandomRule extends AbstractLoadBalancerRule
{

	/**
	 * Randomly choose from all living servers
	 */
	@edu.umd.cs.findbugs.annotations.SuppressWarnings(value = "RCN_REDUNDANT_NULLCHECK_OF_NULL_VALUE")
	public 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)
			{
				/*
				 * No servers. End regardless of pass, because subsequent passes only get more
				 * restrictive.
				 */
				return null;
			}

			int index = chooseRandomInt(serverCount);
			server = upList.get(index);

			if (server == null)
			{
				/*
				 * The only time this should happen is if the server list were somehow trimmed.
				 * This is a transient condition. Retry after yielding.
				 */
				Thread.yield();
				continue;
			}

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

			// Shouldn't actually happen.. but must be transient or a bug.
			server = null;
			Thread.yield();
		}

		return server;

	}

	protected int chooseRandomInt(int serverCount)
	{
		return ThreadLocalRandom.current().nextInt(serverCount);
	}

	@Override
	public Server choose(Object key)
	{
		return choose(getLoadBalancer(), key);
	}
}

 

RetryRule:

package com.netflix.loadbalancer;

import com.netflix.client.config.IClientConfig;

/**
 * Given that {@link IRule} can be cascaded, this {@link RetryRule} class allows
 * adding a retry logic to an existing Rule.
 * 
 * @author stonse
 * 
 */
public class RetryRule extends AbstractLoadBalancerRule
{
	IRule subRule = new RoundRobinRule();
	long maxRetryMillis = 500;

	public RetryRule()
	{
	}

	public RetryRule(IRule subRule)
	{
		this.subRule = (subRule != null) ? subRule : new RoundRobinRule();
	}

	public RetryRule(IRule subRule, long maxRetryMillis)
	{
		this.subRule = (subRule != null) ? subRule : new RoundRobinRule();
		this.maxRetryMillis = (maxRetryMillis > 0) ? maxRetryMillis : 500;
	}

	public void setRule(IRule subRule)
	{
		this.subRule = (subRule != null) ? subRule : new RoundRobinRule();
	}

	public IRule getRule()
	{
		return subRule;
	}

	public void setMaxRetryMillis(long maxRetryMillis)
	{
		if (maxRetryMillis > 0)
		{
			this.maxRetryMillis = maxRetryMillis;
		} else
		{
			this.maxRetryMillis = 500;
		}
	}

	public long getMaxRetryMillis()
	{
		return maxRetryMillis;
	}

	@Override
	public void setLoadBalancer(ILoadBalancer lb)
	{
		super.setLoadBalancer(lb);
		subRule.setLoadBalancer(lb);
	}

	/*
	 * Loop if necessary. Note that the time CAN be exceeded depending on the
	 * subRule, because we're not spawning additional threads and returning early.
	 */
	public Server choose(ILoadBalancer lb, Object key)
	{
		long requestTime = System.currentTimeMillis();
		long deadline = requestTime + maxRetryMillis;

		Server answer = null;

		answer = subRule.choose(key);

		if (((answer == null) || (!answer.isAlive())) && (System.currentTimeMillis() < deadline))
		{

			InterruptTask task = new InterruptTask(deadline - System.currentTimeMillis());

			while (!Thread.interrupted())
			{
				answer = subRule.choose(key);

				if (((answer == null) || (!answer.isAlive())) && (System.currentTimeMillis() < deadline))
				{
					/* pause and retry hoping it's transient */
					Thread.yield();
				} else
				{
					break;
				}
			}

			task.cancel();
		}

		if ((answer == null) || (!answer.isAlive()))
		{
			return null;
		} else
		{
			return answer;
		}
	}

	@Override
	public Server choose(Object key)
	{
		return choose(getLoadBalancer(), key);
	}
}

 

2、實現隨機訪問策略,一種隨機分配現有流量的負載平衡策略

修改消費者服務 “ microservice-consumer-80 ” 的 “ ConfigBean ” 類,新增內容:

	@Bean
	public IRule randomIRule() {
		return new RandomRule();
	}

完整內容:

package com.huazai.springcloud.cfgbeans;

import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.RandomRule;

/**
 * 
 * <p>
 * 
 * @ClassName : ConfigBean
 *            </p>
 *            <p>
 * @Description : TODO
 *              </p>
 * 
 * @Author : HuaZai
 * @ContactInformation : [email protected]
 * @Date : 2018年05月23日 下午9:01:21
 * @Version : V1.0.0
 * 
 * @param
 */
@Configuration
public class ConfigBean
{
	@Bean
	@LoadBalanced
	public RestTemplate getRestTemplate()
	{
		return new RestTemplate();
	}
	
	@Bean
	public IRule randomIRule() {
		return new RandomRule();
	}
	
	
}

 

測試:

首先啓動 Eureka 集羣,再啓動三臺提供者服務器,最後啓動消費者服務器,並訪問消費者服務器地址,反覆刷新,發現訪問的服務器是沒有規則的,隨機訪問,注意數據庫的變化,如下圖:

 

3、自定義訪問策略

新建一個類 “ IRuleConfig ”,用於定義基於 Ribbon 的訪問策略,完整內容如下:

package com.huazai.springcloud.cfgbeans;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.RandomRule;

/**
 * 
 * @author HuaZai
 * @contact [email protected]
 *          <ul>
 * @description 自定義訪問策略
 *              </ul>
 * @className IRuleConfig
 * @package com.huazai.springcloud.cfgbeans
 * @createdTime 2018年05月23日 下午3:57:31
 *
 * @version V1.0.0
 */
@Configuration
public class IRuleConfig
{
	@Bean
	public IRule myIRule()
	{
		// return new RetryRule();
		return new RandomRule();
		// return new BestAvailableRule();
		// return new ZoneAvoidanceRule();
		// return new AvailabilityFilteringRule();
		// return new WeightedResponseTimeRule();
	}
}

 

修改消費者 “ microservice-consumer-80 ” 的主啓動類,新增註解 “ @RibbonClient ” ,其目的是在啓動該微服務的時候就能去加載自定義 Ribbon 配置類,從而使配置立即生效,完整內容如下:

package com.huazai.springcloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.ribbon.RibbonClient;

import com.huazai.springcloud.cfgbeans.ConfigBean;
import com.huazai.springcloud.cfgbeans.IRuleConfig;

/**
 * 
 * @author HuaZai
 * @contact [email protected]
 *          <ul>
 * @description
 *              <li>服務消費者
 *              </ul>
 * @className MicroServiceConsumerApp
 * @package com.huazai.springcloud
 * @createdTime 2018年05月22日 下午3:47:02
 *
 * @version V1.0.0
 */
@SpringBootApplication
@EnableEurekaClient
@RibbonClient(name = "MICROSERVICE-PROVIDER", configuration = IRuleConfig.class)
public class MicroServiceConsumerApp
{

	public static void main(String[] args)
	{
		SpringApplication.run(MicroServiceConsumerApp.class, args);
	}
}

 

測試

首先啓動 Eureka 集羣,再啓動三臺提供者服務器,最後啓動消費者服務器,並訪問消費者服務器地址,反覆刷新,發現訪問的服務器是沒有規則的,隨機訪問,注意數據庫的變化,和上面一樣的哈,如下圖:

 

4、新增需求

需求一:由於公司資源有限(說白了就是降低成本),現在有三臺服務器,配置由高到低排序 1號服務器 > 2號服務器 > 3號服務器,要求:1號服務器承載50%的流量,2號服務器承載30%的流量,3號服務器承載20%的流量,要求自定義Ribbon 算法實現,這個是當年博主遇到的一個很犯賤的問題,對於當時而言很難,因爲什麼都要自己寫,不像現在什麼都封裝成了API,直接調用即可,當然現在一般的互聯網公司很難遇到了,所以這兒的實現就省略咯。。。

需求二:根據客戶那邊對環境的需求,指定方案需要對流量進行絕對的限制,但依然是輪詢機制,例如:1號服務器被調用3次,之後,2號服務器被調用3次,之後,3號服務器被調用3次,因爲也基於輪詢機制的,所以可以直接在上面的 " RoundRobinRule.java " 的源代碼中進行簡單修改即可。

1)新增自定義算法 “ Custom_RandomRule ” 類,並重寫IRule的 “ choose ” 方法和IClientConfigAware的 “  initWithNiwsConfig”方法,完整內容如下:

package com.huazai.springcloud.cfgbeans;

import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.AbstractLoadBalancerRule;
import com.netflix.loadbalancer.ILoadBalancer;
import com.netflix.loadbalancer.Server;

/**
 * 
 * @author HuaZai
 * @contact [email protected]
 *          <ul>
 * @description
 *              <li>自定義輪詢機制
 *              </ul>
 * @className Custom_RandomRule
 * @package com.huazai.springcloud.cfgbeans
 * @createdTime 2018年05月24日 下午4:49:51
 *
 * @version V1.0.0
 */
@SuppressWarnings("unused")
public class Custom_RandomRule extends AbstractLoadBalancerRule
{
	private AtomicInteger nextServerCyclicCounter;
	private static final boolean AVAILABLE_ONLY_SERVERS = true;
	private static final boolean ALL_SERVERS = false;

	private int total = 0; // 總的被調用次數
	private int currentIndex = 0;// 當前提供服務的機器號

	private static Logger log = LoggerFactory.getLogger(Custom_RandomRule.class);

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

		Server server = null;

		while (server == null)
		{
			if (Thread.interrupted())
			{
				return null;
			}

			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;
			}

			// 實現每臺服務請求三次的業務邏輯
			if (total < 3)
			{
				server = reachableServers.get(currentIndex);
				total++;
			} else
			{
				total = 0;
				currentIndex++;
				if (currentIndex >= upCount)
				{
					currentIndex = 0;
				}
			}

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

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

			// Next.
			server = null;
			Thread.yield();
		}
		return server;
	}

	@Override
	public Server choose(Object key)
	{
		return choose(getLoadBalancer(), key);
	}

	@Override
	public void initWithNiwsConfig(IClientConfig clientConfig)
	{
		// TODO Auto-generated method stub

	}

}

 

修改配置文件 “ IRuleConfig ” ,將 IRule 修改爲自定義的服務調用算法機制,完整內容如下:

package com.huazai.springcloud.cfgbeans;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.RandomRule;

/**
 * 
 * @author HuaZai
 * @contact [email protected]
 *          <ul>
 * @description 自定義訪問策略
 *              </ul>
 * @className IRuleConfig
 * @package com.huazai.springcloud.cfgbeans
 * @createdTime 2018年05月23日 下午3:57:31
 *
 * @version V1.0.0
 */
@Configuration
public class IRuleConfig
{
	@Bean
	public IRule myIRule()
	{
		return new Custom_RandomRule(); // 自定義算法
	}
}

 

效果圖如下:

 

 

 

 

GitLab 源碼地址:

項目源碼地址(zip格式的工程包):


好了,關於 Spring Cloud 進階--Ribbon核心組件IRule的使用及自定義負載均衡算法 就寫到這兒了,如果還有什麼疑問或遇到什麼問題歡迎掃碼提問,也可以給我留言哦,我會一一詳細的解答的。 
歇後語:“ 共同學習,共同進步 ”,也希望大家多多關注CSND的IT社區。


作       者: 華    仔
聯繫作者: [email protected]
來        源: CSDN (Chinese Software Developer Network)
原        文: https://blog.csdn.net/Hello_World_QWP/article/details/88185574
版權聲明: 本文爲博主原創文章,請在轉載時務必註明博文出處!
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章