文章目录
特别声明:文章的内容学习自慕课网大目老师的微服务视频,视频链接: https://coding.imooc.com/learn/list/358.html,大目老师还写了很多微服务有关的手记,都很实用!
前情提要:上一篇博文已经介绍了服务注册nacos的实现。现在我注册了content-center和user-center两个服务,content-center使用RestTemplate调用user-center服务,使用ribbon做负载均衡
1.ribbon的组成
- IRule 核心接口,负载均衡的规则,默认是ZoneAvoidanceRule
- ServerListUpdater,更新ServerList的策略,当nacos上server实例个数发生变化时,ribbon里的ServerList什么时候去更新呢?默认值PollingServerListerUpdate定时更新
2.项目使用Ribbon
2.1 引入依赖
Ribbon已经包含在spring-cloud-starter-alibaba-nacos-discovery里了,所以不用再重复添加依赖了
2.2 加注解
启动类那边不用加注解,注解加载RestTemplate上面@LoadBalanced
@Configuration
public class RestTemplateConfig {
@Bean
@LoadBalanced
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
}
2.3 写配置
暂时不用写配置
3.负载均衡规则 IRule
3.1 测试 ZoneAvoidanceRule 默认规则
这里的zone是什么,我也不知道,我看源码是个String。我测试的环境应该是没有zone的,看启动当然日志是 UNKNOWN ,所以它相当于RoundRobinRule轮询。
启动三个user-center和一个content-center:
- content-center 代码:
@RestController
@RequestMapping("poem")
public class PoemController {
private static final Logger log = LoggerFactory.getLogger(PoemController.class);
@Autowired
private PoemService poemService;
@Autowired
private RestTemplate restTemplate;
@Autowired
private DiscoveryClient discoveryClient;
@GetMapping(value = "testRibbon")
public Object testRibbon() {
// List<ServiceInstance> instances = discoveryClient.getInstances("user-center");
// String targetUrl = instances.stream()
// .map(instance -> instance.getUri().toString() + "/reciteHis/testAno")
// .findFirst()
// .orElseThrow(() -> new IllegalArgumentException("当前没有实例!"));
String targetUrl= "http://user-center/reciteHis/testAno";
log.info("targetUrl={}",targetUrl);
Page forObject = restTemplate.getForObject(targetUrl, Page.class);
return ResponseVO.success(forObject);
}
String targetUrl= “http://user-center/reciteHis/testAno” 里的user-center就是nacos里user-center的服务名
- user-center 代码;
@RestController
@RequestMapping("/reciteHis")
public class ReciteHisController {
private static final Logger log = LoggerFactory.getLogger(ReciteHisController.class);
@Autowired
private ReciteHisService reciteHisService;
@Autowired
private MemberService memberService;
@GetMapping(value = "testAno")
public Object memberRctHisAno(HttpServletRequest request){
Page<ReciteHisOT> page = new Page<>(1, 2);
page.setRecords(reciteHisService.selectUserListPageAno(page,"3"));
log.info("我是{},我被调用了",request.getRequestURL());
return page;
}
随便提供一个测试接口就行,返回的内容不重要
- 访问content-center的 http://localhost:8081/poem/testRibbon 接口测试,刷新6次
看user-center的日志,三个console窗口各显示被调用了2次,是平均的,看来是轮询规则没错
3.2 测试 RandomRule 随机规则
- 写配置
application.yml。没有代码提示,纯手敲,user-center代表针对的是user-center服务写的配置 (这里用的是属性配置方式,还有一种java代码的配置方式)
user-center:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
- 测试
请求content-center的 http://localhost:8081/poem/testRibbon 接口测试,请求10次
user-center服务,我关掉了一个,三个太多了,现在2个服务实例,一个被调用6次,一个4次,简单证明是随机
3.3 其它的内置规则没那么方便测试,我不测试了
3.4 ribbon的全局配置
上面提到了ribbon的配置有两种方式
- 属性配置
这种方式暂时无法实现全局配置,刚才配置的RandomRule也只能对user-center服务起作用,如果调用其它服务,则还是默认的ZoneAvoidanceRule - java代码配置
step1:新建一个和com包平行的configuration包,新建RibbonConfiguration类
新建一个平行的包的目的是为了避免这个类被启动类扫描到,如果被启动类扫描到,会有父子上下文问题,如果父子上下文了,轻则配置不灵光了,重则服务启动不来,对于RibbonConfiguration这个来说,如果父子上下文了,那么它就自动变成全局的ribbon配置了,无法用于针对单个服务的特别配置
package configuration;
import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.RoundRobinRule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RibbonConfiguration {
@Bean
public IRule ribbonRule(){
return new RoundRobinRule();
}
}
这里配置的是轮询规则
step2:新建个RibbonClientConfig类,类名不是固定的,大家随意
package com.zengchen.content.config;
import configuration.RibbonConfiguration;
import org.springframework.cloud.netflix.ribbon.RibbonClients;
import org.springframework.context.annotation.Configuration;
@Configuration
@RibbonClients(defaultConfiguration = RibbonConfiguration.class)
public class RibbonClientConfig {
}
step3: 全局配置了轮询规则,测试一下
http://localhost:8081/poem/testRibbon 接口测试,请求6次
user-center 两个实例个被调用3次,基本证明全局配置生效。
而此时,application.yml里配置的是随机规则 ,并没有注释,所以java代码方式的配置优先级应该比属性配置的方式高一些。但是这个意义不大,最好不要两种方式同时配置!
user-center:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
3.5 自定义nacos权重负载均衡规则
- content-center里新写个类NacosWeightedRule
@Slf4j
public class NacosWeightedRule extends AbstractLoadBalancerRule {
@Autowired
private NacosDiscoveryProperties discoveryProperties;
private Server choose(DynamicServerListLoadBalancer lb, Object key) {
if (lb == null) {
return null;
}
String name = lb.getName();
try {
Instance instance = discoveryProperties.namingServiceInstance()
.selectOneHealthyInstance(name);
return new NacosServer(instance);
} catch (NacosException e) {
log.error("权重规则发生异常",e);
}
return null;
}
@Override
public Server choose(Object key) {
return choose((DynamicServerListLoadBalancer) getLoadBalancer(), key);
}
@Override
public void initWithNiwsConfig(IClientConfig iClientConfig) {
}
}
- 修改RibbonConfiguration返回新的NacosWeightedRule
@Configuration
public class RibbonConfiguration {
@Bean
public IRule ribbonRule(){
// return new RoundRobinRule();
return new NacosWeightedRule();
}
}
- 打开nacos控制台修改两个user-center服务实例的权重,8082端口的设置为1,8083端口的设置为4,这样8083的服务实例会接收更多的请求访问
- 重启content-center服务,请求http://localhost:8081/poem/testRibbon ,请求20次
结果:8082访问了7次,8083访问了13次
第二次测试结果:8082被访问了3次,8083被访问了17次
(如果把权重设置为0,就不会有请求访问了,可以做到优雅下线)
3.6 自定义同集群优先访问的负载均衡规则
- 新建类NacosSameClusterWeightedRule
@Slf4j
public class NacosSameClusterWeightedRule extends AbstractLoadBalancerRule {
@Autowired
private NacosDiscoveryProperties discoveryProperties;
private Server choose(DynamicServerListLoadBalancer lb, Object key) {
if (lb == null) {
return null;
}
try {
// 本服务所属集群
String clusterName = discoveryProperties.getClusterName();
// 要请求的服务的名称
String targetInstanceName = lb.getName();
// 服务相关Api
NamingService namingService = discoveryProperties.namingServiceInstance();
// 获取目标服务所有实例(健康的实例)
List<Instance> instances = namingService.selectInstances(targetInstanceName, true);
// 获得同集群实例
List<Instance> sameClusterInstances = instances.stream()
.filter(instance -> {
return Objects.equals(instance.getClusterName(), clusterName);
}).collect(Collectors.toList());
// 等待被选择的实例
List<Instance> toBeChosenInstances = sameClusterInstances;
// 如果没有同集群的实例,那就用所有实例
if(CollectionUtils.isEmpty(sameClusterInstances)){
toBeChosenInstances = instances;
log.warn("发生跨集群调用,集群名称={},目标服务名称={}",clusterName,targetInstanceName);
}
// 根据权重从toBeChosenInstances选择一个实例
return new NacosServer(ExtendsBalancer.getInstanceByWeight(toBeChosenInstances));
} catch (NacosException e) {
log.error("同集群负载规则发生异常", e);
}
return null;
}
@Override
public Server choose(Object key) {
return choose((DynamicServerListLoadBalancer) getLoadBalancer(), key);
}
@Override
public void initWithNiwsConfig(IClientConfig iClientConfig) {
}
}
/**
* <p>
* 利用子类使用Balance的protected getHostByRandomWeight方法
* </p>
*
* @author zengchen
* @since 2019-08-11
*/
class ExtendsBalancer extends Balancer {
public static Instance getInstanceByWeight(List<Instance> instances){
return getHostByRandomWeight(instances);
}
}
- 修改RibbonConfiguration返回新的NacosSameClusterWeightedRule
- 测试前置准备
启动三个user-center实例,两个SH集群的,一个BJ集群的
8082:SH,权重 1
8083:SH,权重 4
8084:BJ, 权重 1
spring:
cloud:
nacos:
discovery:
server-addr: 118.31.11.178:8848
# 所属集群 上海
cluster-name: SH
content-center 启动一个属于SH的
- 测试
请求http://localhost:8081/poem/testRibbon ,请求20次
8082:SH,权重 1,被访问2次
8083:SH,权重 4,被访问18次
8084:BJ, 权重 1,被访问0次
符合预期!
3.7 自定义根据服务实例元数据的负载均衡规则
可以实现v1版本的content-center只调用v1版本user-center
可以实现v2版本的content-center只调用v2版本user-center
- 给content-center配置元数据 self-version: v1, target-version: v1
application.yml
spring:
cloud:
nacos:
discovery:
server-addr: 118.31.11.178:8848
# 集群名称
cluster-name: SH
# 元数据
metadata:
self-version: v1
target-version: v1
- 新建类 NacosClusterMetadataWeightedRule
@Slf4j
public class NacosClusterMetadataWeightedRule extends AbstractLoadBalancerRule {
@Autowired
private NacosDiscoveryProperties discoveryProperties;
// 自己的版本号
private final static String SELF_VERSION = "self-version";
// 需要调用的版本号
private final static String TARGET_VERSION = "target-version";
private Server choose(DynamicServerListLoadBalancer lb, Object key) {
if (lb == null) {
return null;
}
try {
// 本服务目标版本号
String targetVersion = discoveryProperties.getMetadata().get(SELF_VERSION);
// 本服务所属集群
String clusterName = discoveryProperties.getClusterName();
// 要请求的服务的名称
String targetInstanceName = lb.getName();
// 服务相关Api
NamingService namingService = discoveryProperties.namingServiceInstance();
// 获取目标服务所有实例(健康的实例)
List<Instance> instances = namingService.selectInstances(targetInstanceName, true);
List<Instance> versionMatchInstances = instances;
// 获得targetVersion的实例
if(StringUtils.isNotBlank(targetVersion)){
versionMatchInstances = instances.stream()
.filter(instance -> {
return Objects.equals(instance.getMetadata().get(SELF_VERSION),TARGET_VERSION);
}).collect(Collectors.toList());
if(CollectionUtils.isEmpty(versionMatchInstances)){
log.warn("未找到元数据匹配的目标实例!请检查配置。targetVersion = {}, instance = {}", targetVersion, instances);
return null;
}
}
// 从元数据匹配的实例中获得同集群实例
List<Instance> clusterMetadataMatchInstances = versionMatchInstances;
if(StringUtils.isNotBlank(clusterName)){
clusterMetadataMatchInstances = versionMatchInstances.stream()
.filter(instance -> {
return Objects.equals(instance.getClusterName(), clusterName);
}).collect(Collectors.toList());
if(CollectionUtils.isEmpty(clusterMetadataMatchInstances)){
log.warn("发生跨集群调用。clusterName = {}, targetVersion = {}, clusterMetadataMatchInstances = {}", clusterName, targetVersion, clusterMetadataMatchInstances);
clusterMetadataMatchInstances = versionMatchInstances;
}
}
// 根据权重从toBeChosenInstances选择一个实例
return new NacosServer(ExtendsBalancer.getInstanceByWeight(clusterMetadataMatchInstances));
} catch (NacosException e) {
log.error("元数据同集群负载规则发生异常", e);
}
return null;
}
@Override
public Server choose(Object key) {
return choose((DynamicServerListLoadBalancer) getLoadBalancer(), key);
}
@Override
public void initWithNiwsConfig(IClientConfig iClientConfig) {
}
}
- 修改RibbonConfiguration返回新的NacosClusterMetadataWeightedRule
@Configuration
public class RibbonConfiguration {
@Bean
public IRule ribbonRule(){
// return new RoundRobinRule();
// return new NacosWeightedRule();
// return new NacosSameClusterWeightedRule();
return new NacosClusterMetadataWeightedRule();
}
}
- 测试前置准备
启动四个user-center实例,三个SH集群的,一个BJ集群的
8082:SH,权重 1,self-version v1
8083:SH,权重 4,self-version v1
8084:SH,权重 1,self-version v2
8085:BJ, 权重 1,self-version v1
- 测试
请求http://localhost:8081/poem/testRibbon ,请求20次
8082:SH,权重 1,self-version v1,被访问2次
8083:SH,权重 4,self-version v1,被访问18次
8084:SH,权重 1,self-version v2,被访问0次
8085:BJ, 权重 1,self-version v1,被访问0次
符合预期!
4.饥饿加载
第一次调用user-center服务,速度都很慢。因为ribbon是懒加载模式,可以通过属性配置方式改成饥饿模式,这样第一次和后面再次请求的速度就没有区别了
application.yml
ribbon:
# 饥饿加载
eager-load:
# 开启
enabled: true
# 哪些服务,英文逗号隔开
clients: user-center