咱们都知道在微服务架构中,微服务之间老是须要互相调用,以此来实现一些组合业务的需求。例如组装订单详情数据,因为订单详情里有用户信息,因此订单服务就得调用用户服务来获取用户信息。要实现远程调用就须要发送网络请求,而每一个微服务均可能会存在有多个实例分布在不一样的机器上,那么当一个微服务调用另外一个微服务的时候就须要将请求均匀的分发到各个实例上,以此避免某些实例负载太高,某些实例又太空闲,因此在这种场景必需要有负载均衡器。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); } }