[TOC]
负载均衡
我们都知道在微服务架构中,微服务之间总是需要互相调用,以此来实现一些组合业务的需求。例如组装订单详情数据,由于订单详情里有用户信息,所以订单服务就得调用用户服务来获取用户信息。要实现远程调用就需要发送网络请求,而每个微服务都可能会存在有多个实例分布在不同的机器上,那么当一个微服务调用另一个微服务的时候就需要将请求均匀的分发到各个实例上,以此避免某些实例负载过高,某些实例又太空闲,所以在这种场景必须要有负载均衡器。
目前实现负载均衡主要的两种方式:
1、服务端负载均衡;例如最经典的使用Nginx做负载均衡器。用户的请求先发送到Nginx,然后再由Nginx通过配置好的负载均衡算法将请求分发到各个实例上,由于需要作为一个服务部署在服务端,所以该种方式称为服务端负载均衡。如图:
文章图片
image.png
2、客户端侧负载均衡;之所以称为客户端侧负载均衡,是因为这种负载均衡方式是由发送请求的客户端来实现的,也是目前微服务架构中用于均衡服务之间调用请求的常用负载均衡方式。因为采用这种方式的话服务之间可以直接进行调用,无需再通过一个专门的负载均衡器,这样能够提高一定的性能以及高可用性。以微服务A调用微服务B举例,简单来说就是微服务A先通过服务发现组件获取微服务B所有实例的调用地址,然后通过本地实现的负载均衡算法选取出其中一个调用地址进行请求。如图:
文章图片
image.png
【Spring|Spring Cloud Alibaba之负载均衡组件 - Ribbon】我们来通过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 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);
}
}
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();
}
}
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;
}
@Configuration
public class RibbonConfig {@Bean
public IRule ribbonRule(){
// 随机的负载均衡策略对象
return new RandomRule();
}
}
@Configuration
// 该注解用于自定义Ribbon客户端配置,这里声明为属于user-center的配置
@RibbonClient(name = "user-center", configuration = RibbonConfig.class)
public class UserCenterRibbonConfig {
}
https://cloud.spring.io/spring-cloud-static/Greenwich.SR2/single/spring-cloud.html#_customizing_the_ribbon_client2、使用配置文件进行配置就更简单了,不需要写代码还不会有父子上下文扫描重叠的坑,只需在配置文件中增加如下一段配置就可以实现以上使用代码配置等价的效果:
user-center:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
@Configuration
// 该注解用于全局配置
@RibbonClients(defaultConfiguration = RibbonConfig.class)
public class GlobalRibbonConfig {
}
ribbon:
eager-load:
enabled: true
# 为哪些客户端开启饥饿加载,多个客户端使用逗号分隔(非必须)
clients: user-center
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
privateNacosDiscoveryProperties 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
个人认为,这主要是为了符合Spring Cloud标准。Spring Cloud Commons有个子项目 spring-cloud-loadbalancer ,该项目制定了标准,用来适配各种客户端负载均衡器(虽然目前实现只有Ribbon,但Hoxton就会有替代的实现了)。其他实现方式的参考文章:
Spring Cloud Alibaba遵循了这一标准,所以整合了Ribbon,而没有去使用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.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 instances = namingService.selectInstances(serviceId, true);
// 过滤出相同集群下的所有实例
List sameClusterInstances = instances.stream()
.filter(i -> Objects.equals(i.getClusterName(), clusterName))
.collect(Collectors.toList());
// 相同集群下没有实例则需要使用其他集群下的实例
List 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 hosts) {
return getHostByRandomWeight(hosts);
}
}
user-center:
ribbon:
NFLoadBalancerRuleClassName: com.zj.node.contentcenter.configuration.NacosSameClusterWeightedRule
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 instances = getInstances(serviceId);
List 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 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 filter(List instances, Predicate predicate) {
return instances.stream()
.filter(predicate)
.collect(Collectors.toList());
}private List 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 hosts) {
return getHostByRandomWeight(hosts);
}
}
下一篇:DOM树问题