質量、速度、廉價,只能選擇其中兩個。
–> 返回專欄總目錄 <–
代碼下載地址:https://github.com/f641385712/netflix-learning
目錄
前言
隨着微服務、雲源生的流行,多雲、多區域(zone)、跨機房部署的case越來越多。Ribbon作爲微服務領域的優秀組件,自然也提供了對多區域支持的負載均衡能力。
作爲基礎,本文將介紹多zone負載均衡中最爲重要的一個方法:ZoneAvoidanceRule.getAvailableZones()
,它解決了根據LoadBalancerStats
狀態信息仲裁出可用區出來。
正文
關於getAvailableZones
方法,其實有兩處地方都叫這個名字,但是它們的功能是不一樣的,且存在依賴的關係,爲了避免讀者迷糊,現分別進行闡述。
LoadBalancerStats#getAvailableZones實例方法
它是LoadBalancerStats
裏的一個實例方法:
LoadBalancerStats:
volatile Map<String, List<? extends Server>> upServerListZoneMap = new ConcurrentHashMap<>();
public Set<String> getAvailableZones() {
return upServerListZoneMap.keySet();
}
若有下面邏輯的存在,其實我覺得該方法的命令是頗具歧義的,或許叫getAllAvailableZones()
會更合適一些。因爲它僅是一個普通的獲取方法,並不考慮對應zone內Server的負載情況、可用情況,這些都交給下面這個工具方法進行完成。
ZoneAvoidanceRule靜態工具方法
首先我吐槽一下:作爲static工具方法,爲毛放在ZoneAvoidanceRule
裏呢?統一放在LoadBalancerStats
裏內聚起來不香嗎?
randomChooseZone()
在開始之前,我們先了解下這個選擇支持方法,但是它是非public。調用方僅有兩處:
- 下面的getAvailableZones()方法
ZoneAwareLoadBalancer#chooseServer()
方法(後文重點闡述,非常重要)
ZoneAvoidanceRule:
static String randomChooseZone(
Map<String, ZoneSnapshot> snapshot,
Set<String> chooseFrom) {
if (chooseFrom == null || chooseFrom.size() == 0) {
return null;
}
// 注意:默認選擇的是第一個zone區域
// 若總共就1個區域,那就是它了。若有多個,那就需要隨機去選
String selectedZone = chooseFrom.iterator().next();
if (chooseFrom.size() == 1) {
return chooseFrom.iterator().next();
}
// 所有的區域中總的Server實例數
int totalServerCount = 0;
for (String zone : chooseFrom) {
totalServerCount += snapshot.get(zone).getInstanceCount();
}
// 從所有的實例總數中隨機選個數字。比如總數是10臺機器
// 那就是從[1-10]之間隨機選個數字,比如選中爲6
int index = random.nextInt(totalServerCount) + 1;
// sum代表當前實例統計的總數
// 它的邏輯是:當sum超過這個index時,就以這個區域爲準
int sum = 0;
for (String zone : chooseFrom) {
sum += snapshot.get(zone).getInstanceCount();
if (index <= sum) {
selectedZone = zone;
break;
}
}
}
這個隨機算法最核心的就是最後面的index
和sum
算法,看完後你應該有如下疑問:爲何不來個從chooseFrom
這個集合裏隨機彈出一個zone就成,而非弄的這麼麻煩呢?
其實這麼做的是很有意義的,這麼做能保證:zone裏面機器數越多的話,被選中的概率是越大的,這樣隨機纔是最合理的。
getAvailableZones()
該方法是一個靜態工具方法,顧名思義它用於獲取真實的可用區,它在LoadBalancerStats#getAvailableZones
方法的基礎上,結合每個zone對應的ZoneSnapshot
的情況再結合閾值設置,篩選真正可用的zone區域。
ZoneAvoidanceRule:
// snapshot:zone對應的ZoneSnapshot的一個map
// triggeringLoad:
// triggeringBlackoutPercentage:
public static Set<String> getAvailableZones(
Map<String, ZoneSnapshot> snapshot,
double triggeringLoad,
double triggeringBlackoutPercentage) {
// 爲毛一個都木有不返回空集合???有點亂啊。。。。不過沒關係
if (snapshot.isEmpty()) {
return null;
}
//最終需要return的可用區,中途會進行排除的邏輯
Set<String> availableZones = new HashSet<>(snapshot.keySet());
// 如果有且僅有一個zone可用,再糟糕也得用,不用進行其他邏輯了
if (availableZones.size() == 1) {
return availableZones;
}
// 記錄很糟糕
Set<String> worstZones = new HashSet<>();
// 所有zone中,平均負載最高值
double maxLoadPerServer = 0;
// true:zone有限可用
// false:zone全部可用
boolean limitedZoneAvailability = false;
// 對每個zone的情況逐一分析
for (Map.Entry<String, ZoneSnapshot> zoneEntry : snapshot.entrySet()) {
String zone = zoneEntry.getKey();
ZoneSnapshot zoneSnapshot = zoneEntry.getValue();
int instanceCount = zoneSnapshot.getInstanceCount();
// 若該zone內一個實例都木有了,那就是完全不可用,那就移除該zone
// 然後標記zone是有限可用的(並非全部可用嘍)
if (instanceCount == 0) {
availableZones.remove(zone);
limitedZoneAvailability = true;
} else {
// 該zone的平均負載
double loadPerServer = zoneSnapshot.getLoadPerServer();
// 機器的熔斷總數 / 總實例數已經超過了閾值(默認爲1,也就是全部熔斷纔會認爲該zone完全不可用)
// 或者 loadPerServer < 0 (啥時候小於0???下面說)
if (((double) zoneSnapshot.getCircuitTrippedCount()) / instanceCount >= triggeringBlackoutPercentage
|| loadPerServer < 0) {
// 證明這個zone完全不可用,就移除掉
availableZones.remove(zone);
limitedZoneAvailability = true;
} else { // 並不是完全不可用,就看看狀態是不是很糟糕
// 若當前負載和最大負載相當,那認爲已經很糟糕了
if (Math.abs(loadPerServer - maxLoadPerServer) < 0.000001d) {
worstZones.add(zone);
// 或者若當前負載大於最大負載了
} else if (loadPerServer > maxLoadPerServer) {
maxLoadPerServer = loadPerServer;
worstZones.clear();
worstZones.add(zone);
}
}
}
}
// 若最大負載小於設定的負載閾值 並且limitedZoneAvailability=false
// 就是說全部zone都可用,並且最大負載都還沒有達到閾值,那就把全部zone返回
if (maxLoadPerServer < triggeringLoad && !limitedZoneAvailability) {
// zone override is not needed here
return availableZones;
}
String zoneToAvoid = randomChooseZone(snapshot, worstZones);
if (zoneToAvoid != null) {
availableZones.remove(zoneToAvoid);
}
return availableZones;
}
這個選擇可用區的步驟還是比較重要的,畢竟現在多區域部署、多雲部署都比價常見,現在對它的處理過程做如下文字總結:
- 若zone爲null,返回null。若只有一個zone,就返回當前zone,不用再繼續判斷。否則默認返回所有zone:
availableZones
。接下來會一步步做remove()移除動作 - 使用變量
Set<String> worstZones
記錄所有zone中比較糟糕的zone們;用maxLoadPerServer
表示所有zone中負載最高的區域;用limitedZoneAvailability
表示是否是部分zone可用(true:部分可用,false:全部可用) - 遍歷所有的zone,根據其對應的快照
ZoneSnapshot
來判斷負載情況 - 若當前zone的
instanceCount
也就是實例總數是0,那就remove(當前zone),並且標記limitedZoneAvailability=true
(因爲移除了一個,就不是全部了嘛)。若當前zone的實例數>0,那就繼續 - 拿到當前總的平均負載
loadPerServer
,如果zone內的熔斷實例數 / 總實例數 >= triggeringBlackoutPercentage閾值
或者loadPerServer < 0
的話,那就執行remove(當前zone),並且limitedZoneAvailability=true
熔斷實例數 / 總實例數 >= 閾值
標記爲當前zone就不可用了(移除掉),這個很好理解。這個閾值爲0.99999d
也就說所有的Server實例被熔斷了,該zone纔算不可用了loadPerServer < 0
是什麼鬼?那麼什麼時候loadPerServer會是負數呢?它在LoadBalancerStats#getZoneSnapshot()
方法裏:if (circuitBreakerTrippedCount == instanceCount)
的時候,loadPerServer = -1
,也就說當所有實例都熔斷了,那麼loadPerServer
也無意義了嘛,所以賦值爲-1。- 總的來說1和2觸達條件差不多,只是1的閾值是可以配置的,比如你配置爲0.9那就是隻有當90%機器都熔斷了就認爲該zone不可用了,而不用100%(請原諒我把
0.99999d
當1來看待)
- 經過以上步驟,說明所有的zone是基本可用的,但可能有些負載高有些負載低,因此接下來需要判斷區域負載情況,就是如下這段代碼。這段代碼的總體意思是:從所有zone中找出負載最高的區域們(若負載差在
0.000001d
只能被認爲是相同負載,都認爲是負載最高的們)。- 說明:
worstZones
裏面裝載着負載最高的zone們,也就是top1(當然可能多個並列第一的情況)
- 說明:
if (Math.abs(loadPerServer - maxLoadPerServer) < 0.000001d) {
// they are the same considering double calculation
// round error
worstZones.add(zone);
} else if (loadPerServer > maxLoadPerServer) {
maxLoadPerServer = loadPerServer;
worstZones.clear();
worstZones.add(zone);
}
- 分析好數據後,最後準備返回結果。若統計完所有的區域後,最高負載
maxLoadPerServer
仍舊小於提供的triggeringLoad閾值
,並且並且limitedZoneAvailability=false
(就是說所有zone都可用的情況下),那就返回所有的zone吧:availableZones
。- 這個很好理解:所有的兄弟們負載都很低,並且一個哥們都沒“死”,那就都返回出去唄
triggeringLoad
閾值的默認值是0.2,負載的計算方式是:loadPerServer = 整個zone的活躍請求總數 / 整個zone內可用實例總數
。- 注意:一定是活躍連接數。也就是說正在處理中的鏈接數纔算做服務壓力嘛
- 若最大負載超過閾值(或者死了一個/N個兄弟),那麼就不能返回全部拉。那就從負載最高的兄弟們中(因爲可能多個,可能1個,大概率是隻有1個值的)隨機選擇一個出來:
randomChooseZone(snapshot, worstZones)
,然後執行移除remove(zoneToAvoid)
掉,這麼處理的目的是把負載最高的那個哥們T除掉,再返回結果。- 說明:這裏使用的隨機算法就是上面所講述的(誰的zone裏面實例數最多,就越可能被選中)
總而言之:選擇可用區的原則是T除掉不可用的、T掉負載最高的區域,其它區域返回結果,這樣處理後返回的結果纔是健康程度綜合最好的。
另外,該方法還有個重載的,便捷使用方法:
ZoneAvoidanceRule:
// 實際調用仍舊爲getAvailableZones方法~
// 它友好的只需要傳參LoadBalancerStats即可,內部幫你構建snapshot這個Map
public static Set<String> getAvailableZones(LoadBalancerStats lbStats,
double triggeringLoad, double triggeringBlackoutPercentage) {
if (lbStats == null) {
return null;
}
Map<String, ZoneSnapshot> snapshot = createSnapshot(lbStats);
return getAvailableZones(snapshot, triggeringLoad,
triggeringBlackoutPercentage);
}
該方法使用處
getAvailableZones()
方法的調用處主要有兩個地方:
ZoneAvoidancePredicate#apply()
:用於過濾掉哪些超過閾值的、不可用的zone區域們ZoneAwareLoadBalancer#chooseServer()
:通過此方法拿到可用區域們availableZones
,然後再通過randomChooseZone()
方法從中隨機選取一個出來,再從zone裏選擇一臺Server就是最佳的Server
不合理的默認值
可以先看下面代碼示例。計算可用區的兩個閾值是:
triggeringLoad
:平均負載閾值。該閾值可配置ZoneAvoidancePredicate
:默認值均爲0.2d- 默認key:
ZoneAwareNIWSDiscoveryLoadBalancer.triggeringLoadPerServerThreshold
- 個性化key:
"ZoneAwareNIWSDiscoveryLoadBalancer." + clientConfig.getClientName() + ".triggeringLoadPerServerThreshold"
- 默認key:
ZoneAwareLoadBalancer
:默認值亦爲0.2- 配置同上
triggeringBlackoutPercentage
:觸發“熄滅”的百分比閾值(簡單的說當你的實例掛了%多少時,就移除掉此區域)。
triggeringBlackoutPercentage
這個閾值尚且合理(默認所有實例掛了纔會移除這個zone),但是triggeringLoad
這個閾值僅設置爲0.2,what a fuck???也就說一個zone裏面有10臺機器的話,超過2個請求打進來就算負載過重,從而最終結果會移除掉一個負載最高的可用區,這麼設定腦子不是怕陪驢砸了吧?
這麼配置有何後果?
0.2的閾值等於所有zone都處於過載狀態,因此選擇可用區的時候永遠會T除掉一個(當然你只有一個可用區除外),假如你總共只有2個可用區,這將使得負載均衡策略完全失效~~~~
說明:我強烈懷疑老外是想表達負載超過20%了就算負載過重了,只是它沒考慮到
ZoneSnapshot.loadPerServer
它並不是一個百分比值~~~
在實際生產中:我個人強烈建議你增加默認配置ZoneAwareNIWSDiscoveryLoadBalancer.triggeringLoadPerServerThreshold = 100
。表示單臺機器超過100個併發後認爲負載過高了(當然100這個數值你可以根據機器配置具體設定,此處僅供參考),這樣能極大的提高zone之間的負載均衡能力。
說明:這一切都建立在你的應用部署在多zone的情況下,若你僅有一個zone,那麼請忽略本文內容~
代碼示例
// 單獨線程模擬刷頁面,獲取監控到的數據
private void monitor(LoadBalancerStats lbs) {
List<String> zones = Arrays.asList("華南", "華東", "華北");
new Thread(() -> {
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);
executorService.scheduleWithFixedDelay(() -> {
// 打印當前可用區
// 獲取可用區
Set<String> availableZones = ZoneAvoidanceRule.getAvailableZones(lbs, 0.2d, 0.99999d);
System.out.println("=====當前可用區爲:" + availableZones);
zones.forEach(zone -> {
System.out.printf("區域[" + zone + "]概要:");
int instanceCount = lbs.getInstanceCount(zone);
int activeRequestsCount = lbs.getActiveRequestsCount(zone);
double activeRequestsPerServer = lbs.getActiveRequestsPerServer(zone);
// ZoneSnapshot zoneSnapshot = lbs.getZoneSnapshot(zone);
System.out.printf("實例總數:%s,活躍請求總數:%s,平均負載:%s\n", instanceCount, activeRequestsCount, activeRequestsPerServer);
// System.out.println(zoneSnapshot);
});
System.out.println("======================================================");
}, 5, 5, TimeUnit.SECONDS);
}).start();
}
// 請注意:請必須保證Server的id不一樣,否則放不進去List的(因爲Server的equals hashCode方法僅和id有關)
// 所以此處使用index作爲port,以示區分
private Server createServer(String zone, int index) {
Server server = new Server("www.baidu" + zone + ".com", index);
server.setZone(zone);
return server;
}
// 多線程,模擬請求
private void request(ServerStats serverStats) {
new Thread(() -> {
// 每10ms發送一個請求(每個請求處理10-200ms的時間),持續不斷
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);
executorService.scheduleWithFixedDelay(() -> {
new Thread(() -> {
// 請求之前 記錄活躍請求數
serverStats.incrementActiveRequestsCount();
serverStats.incrementNumRequests();
long rt = doSomething();
// 請求結束, 記錄響應耗時
serverStats.noteResponseTime(rt);
serverStats.decrementActiveRequestsCount();
}).start();
}, 10, 10, TimeUnit.MILLISECONDS);
}).start();
}
// 模擬請求耗時,返回耗時時間
private long doSomething() {
try {
int rt = randomValue(10, 200);
TimeUnit.MILLISECONDS.sleep(rt);
return rt;
} catch (InterruptedException e) {
e.printStackTrace();
return 0L;
}
}
// 本地使用隨機數模擬數據收集
private int randomValue(int min, int max) {
return min + (int) (Math.random() * ((max - min) + 1));
}
// ============單元測試
@Test
public void fun6() throws InterruptedException {
LoadBalancerStats lbs = new LoadBalancerStats("YoutBatman");
// 添加Server
List<Server> serverList = new ArrayList<>();
serverList.add(createServer("華南", 1));
serverList.add(createServer("華東", 1));
serverList.add(createServer("華東", 2));
serverList.add(createServer("華北", 1));
serverList.add(createServer("華北", 2));
serverList.add(createServer("華北", 3));
serverList.add(createServer("華北", 4));
lbs.updateServerList(serverList);
Map<String, List<Server>> zoneServerMap = new HashMap<>();
// 模擬向每個Server發送請求 記錄ServerStatus數據
serverList.forEach(server -> {
ServerStats serverStat = lbs.getSingleServerStat(server);
request(serverStat);
// 順便按照zone分組
String zone = server.getZone();
if (zoneServerMap.containsKey(zone)) {
zoneServerMap.get(zone).add(server);
} else {
List<Server> servers = new ArrayList<>();
servers.add(server);
zoneServerMap.put(zone, servers);
}
});
lbs.updateZoneServerMapping(zoneServerMap);
// 從lbs裏拿到一些監控數據
monitor(lbs);
TimeUnit.SECONDS.sleep(500);
}
運行程序,打印:
=====當前可用區爲:[華南, 華東]
區域[華南]概要:實例總數:1,活躍請求總數:10,平均負載:10.0
區域[華東]概要:實例總數:2,活躍請求總數:18,平均負載:9.0
區域[華北]概要:實例總數:4,活躍請求總數:41,平均負載:10.25
======================================================
=====當前可用區爲:[華南, 華北]
區域[華南]概要:實例總數:1,活躍請求總數:9,平均負載:9.0
區域[華東]概要:實例總數:2,活躍請求總數:22,平均負載:11.0
區域[華北]概要:實例總數:4,活躍請求總數:34,平均負載:8.5
======================================================
=====當前可用區爲:[華南, 華東]
區域[華南]概要:實例總數:1,活躍請求總數:9,平均負載:9.0
區域[華東]概要:實例總數:2,活躍請求總數:18,平均負載:9.0
區域[華北]概要:實例總數:4,活躍請求總數:37,平均負載:9.25
======================================================
=====當前可用區爲:[華北, 華東]
區域[華南]概要:實例總數:1,活躍請求總數:10,平均負載:10.0
區域[華東]概要:實例總數:2,活躍請求總數:17,平均負載:8.5
區域[華北]概要:實例總數:4,活躍請求總數:39,平均負載:9.75
======================================================
...
從中可以明顯的看出:每次會把負載最高的Zone給T除掉(請認真觀察輸出的數據來發現規律),這是完全符合預期的。
說明:因爲平均負載均超過閾值0.2,所以會從所有zone中排除掉一個負載最高的zone~
總結
關於Ribbon可用區選擇邏輯就先介紹這,這裏有必要再次強調:雖然它爲static靜態方法,但是它是可用區過濾邏輯、可用區選擇的核心邏輯,這對後面的具有區域意識的LoadBalancer
的理解具有核心要意。
這部分邏輯理解起來稍顯費力,建議多讀幾遍,並且結合自己腦補的場景便可完成,當然嘍,若有不知道的概念,請參閱前面相關文章,畢竟學習就像砌磚,跳不過去的。
聲明
原創不易,碼字不易,多謝你的點贊、收藏、關注。把本文分享到你的朋友圈是被允許的,但拒絕抄襲
。你也可【左邊掃碼/或加wx:fsx641385712】邀請你加入我的 Java高工、架構師 系列羣大家庭學習和交流。
- [享學Netflix] 一、Apache Commons Configuration:你身邊的配置管理專家
- [享學Netflix] 二、Apache Commons Configuration事件監聽機制及使用ReloadingStrategy實現熱更新
- [享學Netflix] 三、Apache Commons Configuration2.x全新的事件-監聽機制
- [享學Netflix] 四、Apache Commons Configuration2.x文件定位系統FileLocator和FileHandler
- [享學Netflix] 五、Apache Commons Configuration2.x別樣的Builder模式:ConfigurationBuilder
- [享學Netflix] 六、Apache Commons Configuration2.x快速構建工具Parameters和Configurations
- [享學Netflix] 七、Apache Commons Configuration2.x如何實現文件熱加載/熱更新?
- [享學Netflix] 八、Apache Commons Configuration2.x相較於1.x使用上帶來哪些差異?
- [享學Netflix] 九、Archaius配置管理庫:初體驗及基礎API詳解
- [享學Netflix] 十、Archaius對Commons Configuration核心API Configuration的擴展實現
- [享學Netflix] 十一、Archaius配置管理器ConfigurationManager和動態屬性支持DynamicPropertySupport
- [享學Netflix] 十二、Archaius動態屬性DynamicProperty原理詳解(重要)
- [享學Netflix] 十三、Archaius屬性抽象Property和PropertyWrapper詳解
- [享學Netflix] 十四、Archaius如何對多環境、多區域、多雲部署提供配置支持?
- [享學Netflix] 十五、Archaius和Spring Cloud的集成:spring-cloud-starter-netflix-archaius
- [享學Netflix] 十六、Hystrix斷路器:初體驗及RxJava簡介
- [享學Netflix] 十七、Hystrix屬性抽象以及和Archaius整合實現配置外部化、動態化
- [享學Netflix] 十八、Hystrix配置之:全局配置和實例配置
- [享學Netflix] 十九、Hystrix插件機制:SPI接口介紹和HystrixPlugins詳解
- [享學Netflix] 二十、Hystrix跨線程傳遞數據解決方案:HystrixRequestContext
- [享學Netflix] 二十一、Hystrix指標數據收集(預熱):滑動窗口算法(附代碼示例)
- [享學Netflix] 二十二、Hystrix事件源與事件流:HystrixEvent和HystrixEventStream
- [享學Netflix] 二十三、Hystrix桶計數器:BucketedCounterStream
- [享學Netflix] 二十四、Hystrix在滑動窗口內統計:BucketedRollingCounterStream、HealthCountsStream
- [享學Netflix] 二十五、Hystrix累計統計流、分發流、最大併發流、配置流、功能流(附代碼示例)
- [享學Netflix] 二十六、Hystrix指標數據收集器:HystrixMetrics(HystrixDashboard的數據來源)
- [享學Netflix] 二十七、Hystrix何爲斷路器的半開狀態?HystrixCircuitBreaker詳解
- [享學Netflix] 二十八、Hystrix事件計數器EventCounts和執行結果ExecutionResult
- [享學Netflix] 二十九、Hystrix執行過程核心接口:HystrixExecutable、HystrixObservable和HystrixInvokableInfo
- [享學Netflix] 三十、Hystrix的fallback回退/降級邏輯源碼解讀:getFallbackOrThrowException
- [享學Netflix] 三十一、Hystrix觸發fallback降級邏輯的5種情況及代碼示例
- [享學Netflix] 三十二、Hystrix拋出HystrixBadRequestException異常爲何不會觸發熔斷?
- [享學Netflix] 三十三、Hystrix執行目標方法時,如何調用線程池資源?
- [享學Netflix] 三十四、Hystrix目標方法執行邏輯源碼解讀:executeCommandAndObserve
- [享學Netflix] 三十五、Hystrix執行過程集大成者:AbstractCommand詳解
- [享學Netflix] 三十六、Hystrix請求命令:HystrixCommand和HystrixObservableCommand
- [享學Netflix] 三十七、源生Ribbon介紹 — 客戶端負載均衡器
- [享學Netflix] 三十八、Ribbon核心API源碼解析:ribbon-core(一)IClient請求客戶端
- [享學Netflix] 三十九、Ribbon核心API源碼解析:ribbon-core(二)IClientConfig配置詳解
- [享學Netflix] 四十、Ribbon核心API源碼解析:ribbon-core(三)RetryHandler重試處理器
- [享學Netflix] 四十一、Ribbon核心API源碼解析:ribbon-core(四)ClientException客戶端異常
- [享學Netflix] 四十二、Ribbon的LoadBalancer五大組件之:IPing心跳檢測
- [享學Netflix] 四十三、Ribbon的LoadBalancer五大組件之:ServerList服務列表
- [享學Netflix] 四十四、netflix-statistics詳解,手把手教你寫個超簡版監控系統
- [享學Netflix] 四十五、Ribbon服務器狀態:ServerStats及其斷路器原理
- [享學Netflix] 四十六、Ribbon負載均衡策略服務器狀態總控:LoadBalancerStats