SpringCloud學習筆記【八】:Ribbon負載均衡服務呼叫
本篇要點
- 介紹Ribbon的基本功能。
- 介紹負載均衡的相關概念。
- 演示Ribbon負載均衡。
- 學習Ribbon預設自帶的負載均衡規則。
- 學習輪詢演算法原理。
Ribbon是什麼?
Ribbon是Netflix釋出的開源專案,主要功能是提供客戶端的軟體負載均衡演算法和服務呼叫
Ribbon客戶端元件提供一系列完善的配置項如連線超時,重試等。簡單的說,就是在配置檔案中列出Load Balancer
(簡稱LB)後面所有的機器,Ribbon會自動的幫助你基於某種規則(如簡單輪詢,隨機連線等)去連線這些機器。我們很容易使用Ribbon實現自定義的負載均衡演算法。
目前Ribbon專案的狀態處於:維護中。
Spring Cloud Ribbon是基於Netflix Ribbon實現的一套客戶端負載均衡的工具。
LoadBalance負載均衡
負載均衡簡單的說就是將使用者的請求平攤的分配到多個服務上,從而達到系統的HA【高可用】。
常見的負載均衡有軟體Nginx,LVS,硬體 F5等。
Ribbon與Nginx負載均衡的區別
Nginx是伺服器負載均衡,客戶端所有請求都會交給nginx,nginx實現轉發請求,負載均衡由服務端實現。
Ribbon是本地負載均衡,在呼叫微服務介面時候,會在註冊中心上獲取註冊資訊服務列表之後快取到JVM本地,從而在本地實現RPC遠端服務呼叫。
集中式LB與程序內LB
集中式負載均衡:即在服務的消費方和提供方之間使用獨立的LB設施(可以是硬體,如F5, 也可以是軟體,如nginx), 由該設施負責把訪問請求通過某種策略轉發至服務的提供方;
程序內負載均衡:將LB邏輯整合到消費方,消費方從服務註冊中心獲知有哪些地址可用,然後自己再從這些地址中選擇出一個合適的伺服器。
Ribbon就屬於程序內LB
Ribbon負載均衡演示
Ribbon是一個軟負載均衡客戶端元件,它可以和其他所需請求的客戶端結合使用,和Eureka結合只是其中一個例項。
Ribbon工作步驟
- 選擇EurekaServer,優先選擇在同一區域內負載較少的server。
- 根據使用者指定的策略,從server獲取到的服務註冊列表中選擇一個地址。策略包括:輪詢,隨機,根據響應時間加權。
整合Ribbon
我們要整合Ribbon,當然需要引入Ribbon響應的依賴:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
事實上,我們之前Eureka的例子,通過80埠,輪詢訪問8001和8002埠,就是客戶端負載均衡的體現,我們之前引入的依賴如下:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
其實就已經整合了Ribbon,:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
<version>2.2.6.RELEASE</version>
<scope>compile</scope>
</dependency>
這就是為什麼我們不用顯式地去引入Ribbon的依賴,我們也可以知道Ribbon的實現其實就是:負載均衡+RestTemplate呼叫。
RestTemplate
getForEntity:返回物件為ResponseEntity物件,包含了響應中的一些重要資訊,如響應頭,響應狀態碼,響應體等。
getForObject:返回物件為響應體中資料轉化成的物件,基本可以理解為json。
Ribbon預設自帶的負載規則IRule
com.netflix.loadbalancer.IRule
介面定義了負載均衡的策略,包括輪詢,響應時間加權等等。
/**
* Interface that defines a "Rule" for a LoadBalancer. A Rule can be thought of
* as a Strategy for loadbalacing. Well known loadbalancing strategies include
* Round Robin, Response Time based etc.
*
* @author stonse
*
*/
public interface IRule{
/*
* choose one alive server from lb.allServers or
* lb.upServers according to key
*
* @return choosen Server object. NULL is returned if none
* server is available
*/
public Server choose(Object key);
public void setLoadBalancer(ILoadBalancer lb);
public ILoadBalancer getLoadBalancer();
}
com.netflix.loadbalancer.RoundRobinRule
:輪詢,最有名也是最主要的負載均衡策略。com.netflix.loadbalancer.RandomRule
:隨機,從存在的servers中隨機找一個。com.netflix.loadbalancer.RetryRule
:先按照輪詢的策略獲取服務,獲取失敗則在指定時間內獲取服務。com.netflix.loadbalancer.WeightedResponseTimeRule
:對RoundRobinRule的擴充套件,響應速度越快的權重越大,越容易被選擇。com.netflix.loadbalancer.BestAvailableRule
:先過濾掉由於多次訪問故障而處於斷路器跳閘狀態的服務,然後選擇一個併發量最小的服務。com.netflix.loadbalancer.AvailabilityFilteringRule
:先過濾故障例項,再選擇併發較小的例項。com.netflix.loadbalancer.ZoneAvoidanceRule
:複合判斷server所在區域的效能和server的可用性選擇伺服器。
Ribbon如何更改負載規則
我們需要在@ComponentScan掃描不到的包下定義配置類,否則該配置類就會被所有Ribbon客戶端所共享,因而達不到定製的效果。
定製規則
包結構如下:
啟動類掃描com.hyh.springcloud
包及其子包,我們配置在com.hyh.rules
包下。
標識客戶端
@EnableEurekaClient
@SpringBootApplication
@RibbonClient(name = "CLOUD-PAYMENT-SERVICE",configuration = MyRule.class)
public class Order80Application {
public static void main(String[] args) {
SpringApplication.run(Order80Application.class, args);
}
}
指定為RibbonClient,name訪問CLOUD-PAYMENT-SERVICE
提供的服務,configuration指定定義的規則。
再次測試,發現負載均衡的規則已經成為隨機獲取server。
Ribbon負載均衡演算法
輪詢原理
看看最重要也是最基礎的輪詢演算法吧,大致思想就是:rest介面第幾次請求數 % 伺服器叢集總數量 = 實際呼叫伺服器位置下標
,每次重啟伺服器後rest介面計數從1開始。
我們不妨開啟原始碼看一下,可能會更加清楚一些:
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() {
// 初始化AtmoicInteger = 0
nextServerCyclicCounter = new AtomicInteger(0);
}
public RoundRobinRule(ILoadBalancer lb) {
this();
// 初始化設定LoadBalancer
setLoadBalancer(lb);
}
public Server choose(ILoadBalancer lb, Object key) {
// 不存在LoadBalancer
if (lb == null) {
log.warn("no load balancer");
return null;
}
// server代表最終會被選擇的
Server server = null;
// 嘗試的次數
int count = 0;
while (server == null && count++ < 10) {
// 獲取up and reachable的servers
List<Server> reachableServers = lb.getReachableServers();
// 所有的servers
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
server = allServers.get(nextServerIndex);
if (server == null) {
/* Transient. */
Thread.yield();
continue;
}
// 滿足條件,返回server
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;
// CAS 操作
if (nextServerCyclicCounter.compareAndSet(current, next))
return next;
}
}
@Override
public Server choose(Object key) {
return choose(getLoadBalancer(), key);
}
@Override
public void initWithNiwsConfig(IClientConfig clientConfig) {
}
}
嘗試模擬輪詢演算法
@Component
public class MyLoadBalancer implements LoadBalancer {
private AtomicInteger atomicInteger = new AtomicInteger(0);
public final int getAndIncrement(){
int current;
int next;
do{
current = this.atomicInteger.get();
next = current >= Integer.MAX_VALUE ? 0 : current + 1;
}while (!this.atomicInteger.compareAndSet(current,next));
System.out.println("訪問次數 next : " + next);
return next;
}
@Override
public ServiceInstance instances(List<ServiceInstance> serviceInstances) {
int index = getAndIncrement() % serviceInstances.size();
return serviceInstances.get(index);
}
}
@RestController
@Slf4j
public class OrderController {
@Resource
private RestTemplate restTemplate;
@Resource
private LoadBalancer loadBalancer;
@Resource
private DiscoveryClient discoveryClient;
@GetMapping("/consumer/payment/lb")
public String getPaymentLb() {
List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");
if (instances == null || instances.size() == 0) {
return null;
}
ServiceInstance instance = loadBalancer.instances(instances);
URI uri = instance.getUri();
return restTemplate.getForObject(uri + "/payment/lb", String.class);
}
}
原始碼下載
本系列文章為《尚矽谷SpringCloud教程》的學習筆記【版本稍微有些不同,後續遇到bug再做相關說明】,主要做一個長期的記錄,為以後學習的同學提供示例,程式碼同步更新到Gitee:https://gitee.com/tqbx/spring-cloud-learning,並且以標籤的形式詳細區分每個步驟,這個系列文章也會同步更新。