负载均衡在系统架构中是一个非常重要,并且是不得不去实施的内容。因为负载均衡是对系统的高可用、网络压力的缓解和处理能力扩容的重要手段之一。我们通常所说的负载均衡都指的是服务端负载均衡,其中分为硬件负载均衡和软件负载均衡。硬件负载均衡主要通过在服务器节点之间按照专门用于负载均衡的设备,比如F5等;而软件负载均衡则是通过在服务器上安装一些用于负载均衡功能或模块等软件来完成请求分发工作,比如Nginx等。不论采用硬件负载均衡还是软件负载均衡,只要是服务端都能以类似下图的架构方式构建起来:
Spring Cloud Ribbon是一个基于HTTP和TCP的客户端负载均衡工具,它基于Netflix Ribbon实现。通过Spring Cloud的封装,可以让我们轻松地将面向服务的REST模版请求自动转换成客户端负载均衡的服务调用。Spring Cloud Ribbon虽然只是一个工具类框架,它不像服务注册中心、配置中心、API网关那样需要独立部署,但是它几乎存在于每一个Spring Cloud构建的微服务和基础设施中。
硬件负载均衡的设备或是软件负载均衡的软件模块都会维护一个下挂可用的服务端清单,通过心跳检测来剔除故障的服务端节点以保证清单中都是可以正常访问的服务端节点。当客户端发送请求到负载均衡设备的时候,该设备按某种算法(比如线性轮询、按权重负载、按流量负载等)从维护的可用服务端清单中取出一台服务端端地址,然后进行转发。
而客户端负载均衡和服务端负载均衡最大的不同点在于上面所提到服务清单所存储的位置。在客户端负载均衡中,所有客户端节点都维护着自己要访问的服务端清单,而这些服务端端清单来自于服务注册中心,比如上一章我们介绍的Eureka服务端。同服务端负载均衡的架构类似,在客户端负载均衡中也需要心跳去维护服务端清单的健康性,默认会创建针对各个服务治理框架的Ribbon自动化整合配置,比如Eureka中的org.springframework.cloud.netflix.ribbon.eureka.RibbonEurekaAutoConfiguration,Consul中的org.springframework.cloud.consul.discovery.RibbonConsulAutoConfiguration。在实际使用的时候,我们可以通过查看这两个类的实现,以找到它们的配置详情来帮助我们更好地使用它。
通过Spring Cloud Ribbon的封装,我们在微服务架构中使用客户端负载均衡调用非常简单,只需要如下两步:
服务提供者只需要启动多个服务实例并注册到一个注册中心或是多个相关联的服务注册中心。
服务消费者直接通过调用被@LoadBalanced注解修饰过的RestTemplate来实现面向服务的接口调用。
这样,我们就可以将服务提供者的高可用以及服务消费者的负载均衡调用一起实现了。
摘自:《Chandler_珏瑜-ribbon详解》
之前的eureka服务测试中,我们使用了非常有用的对象RestTemplate。该对象会使用Ribbon的自动化配置,同时通过配置@LoadBalanced还能够开启客户端负载均衡。之前我们演示了通过RestTemplate实现了最简单的服务访问,下面我们将详细介绍RestTemplate针对几种不同请求类型和参数类型的服务调用实现。
在RestTemplate中,对GET请求可以通过如下两个方法进行调用实现:
getForEntity(): getForEntity函数实际上提供了三种不同的重载实现
getForObject() getForObject函数是对getForEntity函数的再次封装,也对应的提供了三种不同的重载实现
详见《Chandler_珏瑜-ribbon详解》
在RestTemplate中,对GET请求可以通过如下两个方法进行调用实现:
postForEntity(): 该方法同GET请求中的getForEntity类似,会在调用后返回ResponseEntity对象,其中T为请求响应的body类型。postForEntity函数也实现了三种不同的重载方法。
postForObject(): 该方法也跟getForObject的类型类似,它的作用就是简化postForEntity的后续处理。通过直接将请求响应的body内容包装成对象来返回使用。postForObject函数也实现了三种不同的重载方法。
postForLocation(): 该方法实现了以POST请求提交资源,并返回新的资源的URI。
详见《Chandler_珏瑜-ribbon详解》
eureka是集成了ribbon的,所以我们可以在使用eureka做注册中心的时候,直接使用ribbon做负载均衡。
@Configuration
public class MySelfRule {
@Bean
public IRule setIRule(){
/*
* RoundRobinRule 轮询
* RandomRule 随机
* RetryRule 先按照RoundRobinRule的策略获取服务,如果获取服务失败则在指定时间内会进行重
* WeightedResponseTimeRule 对RoundRobinRule的扩展,响应速度越快的实例选择权重越大,越容易被选择
* BestAvailableRule 会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,然后选择一个并发量最小的服务
* AvailabilityFilteringRule 先过滤掉故障实例,再选择并发较小的实例
* ZoneAvoidanceRule 默认规则,复合判断server所在区域的性能和server的可用性选择服务器
* */
return new RandomRule(); // 选择需要的策略
}
}
@SpringBootApplication
@EnableDiscoveryClient
@RibbonClient(name = "CLOUD-PAYMENT-SERVICE",configuration = MySelfRule.class)
public class ConsulOrderMain80 {
public static void main(String[] args) {
SpringApplication.run(ConsulOrderMain80.class,args);
}
}
public interface LoadBalancerService {
//根据访问策略获取当前应该访问的服务器对象信息
public ServiceInstance instance(List<ServiceInstance> serviceInstances);
}
@Service
public class LoadBalancerServiceImpl implements LoadBalancerService {
//声明当前服务对象的访问次数计数器,初始值:0
private AtomicInteger atomicInteger = new AtomicInteger(0);
//获取已经访问的次数,+1为当前访问次数
public final int getAndIncrement(){
int current; //当前已访问次数
int next; //这次是第几次
do {
//初始化当前次数
current = atomicInteger.get();
//设置当前访问次数,如果超出int的最大值,就从0重新开始计数
next = current >= Integer.MAX_VALUE ? 0 : current + 1;
}while (!this.atomicInteger.compareAndSet(current,next));
//将current(期望值)和atomicInteger(当前实际值)进行比较,如果atomicInteger == current,那么将next(新值)更新到atomicInteger中重新记录
//更新后该方法会返回true,未更新返回false
System.out.println("第" + next +"次访问");
return next;
}
@Override
public ServiceInstance instance(List<ServiceInstance> serviceInstances) {
//轮询策略】
int index = getAndIncrement() % serviceInstances.size();
ServiceInstance serviceInstance = serviceInstances.get(index);
return serviceInstance;
}
}
//引入自定义负载均衡类
@Resource
private LoadBalancerService loadBalancerService;
@Resource
private DiscoveryClient discoveryClient;
@GetMapping("/consumer/payment/get/{id}")
public CommonResult<Payment> getPaymentById(@PathVariable("id") Long id){
//获取服务器列表
List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");
if(instances == null || instances.size() <= 0){
return null;
}
ServiceInstance instance = loadBalancerService.instance(instances);
//获取uri
URI uri = instance.getUri();
return restTemplate.getForObject(uri + "/payment/get/" + id,CommonResult.class);
}