Ribbon 內置的負載均衡規則
在 com.netflix.loadbalancer
包下有一個接口 IRule
,它可以根據特定的算法從服務列表中選取一個要訪問的服務,默認使用的是「輪詢機制」
- RoundRobinRule:輪詢
- RandomRule:隨機
- RetryRule:先按照
RoundRobinRule
的策略獲取服務,如果獲取服務失敗則在指定時間內會進行重試,獲取可用的服務 - WeightedResponseTimeRule:對
RoundRobinRule
的擴展,響應速度越快的實例選擇權重越大,越容易被選擇 - BestAvailableRule:會過濾掉由於多次訪問故障而處於斷路器跳閘狀態的服務,然后選擇一個並發量最小的服務
- AvailabilityFilteringRule:先過濾掉故障實例,再選擇並發較小的實例
- ZoneAvoidanceRule:默認規則,復合判斷 server 所在區域的性能和 server 的可用性選擇服務器
負載規則的替換
如果不想使用 Ribbon 默認使用的規則,我們可以通過自定義配置類的方式,手動指定使用哪一種。
需要注意的是,自定義配置類不能放在 @ComponentScan
所掃描的當前包下以及子包下,否則我們自定義的這個配置類就會被所有的 Ribbon 客戶端所共享,達不到特殊化定制的目的了。
因此我們需要在 Spring Boot 啟動類所在包的外面新建一個包存放自定義配置類
@Configuration
public class MyselfRule {
@Bean
public IRule rule(){
//隨機
return new RandomRule();
}
}
然后在啟動類上添加如下注解,指定服務名及自定義配置類
@RibbonClient(value = "CLOUD-PAYMENT-SERVICE", configuration = MyselfRule.class)
Ribbon 默認負載輪詢算法的原理
算法概述
rest 接口第幾次請求數 % 服務器集群總個數 = 實際調用服務器位置下標,服務每次重啟后 rest 請求數變為1
源碼
public Server choose(ILoadBalancer lb, Object key) {
if (lb == null) {
log.warn("no load balancer");
return null;
}
Server server = null;
int count = 0;
//循環獲取服務,最多獲取10次
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;
}
//通過此方法獲取服務的下標,使用了 CAS 和自旋鎖
private int incrementAndGetModulo(int modulo) {
for (;;) {
int current = nextServerCyclicCounter.get();
int next = (current + 1) % modulo;
if (nextServerCyclicCounter.compareAndSet(current, next))
return next;
}
}
手寫輪詢算法
在服務提供者寫一個方法,返回端口號看效果就行
@GetMapping("/payment/lb")
public String roundLb(){
return this.serverPort;
}
負載均衡接口
public interface LoadBalancer {
/**
* 獲取服務實例
*/
ServiceInstance getInstance(List<ServiceInstance>serviceInstances);
}
算法實現類
@Component
public class MyLb implements LoadBalancer {
private AtomicInteger atomicInteger = new AtomicInteger(0);
/**
* 使用「自旋鎖」和「CAS」增加請求次數
*/
public final int incrementAndGet() {
int current;
int next;
do {
current = atomicInteger.get();
//防溢出
next = current >= Integer.MAX_VALUE ? 0 : current + 1;
} while (!atomicInteger.compareAndSet(current, next));
return next;
}
@Override
public ServiceInstance getInstance(List<ServiceInstance> serviceInstances) {
// 實際調用服務器位置下標 = rest 接口第幾次請求數 % 服務器集群總個數
int index = incrementAndGet() % serviceInstances.size();
return serviceInstances.get(index);
}
}
編寫服務消費者方法,記得注釋 @LoadBalanced
注解,否則不生效
@GetMapping("/consumer/payment/lb")
public String roundLb(){
List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");
if (instances == null || instances.size() <= 0){
return null;
}
ServiceInstance instance = loadBalancer.getInstance(instances);
URI uri = instance.getUri();
return restTemplate.getForObject(uri + "/payment/lb", String.class);
}