大家好,我是阿七。
在上一篇文章中,我们已经实现了内容中心总能够调用用户中心,那如何实现负载均衡呢?请听阿七为你娓娓道来。(没看到上篇文章的同学请戳这里:实战(一)nacos注册与发现)
众所周知,在负载均衡领域一般有两种方式去实现,分别是:
1、服务器端负载均衡;
2、客户端侧负载均衡;
在单体架构时代,我们一般会部署多个实例在服务器上,然后使用Nginx做负载均衡(nginx也是部署在服务器上的)。请求全部打在nginx上,nginx根据负载均衡策略将请求转发到不同的实例上。如下面这幅图所示:
有同学会疑问了,为什么nginx可以抗住这么多的请求呢?因为它异步,非阻塞,使用了epoll 和大量的底层代码优化,哈哈跑题了,阿七后面再专门写文章说说nginx。
说完了服务器端负载均衡,那么什么是客户端负载均衡呢?且看下图:
在这幅图中,内容中心是要调用用户中心的,那么内容中心相对于用户中心就是客户端,对吧,这个应该可以理解。现在我们已经可以通过DiscoveryClient来获取用户中心的多个实例,如果我们在内容中心自己写一个负载均衡规则,然后交给RestTemplate来请求一个用户中心实例,这样就实现了客户端负载均衡了。说干就干,咱这就来写一个负载均衡策略。
第一步,修改代码:之前是调用findFirst()默认返回第一个用户中心实例,现在注释掉了,改成随机获取一个用户中心实例。
public ShareDTO findById(Integer id) {
Share share = this.shareMApper.selectByPrimaryKey(id);
Integer userId = share.getUserId();
//用户中心的所有实例信息
List<ServiceInstance> instances = discoveryClient.getInstances("user-center");
// String targetUrl = instances
// .stream()
// .map(instance -> instance.getUri().toString() + "/users/{id}")
// .findFirst()
// .orElseThrow(() -> new IllegalArgumentException("当前没有实例对象"));
List<String> targetUrls = instances
.stream()
.map(instance -> instance.getUri().toString() + "/users/{id}")
.collect(Collectors.toList());
//随机算法
int i = ThreadLocalRandom.current().nextInt(targetUrls.size());
String targetUrl = targetUrls.get(i);
log.info("请求的目标地址:{}",targetUrl);
//根据userId查询用户信息
UserDTO userDTO = this.restTemplate.getForObject(targetUrl, UserDTO.class, userId);
ShareDTO shareDTO = new ShareDTO();
BeanUtils.copyProperties(share, shareDTO);
shareDTO.setWxNickname(userDTO.getWxNickname());
return shareDTO;
}
第二步,测试:
1、我们先启动一个用户中心实例,端口为8888;然后修改端口号为8887,点击Edit Configurations,勾选Allow parallel run,保存,再启动一次项目,这样就可以启动两个用户中心实例。如下图:
2、启动内容中心实例,打开nacos看看
3、接口测试,访问http://localhost:8889/shares/1
这时,我们去控制台看看:
哎,我们会发现,内容中心在随机调用用户中心的实例。说白了,负载均衡器就是给你一个list,你随机从中获取一个实例来调用。 但是我们这个代码还是比较简单的,只是模拟一下。如果我们每次进行服务调用都写这么一堆代码肯定也是不现实的。
那么下面我们就要ribbon进行重构咯,同学们继续往下看。
在使用ribbon进行重构之前,我们肯定得了解什么是ribbon对吧。一句话阐述ribbon是Netflix开源的客户端侧负载均衡器。 其实,ribbon就是我们刚刚写的代码的一个组件,但是它封装的更好,提供了更多的负载均衡策略。
下面我们就来整合ribbon到我们的项目中。还记得三步骤吗?
第一步加依赖:因为nacos-discovery中已经集成了netflix-ribbon,所以这里就不要在单独集成了。
第二步加注解:在RestTemplate上加注解@LoadBalanced
@MapperScan("com.seven")
@SpringBootApplication
public class ContentCenterApplication {
public static void main(String[] args) {
SpringApplication.run(ContentCenterApplication.class, args);
}
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
第三步写配置:在yml文件中写配置,这里不需要写。
第四步修改代码:ribbon会根据user-center去nacos中找到请求的真是路径。
public ShareDTO findById(Integer id) {
Share share = this.shareMapper.selectByPrimaryKey(id);
Integer userId = share.getUserId();
//根据userId查询用户信息
UserDTO userDTO = this.restTemplate
.getForObject("http://user-center/users/{userId}", UserDTO.class, userId);
ShareDTO shareDTO = new ShareDTO();
BeanUtils.copyProperties(share, shareDTO);
shareDTO.setWxNickname(userDTO.getWxNickname());
return shareDTO;
}
ok,到这里ribbon就整合完了。那有人会问,到这就结束了吗?那显然不是,阿七不是浮于表面的人,咱学习一个东西就要刨根问底。
ribbon组件虽小,但是五脏俱全,它的组成都有哪些呢?贴心的我已经为大家准备好了。
同时,ribbon支持8种负载均衡策略,如下所示:
这里的Zone本意为地区、地带的意思,这里作机房的架子,也就是放服务器的机架。那我们实际上没有Zone的,也就是说默认采用的负载均衡策略是RoundRibonRule(轮训策略)。我们看一下源码,找到RoundRobinRule.JAVA类,其中最主要的就是这一段,实际上就是取余得到访问的server的index。
private int incrementAndGetModulo(int modulo) {
int current;
int next;
do {
current = this.nextServerCyclicCounter.get();
next = (current + 1) % modulo;
} while(!this.nextServerCyclicCounter.compareAndSet(current, next));
return next;
}
compareAndSet是AtomicInteger的方法,其实类似参数列表compareAndSwapInt(var1, var2, var5, var4),作为base的var1加上偏移量var2之后和var5比较是不是值相同,相同就update为var4. valueOffset是native的C的方法指针找到的地址。
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
那我们如何细粒度、自定义配置负载均衡策略呢?比如,内容中心想指定一种负载均衡策略来获取用户中心的实例。很简单,我们只需要加一下yml配置文件即可。
#指定服务名
user-center:
ribbon:
#负载均衡策略的全路径
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
这样我们就修改ribbon负载均衡策略为RandomRule,也就是随机获取一个实例。
在实际代码运行中发现,当我们第一次请求http://localhost:8889/shares/1这个接口时,运行结果是很慢的,为什么呢?因为ribbon默认是懒加载。只有在下面代码第一次执行的时候,才会创建一个名叫user-center的ribbon client。
//根据userId查询用户信息
UserDTO userDTO = this.restTemplate
.getForObject("http://user-center/users/{userId}", UserDTO.class, userId);
我们可以通过修改yml配置文件来解决这个问题。
ribbon:
eager-load:
enabled: true
#多个用逗号隔开 user-center,xxx,yyy
clients: user-center
这样就可以第一次请求也变得很快啦。
好了ribbon就学习到这里了。但是,我们的代码还是存在很多问题的,那么下一篇我们将一起学习声明式http客户端--feign,一起将代码优化的更好吧。
喜欢的朋友记得点个关注吧,一起探讨,共同进步。