Ribbon实现负载均衡
一、实现方式
-
服务器端负载均衡(单体架构)
-
客户端侧负载均衡(内容中心相对用户中心,属于客户端,可在内容中心集群中配置负载均衡规则)
二、手写客户端侧负载均衡器
- 随机负载均衡算法
三、使用Ribbon实现负载均衡
1、定义:Netfix开源的客户端侧负载均衡器,即提供了负载均衡算法
2、架构演进:
3、整合Ribbon
- 加依赖:不需要,spring-cloud-starter-alibaba-nacos-discovery已默认提供
- 加注解
- 写配置:无配置
PS:代码改写,使用http://user-center/users/{userId},Ribbon会自动读取名称,进行地址查找并转换
四、Ribbon 组成
1、内置负载均衡规则
PS:默认为ZoneAvoidanceRule,无Zone环境,等价于RoundRobinRule
五、Ribbon 配置
1、java代码配置,采用随机算法
- 新建RibbonConfiguration 配置类
package com.hzb2i.contentcenter.configuration.Ribbon;
import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.RandomRule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RibbonConfiguration {
@Bean
public IRule ribbonRule(){
return new RandomRule();
}
}
- 新建 UserCenterRibbonConfiguration 类
package com.hzb2i.contentcenter.configuration;
import RibbonConfiguration.RibbonConfiguration;
import org.springframework.cloud.netflix.ribbon.RibbonClient;
import org.springframework.context.annotation.Configuration;
@Configuration
@RibbonClient(name = "user-center",configuration = RibbonConfiguration.class)
public class UserCenterRibbonConfiguration {
}
PS:需要注意UserCenterRibbonConfiguration 需要放到@SpringBootApplication启动类扫描不到的包下
- @SpringBootApplication注解默认扫描当前的包和子包
- 若将UserCenterRibbonConfiguration 放到启动类相同路径下,将会出现@SpringBootApplication 主上下文和UserCenterRibbonConfiguration 类的@Configuration 子上下文出现重复扫描,UserCenterRibbonConfiguration 的配置为全局配置,将会被所有RibbonClient共享,即不管内容中心访问用户中心还是其他的微服务,都会采用UserCenterRibbonConfiguration 配置(重复扫描带来的问题,感兴趣可看此分享:spring重复扫描事务失败)
2、用配置属性配置:application.yml加入以下配置
user-center:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
PS:两种配置方式对比
PS:写配置方式,线上无需重新打包,采用的是读取外部配置文件方式
3、最佳实践:
- 尽量使用属性配置,属性配置实现不了情况再考虑用代码配置
- 在同一个微服务内尽量保持单一性,比如统一使用属性配置,不要用两种方式混用,增加定位代码的复杂性
4、全局配置:
-
方式一:让ComponentScan重叠,即将RibbonConfiguration放到启动类相同路径(不建议使用,会导致项目无法启动)
-
正确途径
5、Ribbon其他配置项 -
代码配置:
-
写配置:
六、Ribbon 饥饿加载
- 问题:Ribbon默认采用懒加载,只有http://user-center/users/{userId}执行,才会使用Ribbon自动获取用户中心地址,导致第一次访问加载速度慢
- 措施:写配置,开启饥饿加载
七、Ribbon扩展
1、支持Nacos权重(根据权重判断的负载均衡规则):
- 场景:若微服务有众多实例,分别部署到不同服务器上,可以把在性能好的机器上的实例权重设高点,性能差的机器的实例权重设低点,这样权重高的实例被调用的几率就高一些
- ribbon默认不支持nacos权重,需通过以下方式开发
① 可实现IRule接口
② 继承AbstractLoadBalancerRule抽象类,并将此负载均衡算法设为全局配置
package com.hzb2i.contentcenter.configuration;
import com.alibaba.cloud.nacos.NacosDiscoveryProperties;
import com.alibaba.cloud.nacos.ribbon.NacosServer;
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;
/*
* ribbon 扩展:支撑nacos权重
*/
@Slf4j
public class NacosWeightRule extends AbstractLoadBalancerRule {
@Autowired
private NacosDiscoveryProperties nacosDiscoveryProperties;
@Override
public void initWithNiwsConfig(IClientConfig iClientConfig) {
//读取配置,并初始化NacosWeightRule
}
@Override
public Server choose(Object o) {
try {
//入口
BaseLoadBalancer loadBalancer = (BaseLoadBalancer) this.getLoadBalancer();
//想要请求的微服务名称
String name = loadBalancer.getName();
//实现负载均衡算法
//拿到服务发现的相关API
NamingService namingService = nacosDiscoveryProperties.namingServiceInstance();
// nacos client 自动通过基于权重的负责均衡算法,给我们选择一个实例
Instance instance = namingService.selectOneHealthyInstance(name);
log.info("选择的实例:port={},instance={}",instance.getPort(),instance);
return new NacosServer(instance);
} catch (NacosException e) {
e.printStackTrace();
}
return null;
}
}
在nacos控制台修改权重:
启动用户中心、内容中心测试:请求基本调用权重高的实例
PS:nacos client内置了基于权重的负载均衡算法,spring cloud Alibaba为何还要整合ribbon,直接用nacos client不就行了吗?主要是为符合spring cloud标准,spring cloud有个子项目spring cloud commons(定义了标准),其下边的子项目spring cloud loadbalancer(无权重概念),spring cloud Alibaba符合此标准,故未整合权重的算法
PS:其他实现权重负载均衡算法方式请移步 扩展Ribbon支持Nacos权重的三种方式
2、同集群优先调用
- 场景:如存在北京机房和南京机房,想让北京机房的内容中心有限调用同机房的用户中心,在同机房用户中心不存在的情况下再调用南京机房的用户中心,可以采用此方式
- 代码开发,并将此负载均衡算法设为全局配置:
package com.hzb2i.contentcenter.configuration;
import com.alibaba.cloud.nacos.NacosDiscoveryProperties;
import com.alibaba.cloud.nacos.ribbon.NacosServer;
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.alibaba.nacos.client.naming.utils.Chooser;
import com.alibaba.nacos.client.naming.utils.Pair;
import com.alibaba.nacos.client.utils.LogUtils;
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.util.CollectionUtils;
import java.util.*;
import java.util.stream.Collectors;
@Slf4j
public class NacosSameClusterWeightRule extends AbstractLoadBalancerRule {
@Autowired
private NacosDiscoveryProperties nacosDiscoveryProperties;
@Override
public void initWithNiwsConfig(IClientConfig iClientConfig) {
}
@Override
public Server choose(Object o) {
try {
String clusterName = nacosDiscoveryProperties.getClusterName();
//入口
BaseLoadBalancer loadBalancer = (BaseLoadBalancer) this.getLoadBalancer();
//想要请求的微服务名称
String name = loadBalancer.getName();
//实现负载均衡算法
//拿到服务发现的相关API
NamingService namingService = nacosDiscoveryProperties.namingServiceInstance();
//找到指定服务的所有API
List<Instance> instances = namingService.selectInstances(name,true);
//过滤相同集群的所有实例
List<Instance> sameClusterInstances = instances.stream().filter(instance -> {
return Objects.equals(instance.getClusterName(),clusterName);
}).collect(Collectors.toList());
//如果B(sameClusterInstances)是空,否则用A(instances)
List<Instance> instancesToBeChosen = new ArrayList<>();
if(CollectionUtils.isEmpty(sameClusterInstances)){
instancesToBeChosen = instances;
log.info("跨集群调用:name={},clusterName={},instances={}",name,clusterName,instances);
}else {
instancesToBeChosen = sameClusterInstances;
log.info("同集群调用:name={},clusterName={},instances={}",name,clusterName,sameClusterInstances);
}
//基于权重的负载均衡算法,返回一个实例(namingService无根据list直接返回实例)
Instance instance = ExtendBalancer.getHostByRandomWeight2(instancesToBeChosen);
return new NacosServer(instance);
} catch (NacosException e) {
e.printStackTrace();
}
return null;
}
}
class ExtendBalancer extends Balancer {
public static Instance getHostByRandomWeight2(List<Instance> hosts) {
LogUtils.NAMING_LOGGER.debug("entry randomWithWeight");
if (hosts != null && hosts.size() != 0) {
LogUtils.NAMING_LOGGER.debug("new Chooser");
List<Pair<Instance>> hostsWithWeight = new ArrayList();
Iterator var2 = hosts.iterator();
while(var2.hasNext()) {
Instance host = (Instance)var2.next();
if (host.isHealthy()) {
hostsWithWeight.add(new Pair(host, host.getWeight()));
}
}
LogUtils.NAMING_LOGGER.debug("for (Host host : hosts)");
Chooser<String, Instance> vipChooser = new Chooser("www.taobao.com");
vipChooser.refresh(hostsWithWeight);
LogUtils.NAMING_LOGGER.debug("vipChooser.refresh");
return (Instance)vipChooser.randomWithWeight();
} else {
LogUtils.NAMING_LOGGER.debug("hosts == null || hosts.size() == 0");
return null;
}
}
}
启动测试:优先访问同集群的端口,同集群微服务挂了,才进行跨集群访问
3、基于元数据的版本控制:扩展Ribbon支持基于元数据的版本管理
PS:深入理解Nacos NameSpace:微服务只能调用同一空间的实例