自動化配置
由於 Ribbon 中定義的每一個接口都有多種不同的策略實現,同時這些接口之間又有一定的依賴關系,Spring Cloud Ribbon 中的自動化配置能夠很方便的自動化構建接口的具體實現,接口如下:
- IClientConfig:Ribbon 的客戶端配置,默認采用 com.netflix.client.config.DefaultClientConfigImpl 實現。
- IRule:Ribbon 的負載均衡策略,默認采用 com.netflix.loadbalancer.ZoneAvoidanceRule 實現,該策略能夠在多區域環境下選擇最佳區域的實例進行訪問
- IPing:Ribbon 的實例檢查策略,默認采用 com.netflix.loadbalancer.NoOpPing 實現,該檢查策略是一個特殊的實現,實際上他並不會檢查實例是否可用,而是始終返回 true ,默認認為所有服務實例都是可以使用
- ServerList<Server>:服務實例清單的維護機制,默認采用 com.netflix.loadbalancer.ConfigurationBasedServerList 實現。
- ServerListFilter<Server>:服務實例清單過濾機制,默認采用 org.springframework.cloud.netflix.ribbon.ZonePreferenceServerListFilter 實現,該策略能夠優先過濾出與請求調用方處理同區域的服務實現
- ILoadBalancer:負載均衡器,默認采用 com.netflix.loadbalancer.ZoneAwareLoadBalancer 實現,他具備了區域感知的能力
這些自動化配置內容是在沒有引入 Spring Cloud Eureka 等服務治理框架時如此,在同時引入 Eureka 和 Ribbon 依賴時,自動化配置會有一些不同。通過自動化配置的實現,我們可以輕松的實現客戶端的負載均衡,同時,針對一些個性化的需求,我們也可以方便的替換上面的這些默認實現,示例代碼:
@Configurable
public class RibbonConfiguration {
@Bean
public IRule ribbonRule(IClientConfig clientConfig) {
return new WeightedResponseTimeRule();
}
}
可以使用 @RibbonClient 注解來實現更細粒度的客戶端配置,創建 ConsumerHelloserviceApplication 主類,使用 @RibbonClient 注解來指定服務使用 WebServiceRibbonConfiguration 配置,代碼如下:
@RibbonClient (name = "ORG.DRSOFT.WEBSERVICE.HELLOSERVICE", configuration = RibbonConfiguration.class)
@EnableDiscoveryClient
@SpringBootApplication
public class ConsumerHelloserviceApplication {
public static void main(String[] args) {
SpringApplication.run(ConsumerHelloserviceApplication.class, args);
}
@LoadBalanced
@Bean
public RestTemplate createRestTemplate() {
return new RestTemplate();
}
}
注意:如果需要使服務單獨配置,那么配置類不能放在@ComponentScan所掃描的當前包下以及子包下,否則我們自定義的這個配置類就會被所有的Ribbon客戶端所共享,@SpringBootApplication 注解中就包含了 @ComponentScan 注解,因此必須使配置類處於不同的包以及子包。
如果需要設置默認配置或者多個@RibbonClient 注解,可以使用 @RibbonClients 注解,其 defaultConfiguration 可以設置默認的自定義配置,配置類不限定所屬包,value 可以設置多個 @RibbonClient 注解,示例代碼如下:
@RibbonClients (defaultConfiguration = RibbonConfiguration.class)
@EnableDiscoveryClient
@SpringBootApplication
public class ConsumerHelloserviceApplication {
public static void main(String[] args) {
SpringApplication.run(ConsumerHelloserviceApplication.class, args);
}
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
與 Eureka 結合
當在 Spring Cloud 的應用中同時應用 Spring Cloud Ribbon 和 Spring Cloud Eureka 依賴時,會觸發 Eureka 中實現的對 Ribbon 的自動化配置,接口實現如下:
- ServerList<Server>:服務實例清單的維護機制,默認采用 com.netflix.niws.loadbalancer.DiscoveryEnabledNIWSServerList實現,該實現會將服務清單列表交給 Eureka 的服務治理機制來進行維護
- IPing:Ribbon 的實例檢查策略,默認采用 com.netflix.niws.loadbalancer.NIWSDiscoveryPing 實現,該實現將實例檢查的任務交給了服務治理框架來進行維護
負載均衡策略
IRule 接口其Ribbon 實現了非常多的選擇策略,下面我們來詳細說明一下IRule 接口的定義如下:
public interface IRule{
public Server choose(Object key);
public void setLoadBalancer(ILoadBalancer lb);
public ILoadBalancer getLoadBalancer();
}
IRule 接口的具體實現:
- AbstractLoadBalancerRule:負載均衡策略的抽象類,在該抽象類中定義了負載均衡器 ILoadBalancer 對象,該對象能夠根據在具體實現選擇服務策略時,獲取到一些負載均衡器中維護的信息來作為分配依據,並以此設計一些算法來實習針對特定場景的高效策略
-
RandomRule:該策略實現了從服務實例清單中隨機選擇一個服務實例的功能,查看源碼可以看到 choose 函數實現,使用 Random類的 nextInt 函數來隨機獲取一個隨機數,並將隨機數作為座位 upList 的索引值來返回具體實例
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) {
return null;
}
int index = rand.nextInt(serverCount);
server = upList.get(index);
if (server == null) {
Thread.yield();
continue;
}
if (server.isAlive()) {
return (server);
}
server = null;
Thread.yield();
}
return server;
}
-
RoundRobinRule:該策略實現了按照線性輪詢的方式依次選擇每個服務實例的功能,其詳細結構和 RandomRule類似,具體實現如下:
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;
}
-
WeightedResponseTimeRule:該策略是對 RoundRobinRule的擴展,增加了根據實例的運行情況來計算權重,並根據權重來挑選實例,以達到更優的分配效果,實現主要有三個核心內容:
- 定時任務:在初始化的時候會通過 serverWeightTimer.schedule(new DynamicServerWeightTask(), 0, serverWeightTaskTimerInterval); 啟動一個定時任務,用來為每個服務實例計算權重,該任務默認30秒執行一次。
-
權重計算:維護實例的權重計算過程通過 maintainWeights 函數實現,具體代碼如下:
public void maintainWeights() {
ILoadBalancer lb = getLoadBalancer();
if (lb == null) {
return;
}
if (!serverWeightAssignmentInProgress.compareAndSet(false, true)) {
return;
}
try {
logger.info("Weight adjusting job started");
AbstractLoadBalancer nlb = (AbstractLoadBalancer) lb;
LoadBalancerStats stats = nlb.getLoadBalancerStats();
if (stats == null) {
return;
}
double totalResponseTime = 0;
for (Server server : nlb.getAllServers()) {
ServerStats ss = stats.getSingleServerStat(server);
totalResponseTime += ss.getResponseTimeAvg();
}
Double weightSoFar = 0.0;
List<Double> finalWeights = new ArrayList<Double>();
for (Server server : nlb.getAllServers()) {
ServerStats ss = stats.getSingleServerStat(server);
double weight = totalResponseTime - ss.getResponseTimeAvg();
weightSoFar += weight;
finalWeights.add(weightSoFar);
}
setWeights(finalWeights);
} catch (Exception e) {
logger.error("Error calculating server weights", e);
} finally {
serverWeightAssignmentInProgress.set(false);
}
}
計算規則為 weightSoFar + totalResponseTime - 實例的平均響應時間,其中 weightSoFar 初始化為零,並且每計算好一個權重值需要累加到 weightSoFar 上供下一次計算使用,假設有4個實例A、B、C、D,他們的平均響應時間為10、40、80、100,所以總響應時間是 230,每個實例的權重為總響應時間與實例自身的平均響應時間的差的累積所得,所以A、B、C、D的權重分別如下所示:
- 實例A:230-10=220
- 實例B:220+(230-40)=410
- 實例C:410+(230-80)=560
- 實例D:560+(230-100)=690
需要注意的是,這里的權重值只表示各實例權重區間的上限,每個實例的權重區間如下:
- 實例A:[0,220]
- 實例B:[220,410]
- 實例C:[410,560]
- 實例D:[560,690]
權重區間的寬度越大,而權重區間的寬度越大,被選中的概率越高
- 實例選擇:生成一個[0,最大權重值]區間內的隨機數,然后遍歷權重列表的索引值去服務實例列表中獲取具體的實例。
- ClientConfigEnabledRoundRobinRule:該策略比較特殊,他本身並沒有實現什么特殊的處理策略邏輯,在內部定義了一個RoundRobinRule 策略,我們不會直接使用該策略,但可以通過繼承該策略,默認的choose實現了線性輪詢策略,在子類中做一些高級策略時通常有可能存在一些無法實施的情況,那么就可以用父類的實現作為備選,后面的高級策略都是基於該類的擴展
-
BestAvailableRule:該策略繼承自 ClientConfigEnabledRoundRobinRule ,在實現中他注入了負載均衡器的統計對象 LoadBalancerStats,同時在具體的 choose 方法中利用 LoadBalancerStats 保存的實例統計信息來選擇滿足要求的實例,通過負載均衡器中維護的所有服務實例,會過濾掉故障的實例,並找出並發請求最小的一個,所以該策略的特性是可選出最空閑的實例,代碼如下:
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) {
minimalConcurrentConnections = concurrentConnections;
chosen = server;
}
}
}
if (chosen == null) {
return super.choose(key);
} else {
return chosen;
}
}