您好,登錄后才能下訂單哦!
下文給大家帶來實現負載均衡的主要兩種方式,希望能夠給大家在實際運用中帶來一定的幫助,負載均衡涉及的東西比較多,理論也不多,網上有很多書籍,今天我們就用億速云在行業內累計的經驗來做一個解答。
我們都知道在微服務架構中,微服務之間總是需要互相調用,以此來實現一些組合業務的需求。例如組裝訂單詳情數據,由于訂單詳情里有用戶信息,所以訂單服務就得調用用戶服務來獲取用戶信息。要實現遠程調用就需要發送網絡請求,而每個微服務都可能會存在有多個實例分布在不同的機器上,那么當一個微服務調用另一個微服務的時候就需要將請求均勻的分發到各個實例上,以此避免某些實例負載過高,某些實例又太空閑,所以在這種場景必須要有負載均衡器。
目前實現負載均衡主要的兩種方式:
1、服務端負載均衡;例如最經典的使用Nginx做負載均衡器。用戶的請求先發送到Nginx,然后再由Nginx通過配置好的負載均衡算法將請求分發到各個實例上,由于需要作為一個服務部署在服務端,所以該種方式稱為服務端負載均衡。如圖:
2、客戶端側負載均衡;之所以稱為客戶端側負載均衡,是因為這種負載均衡方式是由發送請求的客戶端來實現的,也是目前微服務架構中用于均衡服務之間調用請求的常用負載均衡方式。因為采用這種方式的話服務之間可以直接進行調用,無需再通過一個專門的負載均衡器,這樣能夠提高一定的性能以及高可用性。以微服務A調用微服務B舉例,簡單來說就是微服務A先通過服務發現組件獲取微服務B所有實例的調用地址,然后通過本地實現的負載均衡算法選取出其中一個調用地址進行請求。如圖:
我們來通過Spring Cloud提供的DiscoveryClient寫一個非常簡單的客戶端側負載均衡器,借此直觀的了解一下該種負載均衡器的工作流程,該示例中采用的負載均衡策略為隨機,代碼如下:
package com.zj.node.contentcenter.discovery;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.Collectors;
/**
* 客戶端側負載均衡器
*
* @author 01
* @date 2019-07-26
**/
public class LoadBalance {
@Autowired
private DiscoveryClient discoveryClient;
/**
* 隨機獲取目標微服務的請求地址
*
* @return 請求地址
*/
public String randomTakeUri(String serviceId) {
// 獲取目標微服務的所有實例的請求地址
List<String> targetUris = discoveryClient.getInstances(serviceId).stream()
.map(i -> i.getUri().toString())
.collect(Collectors.toList());
// 隨機獲取列表中的uri
int i = ThreadLocalRandom.current().nextInt(targetUris.size());
return targetUris.get(i);
}
}
什么是Ribbon:
Ribbon雖然是個主要用于負載均衡的小組件,但是麻雀雖小五臟俱全,Ribbon還是有許多的接口組件的。如下表:
Ribbon默認內置了八種負載均衡策略,若想自定義負載均衡策略則實現上表中提到的IRule接口或AbstractLoadBalancerRule抽象類即可。內置的負載均衡策略如下:
Ribbon主要有兩種使用方式,一是使用Feign,Feign內部已經整合了Ribbon,因此如果只是普通使用的話都感知不到Ribbon的存在;二是配合RestTemplate使用,這種方式則需要添加Ribbon依賴和@LoadBalanced注解。
這里主要演示一下第二種使用方式,由于項目中添加的Nacos依賴已包含了Ribbon所以不需要另外添加依賴,首先定義一個RestTemplate,代碼如下:
package com.zj.node.contentcenter.configuration;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
/**
* bean 配置類
*
* @author 01
* @date 2019-07-25
**/
@Configuration
public class BeanConfig {
@Bean
@LoadBalanced // 加上這個注解表示使用Ribbon
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
然后使用RestTemplate調用其他服務的時候,只需要寫服務名即可,不需要再寫ip地址和端口號。如下示例:
public ShareDTO findById(Integer id) {
// 獲取分享詳情
Share share = shareMapper.selectByPrimaryKey(id);
// 發布人id
Integer userId = share.getUserId();
// 調用用戶中心獲取用戶信息
UserDTO userDTO = restTemplate.getForObject(
"http://user-center/users/{id}", // 只需要寫服務名
UserDTO.class, userId
);
ShareDTO shareDTO = objectConvert.toShareDTO(share);
shareDTO.setWxNickname(userDTO.getWxNickname());
return shareDTO;
}
如果不太清楚RestTemplate的使用,可以參考如下文章:
在實際開發中,我們可能會遇到默認的負載均衡策略無法滿足需求,從而需要更換其他的負載均衡策略。關于Ribbon負載均衡的配置方式主要有兩種,在代碼中配置或在配置文件中配置。
Ribbon支持細粒度的配置,例如我希望微服務A在調用微服務B的時候采用隨機的負載均衡策略,而在調用微服務C的時候采用默認策略,下面我們就來實現一下這種細粒度的配置。
1、首先是通過代碼進行配置,編寫一個配置類用于實例化指定的負載均衡策略對象:
@Configuration
public class RibbonConfig {
@Bean
public IRule ribbonRule(){
// 隨機的負載均衡策略對象
return new RandomRule();
}
}
然后再編寫一個用于配置Ribbon客戶端的配置類,該配置類的目的是指定在調用user-center時采用RibbonConfig里配置的負載均衡策略,這樣就可以達到細粒度配置的效果:
@Configuration
// 該注解用于自定義Ribbon客戶端配置,這里聲明為屬于user-center的配置
@RibbonClient(name = "user-center", configuration = RibbonConfig.class)
public class UserCenterRibbonConfig {
}
需要注意的是RibbonConfig應該定義在主啟動類之外,避免被Spring掃描到,不然會產生父子上下文掃描重疊的問題,從而導致各種奇葩的問題。而在Ribbon這里就會導致該配置類被所有的Ribbon客戶端共享,即不管調用user-center還是其他微服務都會采用該配置類里定義的負載均衡策略,這樣就會變成了一個全局配置了,違背了我們需要細粒度配置的目的。所以需要將其定義在主啟動類之外:
關于這個問題可以參考官方文檔的描述:
https://cloud.spring.io/spring-cloud-static/Greenwich.SR2/single/spring-cloud.html#_customizing_the_ribbon_client
2、使用配置文件進行配置就更簡單了,不需要寫代碼還不會有父子上下文掃描重疊的坑,只需在配置文件中增加如下一段配置就可以實現以上使用代碼配置等價的效果:
user-center:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
兩種配置方式對比:
最佳實踐總結:
以上介紹的是細粒度地針對某個特定Ribbon客戶端的配置,下面我們再演示一下如何實現全局配置。很簡單,只需要把注解改為@RibbonClients即可,代碼如下:
@Configuration
// 該注解用于全局配置
@RibbonClients(defaultConfiguration = RibbonConfig.class)
public class GlobalRibbonConfig {
}
Ribbon默認是懶加載的,所以在第一次發生請求的時候會顯得比較慢,我們可以通過在配置文件中添加如下配置開啟饑餓加載:
ribbon:
eager-load:
enabled: true
# 為哪些客戶端開啟饑餓加載,多個客戶端使用逗號分隔(非必須)
clients: user-center
以上小節基本介紹完了負載均衡及Ribbon的基礎使用,接下來的內容需要配合Nacos,若沒有了解過Nacos的話可以參考以下文章:
在Nacos Server的控制臺頁面可以編輯每個微服務實例的權重,服務列表 -> 詳情 -> 編輯;默認權重都為1,權重值越大就越優先被調用:
權重在很多場景下非常有用,例如一個微服務有很多的實例,它們被部署在不同配置的機器上,這時候就可以將配置較差的機器上所部署的實例權重設置得比較低,而部署在配置較好的機器上的實例權重設置得高一些,這樣就可以將較大一部分的請求都分發到性能較高的機器上。
但是Ribbon內置的負載均衡策略都不支持Nacos的權重,所以我們就需要自定義實現一個支持Nacos權重配置的負載均衡策略。好在Nacos Client已經內置了負載均衡的能力,所以實現起來也比較簡單,代碼如下:
package com.zj.node.contentcenter.configuration;
import com.alibaba.nacos.api.exception.NacosException;
import com.alibaba.nacos.api.naming.NamingService;
import com.alibaba.nacos.api.naming.pojo.Instance;
import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.AbstractLoadBalancerRule;
import com.netflix.loadbalancer.BaseLoadBalancer;
import com.netflix.loadbalancer.Server;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.alibaba.nacos.NacosDiscoveryProperties;
import org.springframework.cloud.alibaba.nacos.ribbon.NacosServer;
/**
* 支持Nacos權重配置的負載均衡策略
*
* @author 01
* @date 2019-07-27
**/
@Slf4j
public class NacosWeightedRule extends AbstractLoadBalancerRule {
@Autowired
private NacosDiscoveryProperties discoveryProperties;
/**
* 讀取配置文件,并初始化NacosWeightedRule
*
* @param iClientConfig iClientConfig
*/
@Override
public void initWithNiwsConfig(IClientConfig iClientConfig) {
// do nothing
}
@Override
public Server choose(Object key) {
BaseLoadBalancer loadBalancer = (BaseLoadBalancer) this.getLoadBalancer();
log.debug("lb = {}", loadBalancer);
// 需要請求的微服務名稱
String name = loadBalancer.getName();
// 獲取服務發現的相關API
NamingService namingService = discoveryProperties.namingServiceInstance();
try {
// 調用該方法時nacos client會自動通過基于權重的負載均衡算法選取一個實例
Instance instance = namingService.selectOneHealthyInstance(name);
log.info("選擇的實例是:instance = {}", instance);
return new NacosServer(instance);
} catch (NacosException e) {
return null;
}
}
}
然后在配置文件中配置一下就可以使用該負載均衡策略了:
user-center:
ribbon:
NFLoadBalancerRuleClassName: com.zj.node.contentcenter.configuration.NacosWeightedRule
思考:既然Nacos Client已經有負載均衡的能力,Spring Cloud Alibaba為什么還要去整合Ribbon呢?
個人認為,這主要是為了符合Spring Cloud標準。Spring Cloud Commons有個子項目 spring-cloud-loadbalancer ,該項目制定了標準,用來適配各種客戶端負載均衡器(雖然目前實現只有Ribbon,但Hoxton就會有替代的實現了)。
Spring Cloud Alibaba遵循了這一標準,所以整合了Ribbon,而沒有去使用Nacos Client提供的負載均衡能力。
在Spring Cloud Alibaba之服務發現組件 - Nacos一文中已經介紹過集群的概念以及作用,這里就不再贅述,加上上一小節中已經介紹過如何自定義負載均衡策略了,所以這里不再啰嗦而是直接上代碼,實現代碼如下:
package com.zj.node.contentcenter.configuration;
import com.alibaba.nacos.api.exception.NacosException;
import com.alibaba.nacos.api.naming.NamingService;
import com.alibaba.nacos.api.naming.pojo.Instance;
import com.alibaba.nacos.client.naming.core.Balancer;
import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.AbstractLoadBalancerRule;
import com.netflix.loadbalancer.BaseLoadBalancer;
import com.netflix.loadbalancer.Server;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.alibaba.nacos.NacosDiscoveryProperties;
import org.springframework.cloud.alibaba.nacos.ribbon.NacosServer;
import org.springframework.util.CollectionUtils;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* 實現同一集群優先調用并基于隨機權重的負載均衡策略
*
* @author 01
* @date 2019-07-27
**/
@Slf4j
public class NacosSameClusterWeightedRule extends AbstractLoadBalancerRule {
@Autowired
private NacosDiscoveryProperties discoveryProperties;
@Override
public void initWithNiwsConfig(IClientConfig iClientConfig) {
// do nothing
}
@Override
public Server choose(Object key) {
// 獲取配置文件中所配置的集群名稱
String clusterName = discoveryProperties.getClusterName();
BaseLoadBalancer loadBalancer = (BaseLoadBalancer) this.getLoadBalancer();
// 獲取需要請求的微服務名稱
String serviceId = loadBalancer.getName();
// 獲取服務發現的相關API
NamingService namingService = discoveryProperties.namingServiceInstance();
try {
// 獲取該微服務的所有健康實例
List<Instance> instances = namingService.selectInstances(serviceId, true);
// 過濾出相同集群下的所有實例
List<Instance> sameClusterInstances = instances.stream()
.filter(i -> Objects.equals(i.getClusterName(), clusterName))
.collect(Collectors.toList());
// 相同集群下沒有實例則需要使用其他集群下的實例
List<Instance> instancesToBeChosen;
if (CollectionUtils.isEmpty(sameClusterInstances)) {
instancesToBeChosen = instances;
log.warn("發生跨集群調用,name = {}, clusterName = {}, instances = {}",
serviceId, clusterName, instances);
} else {
instancesToBeChosen = sameClusterInstances;
}
// 基于隨機權重的負載均衡算法,從實例列表中選取一個實例
Instance instance = ExtendBalancer.getHost(instancesToBeChosen);
log.info("選擇的實例是:port = {}, instance = {}", instance.getPort(), instance);
return new NacosServer(instance);
} catch (NacosException e) {
log.error("獲取實例發生異常", e);
return null;
}
}
}
class ExtendBalancer extends Balancer {
/**
* 由于Balancer類里的getHostByRandomWeight方法是protected的,
* 所以通過這種繼承的方式來實現調用,該方法基于隨機權重的負載均衡算法,選取一個實例
*/
static Instance getHost(List<Instance> hosts) {
return getHostByRandomWeight(hosts);
}
}
同樣的,想要使用該負載均衡策略的話,在配置文件中配置一下即可:
user-center:
ribbon:
NFLoadBalancerRuleClassName: com.zj.node.contentcenter.configuration.NacosSameClusterWeightedRule
在以上兩個小節我們實現了基于Nacos權重的負載均衡策略及同一集群下優先調用的負載均衡策略,但在實際項目中,可能會面臨多版本共存的問題,即一個微服務擁有不同版本的實例,并且這些不同版本的實例之間可能是互不兼容的。例如微服務A的v1版本實例無法調用微服務B的v2版本實例,只能夠調用微服務B的v1版本實例。
而Nacos中的元數據就比較適合解決這種版本控制的問題,至于元數據的概念及配置方式已經在Spring Cloud Alibaba之服務發現組件 - Nacos一文中介紹過,這里主要介紹一下如何通過Ribbon去實現基于元數據的版本控制。
舉個例子,線上有兩個微服務,一個作為服務提供者一個作為服務消費者,它們都有不同版本的實例,如下:
v1和v2是不兼容的。服務消費者v1只能調用服務提供者v1;消費者v2只能調用提供者v2。如何實現呢?下面我們來圍繞該場景,實現微服務之間的版本控制。
綜上,我們需要實現的主要有兩點:
首先我們得在配置文件中配置元數據,元數據就是一堆的描述信息,以k - v形式進行配置,如下:
spring:
cloud:
nacos:
discovery:
# 指定nacos server的地址
server-addr: 127.0.0.1:8848
# 配置元數據
metadata:
# 當前實例版本
version: v1
# 允許調用的提供者實例的版本
target-version: v1
然后就可以寫代碼了,和之前一樣,也是通過負載均衡策略實現,具體代碼如下:
package com.zj.node.contentcenter.configuration; import com.alibaba.nacos.api.exception.NacosException; import com.alibaba.nacos.api.naming.NamingService; import com.alibaba.nacos.api.naming.pojo.Instance; import com.alibaba.nacos.client.naming.utils.CollectionUtils; import com.alibaba.nacos.client.utils.StringUtils; import com.netflix.client.config.IClientConfig; import com.netflix.loadbalancer.AbstractLoadBalancerRule; import com.netflix.loadbalancer.DynamicServerListLoadBalancer; import com.netflix.loadbalancer.Server; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.alibaba.nacos.NacosDiscoveryProperties; import org.springframework.cloud.alibaba.nacos.ribbon.NacosServer; import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.function.Predicate; import java.util.stream.Collectors; /** * 基于元數據的版本控制負載均衡策略 * * @author 01 * @date 2019-07-27 **/ @Slf4j public class NacosFinalRule extends AbstractLoadBalancerRule { @Autowired private NacosDiscoveryProperties discoveryProperties; private static final String TARGET_VERSION = "target-version"; private static final String VERSION = "version"; @Override public void initWithNiwsConfig(IClientConfig iClientConfig) { // do nothing } @Override public Server choose(Object key) { // 獲取配置文件中所配置的集群名稱 String clusterName = discoveryProperties.getClusterName(); // 獲取配置文件中所配置的元數據 String targetVersion = discoveryProperties.getMetadata().get(TARGET_VERSION); DynamicServerListLoadBalancer loadBalancer = (DynamicServerListLoadBalancer) getLoadBalancer(); // 需要請求的微服務名稱 String serviceId = loadBalancer.getName(); // 獲取該微服務的所有健康實例 List<Instance> instances = getInstances(serviceId); List<Instance> metadataMatchInstances = instances; // 如果配置了版本映射,那么代表只調用元數據匹配的實例 if (StringUtils.isNotBlank(targetVersion)) { // 過濾與版本元數據相匹配的實例,以實現版本控制 metadataMatchInstances = filter(instances, i -> Objects.equals(targetVersion, i.getMetadata().get(VERSION))); if (CollectionUtils.isEmpty(metadataMatchInstances)) { log.warn("未找到元數據匹配的目標實例!請檢查配置。targetVersion = {}, instance = {}", targetVersion, instances); return null; } } List<Instance> clusterMetadataMatchInstances = metadataMatchInstances; // 如果配置了集群名稱,需篩選同集群下元數據匹配的實例 if (StringUtils.isNotBlank(clusterName)) { // 過濾出相同集群下的所有實例 clusterMetadataMatchInstances = filter(metadataMatchInstances, i -> Objects.equals(clusterName, i.getClusterName())); if (CollectionUtils.isEmpty(clusterMetadataMatchInstances)) { clusterMetadataMatchInstances = metadataMatchInstances; log.warn("發生跨集群調用。clusterName = {}, targetVersion = {}, clusterMetadataMatchInstances = {}", clusterName, targetVersion, clusterMetadataMatchInstances); } } // 基于隨機權重的負載均衡算法,選取其中一個實例 Instance instance = ExtendBalancer.getHost(clusterMetadataMatchInstances); return new NacosServer(instance); } /** * 通過過濾規則過濾實例列表 */ private List<Instance> filter(List<Instance> instances, Predicate<Instance> predicate) { return instances.stream() .filter(predicate) .collect(Collectors.toList()); } private List<Instance> getInstances(String serviceId) { // 獲取服務發現的相關API NamingService namingService = discoveryProperties.namingServiceInstance(); try { // 獲取該微服務的所有健康實例 return namingService.selectInstances(serviceId, true); } catch (NacosException e) { log.error("發生異常", e); return Collections.emptyList(); } } } class ExtendBalancer extends Balancer { /** * 由于Balancer類里的getHostByRandomWeight方法是protected的, * 所以通過這種繼承的方式來實現調用,該方法基于隨機權重的負載均衡算法,選取一個實例 */ static Instance getHost(List<Instance> hosts) { return getHostByRandomWeight(hosts); }
}
看了以上關于實現負載均衡的主要兩種方式,如果大家還有什么地方需要了解的可以在億速云行業資訊里查找自己感興趣的或者找我們的專業技術工程師解答的,億速云技術工程師在行業內擁有十幾年的經驗了。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。