Skip to content

服务负载均衡

负载均衡作用就是将用户的请求平摊的分配到多个服务上,从而达到系统的HA(高可用),常见的负载均衡有软件Nginx,LVS,硬件F5等。

1. 负载均衡之Ribbon

Spring Cloud Ribbon是基于Netflix Ribbon实现的一套在客户端负载均衡的工具。主要功能是提供客户端的软件负载均衡算法和服务调用。Ribbon客户端组件提供一系列完善的配置项如连接超时,重试等。Ribbon会自动的帮助你基于某种规则(如简单轮询,随机连接等)去连接这些机器。 目前Ribbon项目处于停滞状态,SpringCloud组织基于Ribbon升级开发出Spring Cloud LoadBalancer作为替换解决方案。

2. 负载均衡之LoadBalancer

Spring Cloud LoadBalancer是由SpringCloud官方提供的一个开源的、简单易用的客户端负载均衡器,它包含在SpringCloud-commons中用它来替换了以前的Ribbon组件。相比较于Ribbon,SpringCloud LoadBalancer不仅能够支持RestTemplate,还支持WebClient(WeClient是Spring Web Flux中提供的功能,可以实现响应式异步请求)。

loadbalancer本地负载均衡客户端和Nginx服务端负载均衡区别

Nginx是服务器负载均衡,客户端所有请求都会交给Nginx,然后由Nginx实现转发请求,即负载均衡是由服务端实现的。
loadbalancer本地负载均衡,在调用微服务接口时候,会在注册中心上获取注册信息服务列表之后缓存到JVM本地,从而在本地实现RPC远程服务调用技术。

3. LoadBalancer工作原理

Alt text LoadBalancer在工作时分成两步:
第一步,先选择ConsulServer从服务端查询并拉取服务列表,知道了它有多个服务(上图3个服务),这3个实现是完全一样的,默认轮询调用谁都可以正常执行。类似生活中求医挂号,某个科室今日出诊的全部医生,客户端你自己选一个。
第二步,按照指定的负载均衡策略从server取到的服务注册列表中由客户端自己选择一个地址,所以LoadBalancer是一个客户端的负载均衡器。

4. LoadBalancer演练

4.1 添加多个微服务

  1. 复制cloud-provider-payment8001为cloud-provider-payment8002工程,实现提供支付服务有两个:
    Alt text
  2. 为了做区分,将cloud-provider-payment8001中的启动类改名为PaymentMain8001.java, 将cloud-provider-payment8002中的启动类改名为PaymentMain8002.java,修改cloud-provider-payment8002工程服务器端口为8002:
    Alt text
  3. 分别在cloud-provider-payment8001和cloud-provider-payment8002工程的PayController中添加如下代码:
java
@Value("${server.port}")
String port;
@Value("${spring.application.name}")
String appName;

@GetMapping(value = "/pay/serviceInfo")
public ResultData getServiceInfo(){
    return ResultData.success("当前service为:"+appName + ", 端口为:"+port);
}
  1. 在cloud-consumer-order80工程中的OrderController中添加如下代码:
java
@GetMapping("/consumer/pay/serverInfo")
public ResultData getServerInfo(){
    return restTemplate.getForObject(PaymentSrv_URL + "/pay/serviceInfo", ResultData.class);
}
  1. 启动Consul, 向Consul注册8001、8002服务:
    Alt text

4.2 客户端添加LoadBanlencer

cloud-consumer-order80微服务中添加loadbalancer依赖:

xml
<!--loadbalancer-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>

4.3 测试负载均衡

使用apifox请求接口http://localhost/consumer/pay/Info,发现结果中端口交替变化:
Alt text 为何调用后端微服务,会出现轮询执行呢?
首先他的默认负载均衡算法是轮询,轮询是这样实现的:rest接口第几次请求数%服务器集群总数量=实际调用服务器位置下标,每次服务重启动后rest接口计数从1开始。
具体服务器集群数量是通过使用DiscoveryClient对象调用得到, 在OrderController中添加如下代码:

java
@Resource
DiscoveryClient discoveryClient;

/**
 * 获取所有服务列表信息
 */
@GetMapping("/consumer/pay/serviceList")
public void getServiceList() {
    // 获取所有的服务名称上线列表
    List<String> serviceIds = discoveryClient.getServices();
    serviceIds.forEach(serviceId -> {
        // 根据服务名获取服务实例列表
        List<ServiceInstance> instances = discoveryClient.getInstances(serviceId);
        instances.forEach(instance -> {
            log.info("instanceId: " + instance.getInstanceId() + " ,serviceId: " + serviceId + " ,url: " + instance.getUri());
        });
    });
}

5. LoadBanlencer负载算法

5.1 LoadBanlencer负载算法分类

LoadBanlencer负载算法对应的接口是ReactiveLoadBalancer,目前提供的有以下两种算法:

  1. 轮询:实现类RoundRobinLoadBalancer
  2. 随机:实现类RandomLoadBalancer

5.2 算法切换

默认为轮询算法,切换为随机算法,修改RestTemplateConfig:

java
@Configuration
@LoadBalancerClient(
    //下面的value值大小写一定要和consul里面的名字一样,必须一样
    value = "cloud-payment-service",configuration = RestTemplateConfig.class)
public class RestTemplateConfig {

    @Bean
    //使用 @LoadBalanced注解赋予RestTemplate负载均衡的能力
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
    @Bean
    ReactorLoadBalancer<ServiceInstance> randomLoadBalancer(Environment environment,
                                                            LoadBalancerClientFactory loadBalancerClientFactory) {
        String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);

        return new RandomLoadBalancer(loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), name);
    }
}

6. 负载均衡之OpenFeign

6.1 OpenFeign概述

Feign是一个声明性web服务客户端。它使编写web服务客户端变得更容易。使用Feign创建一个接口并对其进行注释。它具有可插入的注释支持,包括Feign注释和JAX-RS注释。Feign还支持可插拔编码器和解码器。OpenFeign基本上就是当前微服务之间调用的事实标准。

6.2 功能特性

  1. 可插拔的注解支持,包括Feign注解和JAX-RS注解
  2. 支持可插拔的HTTP编码器和解码器
  3. 支持Sentinel和它的Fallback
  4. 支持SpringCloudLoadBalancer的负载均衡
  5. 支持HTTP请求和响应的压缩

6.3 OpenFeign和RestTemplate的区别

  1. 前面在使用SpringCloud LoadBalancer+RestTemplate时,利用RestTemplate对http请求的封装处理形成了一套模版化的调用方法。但是在实际开发中,往往一个接口会被多处调用,导致很多微服务有类似重复的代码,OpenFeign在此基础上做了进一步封装简洁,使用@FeignClient注解即可完成,对服务提供方的接口绑定,统一对外暴露可以被调用的接口方法,大大简化和降低了调用客户端的开发量。
  2. OpenFeign同时还集成SpringCloud LoadBalancer,与SpringCloud LoadBalancer不同的是,通过OpenFeign只需要定义服务绑定接口且以声明式的方法,优雅而简单的实现了服务调用。同时可以集成阿里巴巴Sentinel来提供熔断、降级等功能,功能更加完善。

7. 基于OpenFeign架构调整

订单模块要去调用支付模块,订单和支付两个微服务,需要通过Api接口解耦,一般不要在订单模块写非订单相关的业务, 自己的业务自己做+其它模块走FeignApi接口调用,先调整如下:
Alt text

7.1 创建cloud-consumer-feign-order80

  1. 为了和以前order服务区分,拷贝cloud-consumer-order80为cloud-consumer-feign-order80,然后修改pom.xml中artifactId为cloud-consumer-feign-order80,在父工程springcloud-base中添加模块cloud-consumer-feign-order80:
    Alt text
  2. 添加pom.xml中的依赖:
xml
<!--openfeign-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
  1. 调整application.yml,修改服务名
yml
server:
  port: 80

spring:
  application:
    name: cloud-consumer-openfeign-order
  ####Spring Cloud Consul for Service Discovery
  cloud:
    consul:
      host: localhost
      port: 8500
      discovery:
        prefer-ip-address: true #优先使用服务ip进行注册
        service-name: ${spring.application.name}
  1. 修改启动类,开启OpenFeign功能
java
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients(basePackages = "com.rocket.springcloud.common")
public class OrderOpenFeignMain {
    public static void main(String[] args) {
        SpringApplication.run(OrderOpenFeignMain.class, args);
    }
}

@EnableFeignClients主要用来扫描带有@FeiginClient注解的接口,如果相关接口在jar包中,需要配置扫描的包名。

7.2 修改cloud-api-commons模块

  1. 引入openFeign依赖
xml
<!--openfeign-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
  1. 创建PayFeignApi接口
    在cloud-api-commons模块中创建apis包,新建接口PayFeignApi:
java
package com.rocket.springcloud.common.apis;

import com.rocket.springcloud.common.dto.PayDTO;
import com.rocket.springcloud.common.resp.ResultData;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;

@FeignClient(value="cloud-payment-service")
public interface PayFeignApi {

    @PostMapping("/pay/add")
    public ResultData addOrder(@RequestBody PayDTO payDTO);

    @GetMapping("/pay/get/{id}")
    public ResultData getPayInfo(@PathVariable("id") Integer id);

    @GetMapping(value = "/pay/serviceInfo")
    public ResultData getServiceInfo();
}

7.3 删除部分代码和LoadBalance不相关特性

  1. 删除RestTemplateConfig类
  2. 重写OrderController类
java
@RestController
@Slf4j
public class OrderController {

    @Resource
    private PayFeignApi payFeignApi;

    @PostMapping("/consumer/pay/add")
    public ResultData addOrder(@RequestBody PayDTO payDTO) {
        return payFeignApi.addOrder(payDTO);
    }

    @GetMapping("/consumer/pay/get/{id}")
    public ResultData getPayInfo(@PathVariable("id") Integer id) {
        return payFeignApi.getPayInfo(id);
    }

    // openfeign天然支持负载均衡演示
    @GetMapping(value = "/consumer/pay/serverInfo")
    public ResultData getServerInfo() {
        return payFeignApi.getServiceInfo();
    }
}

7.4 测试基于OpenFeigin服务

启动Consul,查看目前所有的服务:
Alt text 使用apifox调用接口测试:
Alt text

8. OpenFeign高级特性

8.1 OpenFeign超时控制

在Spring Cloud微服务架构中,大部分公司都是利用OpenFeign进行服务间的调用,而比较简单的业务使用默认配置是不会有多大问题的,但是如果是业务比较复杂,服务要进行比较繁杂的业务计算,那后台很有可能会出现Read Timeout这个异常,因此定制化配置超时时间就有必要了。
OpenFeign提供超时时间的选项有两个:

  • connectTimeout: 连接超时时间
  • readTimeout: 请求处理超时时间,默认为60000ms
  1. 全局配置
    修改cloud-consumer-feign-order80工程,自定义全局超时时间:
yml
server:
  port: 80
spring:
  application:
    name: cloud-consumer-order
  cloud:
    consul:
      host: localhost
      port: 8500
      discovery:
        prefer-ip-address: true #优先使用服务ip进行注册
        service-name: ${spring.application.name}
    openfeign:
      client:
        config:
          default:
            connectTimeout: 3000
            #读取超时时间
            readTimeout: 3000
  1. 局部接口配置, 不再使用default而是具体微服务名
yml
server:
  port: 80
spring:
  application:
    name: cloud-consumer-order
  cloud:
    consul:
      host: localhost
      port: 8500
      discovery:
        prefer-ip-address: true #优先使用服务ip进行注册
        service-name: ${spring.application.name}
    openfeign:
      client:
        config:
          # default
          cloud-consumer-order:
            connectTimeout: 3000
            #读取超时时间
            readTimeout: 3000

如果同时配置了全局和局部的微服务超时,局部配置的超时时间优先级高。

8.2 OpenFeign重试机制

  1. 默认OpenFeign没有开启重试机制,开启重试的话,创建config包,新建FeignConfig类:
java
package com.rocket.springcloud.consumer.config;

import feign.Retryer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class FeignConfig {
    @Bean
    public Retryer myRetryer() {
        //return Retryer.NEVER_RETRY; //Feign默认配置是不走重试策略的
        //最大请求次数为3(请求1次+重试2次),初始间隔时间为100ms,重试间最大间隔时间为5s
        return new Retryer.Default(100, 5, 3);
    }
}
  1. 测试代码,修改OrderController,使用getServerInfo()方法做测试,记录开始和结束时间:
java
@GetMapping(value = "/consumer/pay/serverInfo")
public ResultData getServerInfo() {
    log.info("===start==="+DateUtil.now());
    try{
        ResultData serviceInfo = payFeignApi.getServiceInfo();
        return serviceInfo;
    }catch(Exception e){
        log.error(e.getMessage());
        log.info("===end==="+DateUtil.now());
    }
    return ResultData.fail("500", "报错了");
}
  1. 调用查看日志:
    Alt text

8.3 修改OpenFeign默认HttpClient

如果不做特殊配置,OpenFeign默认使用JDK自带的HttpURLConnection发送HTTP请求,由于默认HttpURLConnection没有连接池、性能和效率比较低,OpenFeign支持自定义HttpClient。 比如之前重试报错日志可以看到默认的HttpClient:
Alt text

  1. 添加apache httpclient5的依赖:
xml
<!--  httpclient5-->
<dependency>
    <groupId>org.apache.httpcomponents.client5</groupId>
    <artifactId>httpclient5</artifactId>
    <version>5.4</version>
</dependency>
<!-- feign-hc5-->
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-hc5</artifactId>
    <version>13.3</version>
</dependency>
  1. 开启hc5, 修改application.yml配置
yml
#  Apache HttpClient5 配置开启
spring:
  cloud:
    openfeign:
      httpclient:
        hc5:
          enabled: true
  1. 测试,发现报错信息已经变化:
    Alt text

8.4 OpenFeign请求/响应压缩

Spring Cloud OpenFeign支持对请求和响应进行GZIP压缩,以减少通信过程中的性能损耗。

  1. 对请求和响应进行GZIP压缩
ini
# 通过下面的两个参数设置,就能开启请求与相应的压缩功能:
spring.cloud.openfeign.compression.request.enabled=true
spring.cloud.openfeign.compression.response.enabled=true
  1. 细粒度化设置
ini
# 对请求压缩做一些更细致的设置,比如下面的配置内容指定压缩的请求数据类型并设置了请求压缩的大小下限,
# 只有超过这个大小的请求才会进行压缩
spring.cloud.openfeign.compression.request.enabled=true
spring.cloud.openfeign.compression.request.mime-types=text/xml,application/xml,application/json #触发压缩数据类型
spring.cloud.openfeign.compression.request.min-request-size=2048 #最小触发压缩的大小

8.5 OpenFeign日志打印

Feign 提供了日志打印功能,我们可以通过配置来调整日志级别,从而了解Feign中Http请求的细节。日志级别如下:

  • NONE:默认的,不显示任何日志;
  • BASIC:仅记录请求方法、URL、响应状态码及执行时间;
  • HEADERS:除了 BASIC 中定义的信息之外,还有请求和响应的头信息;
  • FULL:除了 HEADERS 中定义的信息之外,还有请求和响应的正文及元数据。
  1. 配置开启日志,修改FeignConfig类:
java
@Configuration
public class FeignConfig{
    @Bean
    public Retryer myRetryer(){
        return Retryer.NEVER_RETRY; //默认
    }

    @Bean
    Logger.Level feignLoggerLevel() {
        return Logger.Level.FULL;
    }
}
  1. 在application.yml配置具体哪些包需要打印日志:
yml
logging:
  level:
    com:
      rocket:
        springcloud:
          common:
            apis:
              PayFeignApi: debug
  1. 调用接口,查看日志打印:
    Alt text

8.5 OpenFeign整合Sentinel实现服务降级

见后续springcloud alibaba篇章