深入理解微服務(wù)中的負載均衡算法與配置策略
上一期我們詳細探討了微服務(wù)之間的通信,特別是介紹了如何集成Ribbon。簡單來說,通過使用resttemplate類進行RPC調(diào)用時,我們內(nèi)部增加了一個攔截器來實現(xiàn)負載均衡。然而,我們并未深入討論具體的負載均衡算法。因此,本章節(jié)的重點是介紹如何從多個副本中選擇合適的節(jié)點進行服務(wù)調(diào)用。這將幫助大家更好地理解在微服務(wù)架構(gòu)中如何有效地實現(xiàn)負載均衡。
好的,今天我們依舊會涉及源碼,但希望大家能把注意力集中在理念層面,而不是深究每個具體的過程調(diào)用。無需糾結(jié)于代碼的具體行數(shù),因為重要的是理解整體架構(gòu)和流程,這樣才能更好地掌握主題的實質(zhì)。
負載均衡算法
我們首先來探討一下默認情況下Ribbon使用的負載均衡算法。有些人可能會說它使用輪詢算法,因為在本地測試時,我們經(jīng)常會看到輪詢的效果。然而,簡單地依賴這種表面的觀察來回答面試題是有風險的。實際上,忽略了深入理解源代碼可能會導致嚴重的誤解。
盡管實踐是增長知識的一部分,但是在真實的生產(chǎn)環(huán)境中,尤其是跨多個數(shù)據(jù)中心部署的情況下,我們無法簡單地將問題簡化為本地集群的測試環(huán)境。
獲取服務(wù)器ip
我們接著上一篇內(nèi)容,討論如何選擇服務(wù)器的步驟如下復述:
public <T> T execute(String serviceId, LoadBalancerRequest<T> request, Object hint)
throws IOException {
ILoadBalancer loadBalancer = getLoadBalancer(serviceId);
Server server = getServer(loadBalancer, hint);
if (server == null) {
throw new IllegalStateException("No instances available for " + serviceId);
}
RibbonServer ribbonServer = new RibbonServer(serviceId, server,
isSecure(server, serviceId),
serverIntrospector(serviceId).getMetadata(server));
return execute(serviceId, ribbonServer, request);
}
獲取負載均衡器——ZoneAwareLoadBalancer
我們來看看getServer方法,突然間出現(xiàn)這么多負載均衡器,應(yīng)該怎么處理呢?這時候最好的方法就是查看自動配置,看看哪些被注入進來了。
圖片
中間步驟大家就不用再找了,我已經(jīng)事先找好了,就在這里:
圖片
這張圖包含兩個關(guān)鍵信息:首先是注入了一個IRule規(guī)則,其次是將該IRule規(guī)則應(yīng)用到了ZoneAwareLoadBalancer負載均衡器中。好的,現(xiàn)在我們清楚了接下來的步驟。接下來我們繼續(xù)查看
public Server chooseServer(Object key) {
if (!ENABLED.get() || getLoadBalancerStats().getAvailableZones().size() <= 1) {
logger.debug("Zone aware logic disabled or there is only one zone");
return super.chooseServer(key);
}
Server server = null;
try {
//省略多余代碼
Set<String> availableZones = ZoneAvoidanceRule.getAvailableZones(zoneSnapshot, triggeringLoad.get(), triggeringBlackoutPercentage.get());
logger.debug("Available zones: {}", availableZones);
if (availableZones != null && availableZones.size() < zoneSnapshot.keySet().size()) {
String zone = ZoneAvoidanceRule.randomChooseZone(zoneSnapshot, availableZones);
logger.debug("Zone chosen: {}", zone);
if (zone != null) {
BaseLoadBalancer zoneLoadBalancer = getLoadBalancer(zone);
server = zoneLoadBalancer.chooseServer(key);
}
}
} catch (Exception e) {
logger.error("Error choosing server using zone aware logic for load balancer={}", name, e);
}
//省略多余代碼
}
如果是在我們本地環(huán)境,通常會執(zhí)行第一個if分支;但如果是在生產(chǎn)環(huán)境并配置了多個區(qū)域,那么會執(zhí)行下面的分支。讓我們一起來看看。
無配置區(qū)域情況
讓我們來看看第一種情況,即如果沒有區(qū)域或者只有一個區(qū)域,負載均衡規(guī)則是如何應(yīng)用的。我們將查看父類負載均衡器BaseLoadBalancer的代碼。
public class BaseLoadBalancer extends AbstractLoadBalancer implements
PrimeConnections.PrimeConnectionListener, IClientConfigAware {
private final static IRule DEFAULT_RULE = new RoundRobinRule();
protected IRule rule = DEFAULT_RULE;
public Server chooseServer(Object key) {
if (counter == null) {
counter = createCounter();
}
counter.increment();
if (rule == null) {
return null;
} else {
try {
return rule.choose(key);
} catch (Exception e) {
logger.warn("LoadBalancer [{}]: Error choosing server for key {}", name, key, e);
return null;
}
}
}
void initWithConfig(IClientConfig clientConfig, IRule rule, IPing ping, LoadBalancerStats stats) {
// 省略部分代碼
setRule(rule);
// 省略部分代碼
}
}
這里可以看到是有默認的IRule規(guī)則的——RoundRobinRule,但是別沖動,因為我們Spring自動托管的IRule規(guī)則還沒用上,不可能這么簡單的走輪訓。我們可以看到這里是有設(shè)置的地方的。我也抓出來了。
最后讓我們再來看看我們的ZoneAwareLoadBalancer生成構(gòu)造器,因為在注入時我們是會帶入規(guī)則的。以下是相關(guān)的代碼示例:
public ZoneAwareLoadBalancer(IClientConfig clientConfig, IRule rule,
IPing ping, ServerList<T> serverList, ServerListFilter<T> filter,
ServerListUpdater serverListUpdater) {
super(clientConfig, rule, ping, serverList, filter, serverListUpdater);
}
在這里,當super父類構(gòu)造器執(zhí)行完畢后,最終會調(diào)用BaseLoadBalancer類的initWithConfig方法。我沒有一一追蹤下去,但最后ZoneAvoidanceRule的負載均衡代碼也相當復雜。不過,你可以將其理解為在沒有區(qū)域的情況下類似于輪詢。
配置多區(qū)域情況
在這個階段,程序?qū)?zhí)行第二個分支,實際上,主要的代碼如下所示:
String zone = ZoneAvoidanceRule.randomChooseZone(zoneSnapshot, availableZones);
if (zone != null) {
BaseLoadBalancer zoneLoadBalancer = getLoadBalancer(zone);
server = zoneLoadBalancer.chooseServer(key);
}
目的仍然是選擇一個服務(wù)器,但是限定在當前區(qū)域內(nèi)。關(guān)于這部分的詳細討論略去,因為接下來的方法都是關(guān)于ZoneAvoidanceRule的負載均衡算法代碼。
如何配置其他算法
在這種情況下,如果我想使用其他負載均衡算法而不是當前的算法,應(yīng)該如何配置呢?實際上,可以查看注入的源代碼,有兩種方法可以實現(xiàn)這一點。首先,可以通過在配置類中添加一個配置項來指定所需的負載均衡算法。
if (this.propertiesFactory.isSet(IRule.class, name)) {
return this.propertiesFactory.get(IRule.class, config, name);
}
局部配置
在這里可以看到我們也是通過配置文件來進行配置的,不過配置文件的方式使我們能夠進行局部微服務(wù)負載均衡的選擇。讓我們先來看一下源代碼:
public PropertiesFactory() {
classToProperty.put(ILoadBalancer.class, "NFLoadBalancerClassName");
classToProperty.put(IPing.class, "NFLoadBalancerPingClassName");
classToProperty.put(IRule.class, "NFLoadBalancerRuleClassName");
classToProperty.put(ServerList.class, "NIWSServerListClassName");
classToProperty.put(ServerListFilter.class, "NIWSServerListFilterClassName");
}
public boolean isSet(Class clazz, String name) {
return StringUtils.hasText(getClassName(clazz, name));
}
在調(diào)用特定的微服務(wù)時,可以根據(jù)需要使用相應(yīng)的負載均衡策略來配置 application.yml 文件。
#被調(diào)用的微服務(wù)名
mall‐order:
ribbon:
#指定使用Nacos提供的負載均衡策略(優(yōu)先調(diào)用同一集群的實例,基于隨機&權(quán)重)
NFLoadBalancerRuleClassName:com.alibaba.cloud.nacos.ribbon.NacosRule
全局配置
在全局情況下更為簡單,可以觀察到在自動注入時使用了 @ConditionalOnMissingBean 注解。如果我們在Spring中手動加載了相應(yīng)的bean,那么這個注解就不會生效了。
@Bean
public IRule ribbonRule() {
// 指定使用Nacos提供的負載均衡策略(優(yōu)先調(diào)用同一集群的實例,基于隨機權(quán)重)
return new NacosRule();
}
相當簡單了,那么這樣的的話,其實我們也可以進行自定義一個策略的。畢竟照先有的抄下固定實現(xiàn)方法后,自己在實現(xiàn)方法內(nèi)寫上自己的業(yè)務(wù)邏輯不就完了。
自定義策略
看起來,對于實現(xiàn)其他的負載均衡算法策略,有幾個關(guān)鍵點。首先,需要繼承 AbstractLoadBalancerRule 父類,并且實現(xiàn)其抽象方法。接下來,我們可以開始編寫我們的實現(xiàn)代碼:
@Slf4j
public class XiaoYuRandomWithWeightRule extends AbstractLoadBalancerRule {
@Override
public Server choose(Object key) {
//這里實現(xiàn)自己的邏輯即可
return server;
}
@Override
public void initWithNiwsConfig(IClientConfig clientConfig) {
}
}
OK,剩下的就按照局部配置或者全局配置下,讓我們的規(guī)則生效即可。
在這里只講述了算法規(guī)則的配置和自定義方法,實際上負載均衡器的操作也是類似的套路。這里就不重復演示了。
總結(jié)
今天,我們主要補充了上一章關(guān)于微服務(wù)通信的內(nèi)容,并深入探討了負載均衡算法的重要性。我們首先詳細討論了Ribbon默認使用的負載均衡算法。盡管在本地測試時可能會觀察到輪詢的效果,但簡單依賴這種表面的觀察是不夠的。在真實的生產(chǎn)環(huán)境中,特別是在跨多個數(shù)據(jù)中心部署時,負載均衡策略的選擇需要更加深入的理解和分析。
我們進一步分析了如何通過配置和自定義負載均衡規(guī)則來靈活應(yīng)對各種場景。不論是局部配置還是全局配置,我們都能根據(jù)具體需求調(diào)整負載均衡的行為。同時,我們展示了如何通過自定義算法擴展Ribbon的負載均衡能力,以更好地適應(yīng)特定業(yè)務(wù)場景的需求。