咱們都知道在微服務架構中,微服務之間老是須要互相調用,以此來實現一些組合業務的需求。例如組裝訂單詳情數據,因爲訂單詳情裏有用戶信息,因此訂單服務就得調用用戶服務來獲取用戶信息。要實現遠程調用就須要發送網絡請求,而每一個微服務均可能會存在有多個實例分佈在不一樣的機器上,那麼當一個微服務調用另外一個微服務的時候就須要將請求均勻的分發到各個實例上,以此避免某些實例負載太高,某些實例又太空閒,因此在這種場景必需要有負載均衡器。html
目前實現負載均衡主要的兩種方式:java
一、服務端負載均衡;例如最經典的使用Nginx作負載均衡器。用戶的請求先發送到Nginx,而後再由Nginx經過配置好的負載均衡算法將請求分發到各個實例上,因爲須要做爲一個服務部署在服務端,因此該種方式稱爲服務端負載均衡。如圖:
node
二、客戶端側負載均衡;之因此稱爲客戶端側負載均衡,是由於這種負載均衡方式是由發送請求的客戶端來實現的,也是目前微服務架構中用於均衡服務之間調用請求的經常使用負載均衡方式。由於採用這種方式的話服務之間能夠直接進行調用,無需再經過一個專門的負載均衡器,這樣可以提升必定的性能以及高可用性。以微服務A調用微服務B舉例,簡單來講就是微服務A先經過服務發現組件獲取微服務B全部實例的調用地址,而後經過本地實現的負載均衡算法選取出其中一個調用地址進行請求。如圖:
web
咱們來經過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:spring
Ribbon雖然是個主要用於負載均衡的小組件,可是麻雀雖小五臟俱全,Ribbon仍是有許多的接口組件的。以下表:
api
Ribbon默認內置了八種負載均衡策略,若想自定義負載均衡策略則實現上表中提到的IRule接口或AbstractLoadBalancerRule抽象類便可。內置的負載均衡策略以下:
網絡
Ribbon主要有兩種使用方式,一是使用Feign,Feign內部已經整合了Ribbon,所以若是隻是普通使用的話都感知不到Ribbon的存在;二是配合RestTemplate使用,這種方式則須要添加Ribbon依賴和@LoadBalanced註解。架構
這裏主要演示一下第二種使用方式,因爲項目中添加的Nacos依賴已包含了Ribbon因此不須要另外添加依賴,首先定義一個RestTemplate,代碼以下:app
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的時候採用默認策略,下面咱們就來實現一下這種細粒度的配置。
一、首先是經過代碼進行配置,編寫一個配置類用於實例化指定的負載均衡策略對象:
@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仍是其餘微服務都會採用該配置類裏定義的負載均衡策略,這樣就會變成了一個全局配置了,違背了咱們須要細粒度配置的目的。因此須要將其定義在主啓動類以外:
關於這個問題能夠參考官方文檔的描述:
二、使用配置文件進行配置就更簡單了,不須要寫代碼還不會有父子上下文掃描重疊的坑,只需在配置文件中增長以下一段配置就能夠實現以上使用代碼配置等價的效果:
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); } }