java框架之SpringCloud(4)-Ribbon&Feign负载均衡

上一章节已经学习了 Eureka 的使用,SpringCloud 也提供了基于 Eureka 负载均衡的两种方案:Ribbon 和 Feign。html

Ribbon负载均衡

介绍

SpringCloud Ribbon 是基于 Netflix Ribbon 实现的一套客户端负载均衡的工具。java

Ribbon 是 Netflix 发布的开源项目,主要功能是提供客户端的软件负载均衡算法,将 Netflix 的中间层服务链接在一块儿。Ribbon 客户端组件提供一系列完善的配置项如链接超时重试等。简单地说,就是在配置文件中列出 Load Balancer(简称 LB)后面全部的机器,Ribbon 会自动帮助你基于某种规则(如简单轮询、随机链接等)去链接这些机器。咱们也很容易使用 Ribbon 实现自定义的负载均衡算法。mysql

LB,即负载均衡(Load Balance),在微服务或分布式集群中常常用的一种应用。负载均衡简单地说就是将用户的请求平均的分配到多个服务上,从而达到系统的 HA。web

常见的负载均衡方案有软件 Nginx、LVS,硬件 F5 等。算法

而 LB 又能够分为如下两种:spring

  • 集中式 LB:在服务的消费方和提供方之间使用独立的 LB 设施(能够是硬件,如 F5,也能够是软件,如 Nginx),由该设施负责把访问请求经过某种策略转发至服务的提供方。
  • 进程内 LB:将 LB 逻辑集中到消费方,消费方从注册中心获知有哪些地址可用,而后本身再从这些地址中选择出一个合适的服务器。Ribbon 就属于进程内 LB,它只是一个类库,集成于消费方进程,消费方经过它来获取到服务提供方的地址。

使用

一、修改消费者服务工程,对应占用端口为 80,添加依赖以下:sql

<!--ribbon 相关-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-ribbon</artifactId>
</dependency>

二、修改配置将消费者服务工程做为 Eureka 客户端注册到 Eureka 集群:数据库

server:
  port: 80

eureka:
  client:
    service-url:
      defaultZone: http://www.eurekaserver1.com:7001/eureka,http://www.eurekaserver2.com:7002/eureka,http://www.eurekaserver3.com:7003/eureka
  instance:
    instance-id: microservicecloud-provider-dept
    prefer-ip-address: true # 访问路径显示 IP

spring:
  application:
    name: microservicecloud-consumer-dept
application.yml

三、启用 Eureka 客户端功能:服务器

package zze.springcloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

@SpringBootApplication
@EnableEurekaClient
public class Application_80 {

    public static void main(String[] args) {
        SpringApplication.run(Application_80.class, args);
    }
}
zze.springcloud.Application_80

四、修改配置类,注册 RestTemplate  bean 时添加注解启用负载均衡:mybatis

package zze.springcloud.cfgbeans;

import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
public class ConfigBean {
    @Bean
    @LoadBalanced // 客户端负载均衡
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}
zze.springcloud.cfgbeans.ConfigBean

五、修改 Controller,修改微服务具体访问 URL 为微服务名称:

package zze.springcloud.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;
import zze.springcloud.entities.Dept;

import java.util.List;

@RestController
@RequestMapping("/consumer/dept")
public class DeptController {
    // 微服务 Provider 的服务地址
    // private static String REST_URL_PREFIX = "http://localhost:8001";

    // 要调用的微服务名称 即微服务工程的 spring.application.name 对应值
    private static String REST_URL_PREFIX = "http://microservicecloud-provider-dept";
    @Autowired
    private RestTemplate restTemplate;

    @PostMapping("/add")
    public boolean add(@RequestBody Dept dept) {
        return restTemplate.postForObject(REST_URL_PREFIX + "/dept/add", dept, Boolean.class);
    }

    @GetMapping("/get/{id}")
    public Dept get(@PathVariable Long id) {
        return restTemplate.getForObject(REST_URL_PREFIX + "/dept/get/" + id, Dept.class);
    }

    @GetMapping("/list")
    public List<Dept> list() {
        return restTemplate.getForObject(REST_URL_PREFIX + "/dept/list", List.class);
    }
}
zze.springcloud.controller.DeptController

到这里其实已经完成了负载均衡的基本配置,依次启动项目是能够正常访问的,可是由于提供者服务只有一个,看不出负载均衡的效果。

六、新建两个数据库,分别名为 "springcloud_8002" 和 "springcloud_8003",接着分别在两个数据库中执行下面 sql 脚本初始化表结构及数据:

CREATE TABLE dept
(
  dept_no   BIGINT NOT NULL PRIMARY KEY AUTO_INCREMENT,
  dept_name VARCHAR(60),
  db_source VARCHAR(60)
);

INSERT INTO dept(dept_name, db_source)
VALUES ('开发部', DATABASE());
INSERT INTO dept(dept_name, db_source)
VALUES ('人事部', DATABASE());
INSERT INTO dept(dept_name, db_source)
VALUES ('财务部', DATABASE());
INSERT INTO dept(dept_name, db_source)
VALUES ('市场部', DATABASE());
INSERT INTO dept(dept_name, db_source)
VALUES ('运维部', DATABASE());

SELECT * FROM dept;
dept.sql

七、新建两个子工程做为 Provider 服务工程与 "microservicecloud-provider-dept-8001" 提供相同服务作 Provider 服务集群,分别名为 "microservicecloud-provider-dept-8002" 和 "microservicecloud-provider-dept-8003",三个工程链接不一样数据库,对应配置以下:

server:
  port: 8001

mybatis:
  config-location: classpath:mybatis/mybatis.cfg.xml # mybatis 配置文件路径
  type-aliases-package: zze.springcloud.entities # 全部 Entity 别名类所在包
  mapper-locations:
    - classpath:mybatis/mapper/**/*.xml # mapper 映射文件

spring:
  application:
    name: microservicecloud-provider-dept # 当前微服务名称
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource # 数据源操做类型
    driver-class-name: org.gjt.mm.mysql.Driver # mysql 驱动包
    url: jdbc:mysql:///springcloud_8001 # 数据库链接 root
    username: root
    password: root
    dbcp2:
      min-idle: 5 # 数据库链接池的最小维持链接数
      initial-size: 5 # 初始化链接数
      max-total: 5 # 最大链接数
      max-wait-millis: 200 # 等待链接获取的最大超时时间

eureka:
  client: # 将当前工程做为 Eureka 客户端
    service-url:
      # 单机版
      # defaultZone: http://localhost:7001/eureka # Eureka 服务端地址
      defaultZone: http://www.eurekaserver1.com:7001/eureka,http://www.eurekaserver2.com:7002/eureka,http://www.eurekaserver3.com:7003/eureka
  instance:
    instance-id: microservicecloud-provider-dept-8001
    prefer-ip-address: true # 访问路径显示 IP

info:
  host: ${java.rmi.server.hostname}
  port: ${server.port}
  app.name: microservicecloud-provider-dept-8001
  build.artifactId: ${project.artifactId}
  build.version: ${project.version}
application.yml#8001
server:
  port: 8002

mybatis:
  config-location: classpath:mybatis/mybatis.cfg.xml # mybatis 配置文件路径
  type-aliases-package: zze.springcloud.entities # 全部 Entity 别名类所在包
  mapper-locations:
    - classpath:mybatis/mapper/**/*.xml # mapper 映射文件

spring:
  application:
    name: microservicecloud-provider-dept # 当前微服务名称
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource # 数据源操做类型
    driver-class-name: org.gjt.mm.mysql.Driver # mysql 驱动包
    url: jdbc:mysql:///springcloud_8002 # 数据库链接 root
    username: root
    password: root
    dbcp2:
      min-idle: 5 # 数据库链接池的最小维持链接数
      initial-size: 5 # 初始化链接数
      max-total: 5 # 最大链接数
      max-wait-millis: 200 # 等待链接获取的最大超时时间

eureka:
  client: # 将当前工程做为 Eureka 客户端
    service-url:
      # 单机版
      # defaultZone: http://localhost:7001/eureka # Eureka 服务端地址
      defaultZone: http://www.eurekaserver1.com:7001/eureka,http://www.eurekaserver2.com:7002/eureka,http://www.eurekaserver3.com:7003/eureka
  instance:
    instance-id: microservicecloud-provider-dept-8002
    prefer-ip-address: true # 访问路径显示 IP

info:
  host: ${java.rmi.server.hostname}
  port: ${server.port}
  app.name: microservicecloud-provider-dept-8002
  build.artifactId: ${project.artifactId}
  build.version: ${project.version}
application.yml#8002
server:
  port: 8003

mybatis:
  config-location: classpath:mybatis/mybatis.cfg.xml # mybatis 配置文件路径
  type-aliases-package: zze.springcloud.entities # 全部 Entity 别名类所在包
  mapper-locations:
    - classpath:mybatis/mapper/**/*.xml # mapper 映射文件

spring:
  application:
    name: microservicecloud-provider-dept # 当前微服务名称
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource # 数据源操做类型
    driver-class-name: org.gjt.mm.mysql.Driver # mysql 驱动包
    url: jdbc:mysql:///springcloud_8003 # 数据库链接 root
    username: root
    password: root
    dbcp2:
      min-idle: 5 # 数据库链接池的最小维持链接数
      initial-size: 5 # 初始化链接数
      max-total: 5 # 最大链接数
      max-wait-millis: 200 # 等待链接获取的最大超时时间

eureka:
  client: # 将当前工程做为 Eureka 客户端
    service-url:
      # 单机版
      # defaultZone: http://localhost:7001/eureka # Eureka 服务端地址
      defaultZone: http://www.eurekaserver1.com:7001/eureka,http://www.eurekaserver2.com:7002/eureka,http://www.eurekaserver3.com:7003/eureka
  instance:
    instance-id: microservicecloud-provider-dept-8003
    prefer-ip-address: true # 访问路径显示 IP

info:
  host: ${java.rmi.server.hostname}
  port: ${server.port}
  app.name: microservicecloud-provider-dept-8003
  build.artifactId: ${project.artifactId}
  build.version: ${project.version}
application.yml#8003

八、测试:

先启动占用端口 700一、700二、7003 的 EurekaServer 服务,
再启动占用端口 800一、800二、8003 的 Provider 服务,
最后启动占用端口 80 的消费者服务,随便访问一个 EurekaServer 的 WebUI 页面:

会发现消费者服务与提供者服务都注册到了 EurekaServer,而且提供者服务有三个实例分别占用端口 800一、800二、8003,
屡次访问 http://localhost/consumer/dept/list,会发现每次访问返回的数据都是从不一样的数据库返回,即负载均衡环境搭建成功。
test

Ribbon 在工做时分为两步走:

        一、选择 EurekaServer,它优先选择在同一个区域内负载较少的 Server。

        二、根据用户指定的策略,从 EurekaServer 取到的服务注册列表中选择一个服务。

其中 Ribbon 提供了多种策略,例如轮询(默认)、随机和根据响应时间加权。

若测试机器内存不足,则能够只启动一部分服务测试,例如能够只启动一个 EurekaServer,由于在 EurekaServer 集群中每个 EurekaServer 都是平等的,还能够只启动两个提供者服务,能看到返回数据的不一样便可,内存没有 16G 有点 hold 不住。

核心组件IRule

由于 Ribbon 使用的是客户端负载均衡,因此下面的操做都是在占用 80 端口的消费者服务工程下。

默认提供的Rule

经过上面的使用测试会发现 Ribbon 默认的负载均衡策略是依次轮询访问每一个微服务,若是咱们须要修改它默认的负载均衡策略,则可使用 IRule 组件。

IRule:其实是一个接口,它的实现类须要可以根据特定算法从服务列表中选取一个要访问的服务,默认提供的实现有以下:

IRule
    AbstractLoadBalancerRule (com.netflix.loadbalancer)
        ClientConfigEnabledRoundRobinRule (com.netflix.loadbalancer)
            BestAvailableRule (com.netflix.loadbalancer) // 先过滤掉因为屡次访问故障而处于断路器跳闸状态的服务,而后选择一个并发量最小的服务
            PredicateBasedRule (com.netflix.loadbalancer)
                ZoneAvoidanceRule (com.netflix.loadbalancer) // 默认规则,综合判断 Server 所在区域的性能和 server 的可用性选择服务器
                AvailabilityFilteringRule (com.netflix.loadbalancer) // 先过滤掉因为屡次访问故障而处于断路器跳闸状态的服务,还有并发的连接数量超过阈值的服务,而后对剩余的服务按照轮询策略进行选择
        RoundRobinRule (com.netflix.loadbalancer) // 轮询
            WeightedResponseTimeRule (com.netflix.loadbalancer) // 根据平均响应时间计算全部服务的权重,响应时间越快服务权重越大,被选择的概率越高。刚启动的时候若是统计信息不足,则使用 RoundRobinRule,等统计信息完善时会自动切换到当前策略
            ResponseTimeWeightedRule (com.netflix.loadbalancer)
        RandomRule (com.netflix.loadbalancer) // 随机
        RetryRule (com.netflix.loadbalancer) // 先按照 RoundRobinRule 策略获取服务,若是获取服务失败则在指定时间内会重试,若是依旧失败则会获取其它可用的服务

若是要修改负载均衡策略,只须要将实现 IRule 接口的 bean 注册到 IoC 容器便可,如:

package zze.springcloud.cfgbeans;

import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.RandomRule;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
public class ConfigBean {
    @Bean
    @LoadBalanced // 客户端负载均衡
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }

    @Bean // 负载均衡策略使用随机访问
    public IRule randomRule(){
        return new RandomRule();
    }
}
zze.springcloud.cfgbeans.ConfigBean

自定义Rule

上面说过咱们只须要将实现 IRule 接口的 bean 注册到 IoC 容器规则就能够生效,因此咱们能够自定义一个 IRule 的实现类,好比要定制一个每一个服务依次访问 5 次的规则:

package zze.springcloud.cfgbeans;

import java.util.List;

import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.AbstractLoadBalancerRule;
import com.netflix.loadbalancer.ILoadBalancer;
import com.netflix.loadbalancer.Server;

/**
 * 参考 RandomRule 源码自定义 Rule,每一个服务访问 5 次
 */
public class MyRandomRule extends AbstractLoadBalancerRule
{
    private int total = 0;             // 总共被调用的次数,目前要求每台被调用5次
    private int currentIndex = 0;    // 当前提供服务的机器号

    public Server choose(ILoadBalancer lb, Object key)
    {
        if (lb == null) {
            return null;
        }
        Server server = null;

        while (server == null) {
            if (Thread.interrupted()) {
                return null;
            }
            List<Server> upList = lb.getReachableServers();
            List<Server> allList = lb.getAllServers();

            int serverCount = allList.size();
            if (serverCount == 0) {
                /*
                 * No servers. End regardless of pass, because subsequent passes only get more
                 * restrictive.
                 */
                return null;
            }

//            int index = rand.nextInt(serverCount);// java.util.Random().nextInt(3);
//            server = upList.get(index);

            
//            private int total = 0;             // 总共被调用的次数,目前要求每台被调用5次
//            private int currentIndex = 0;    // 当前提供服务的机器号
            if(total < 5)
            {
                server = upList.get(currentIndex);
                total++;
            }else {
                total = 0;
                currentIndex++;
                if(currentIndex >= upList.size())
                {
                  currentIndex = 0;
                }
            }            
            
            
            if (server == null) {
                /*
                 * The only time this should happen is if the server list were somehow trimmed.
                 * This is a transient condition. Retry after yielding.
                 */
                Thread.yield();
                continue;
            }

            if (server.isAlive()) {
                return (server);
            }

            // Shouldn't actually happen.. but must be transient or a bug.
            server = null;
            Thread.yield();
        }

        return server;

    }

    @Override
    public Server choose(Object key)
    {
        return choose(getLoadBalancer(), key);
    }

    @Override
    public void initWithNiwsConfig(IClientConfig clientConfig)
    {
        // TODO Auto-generated method stub

    }
}
zze.springcloud.cfgbeans.MyRandomRule

除了将 IRule 实例直接注册到 IoC 容器这种方式,咱们还能够自定义一个规则配置类:

package zze.config;

import org.springframework.context.annotation.Bean;

import com.netflix.loadbalancer.IRule;
import org.springframework.context.annotation.Configuration;
import zze.springcloud.cfgbeans.MyRandomRule;

/*
注意,官方文档表示该类要定义在 @ComponentScan 注解扫描范围以外
 */
@Configuration
public class MySelfRuleCofnig
{
    @Bean
    public IRule myRule()
    {
        return new MyRandomRule();
    }
}
zze.config.MySelfRuleCofnig

接下来能够经过在主启动类上经过指定注解让配置类对指定服务生效:

package zze.springcloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.ribbon.RibbonClient;
import zze.config.MySelfRuleCofnig;

@SpringBootApplication
@EnableEurekaClient
@RibbonClient(name = "MICROSERVICECLOUD-PROVIDER-DEPT",configuration = MySelfRuleCofnig.class) // 针对 MICROSERVICECLOUD-PROVIDER-DEPT 服务实例采用的负载均衡策略配置类为 MySelfRuleCofnig
public class Application_80 {

    public static void main(String[] args) {
        SpringApplication.run(Application_80.class, args);
    }
}
zze.springcloud.Application_80

Feign负载均衡

介绍

Feign 是一个声明式 WebService 客户端。使用 Feign 可以让编写 WebService 客户端更加简单,它的使用方法是定义一个接口,而后在上面添加注解,同时也支持 JAX-RS 标准的注解。Feign 也支持可拔插式的编码器和解码器。SpringCloud 对 Feign 进行了封装,使其支持了 SpringMVC 标准注解和 HttpMessageConverters。Feign 能够与 Eureka 和 Ribbon 组合使用以支持负载均衡。

Feign 能干什么?

前面在使用 Ribbon+RestTemplate,利用 RestTemplate 对 httpClient 封装,造成了一套模板化的调用方法。可是在实际开发中,因为对服务依赖的调用可能不止一处,每每一个接口会被多处调用,因此一般都会针对每一个微服务自行封装一些客户端类来包装这些依赖服务的调用。而 Feign 就是在这个基础上作了进一步封装,由他来帮助咱们实现依赖服务接口。在 Feign 的实现下,咱们只须要建立一个接口并使用注解的方式来配置它(相似于 MyBatis 的 Mapper 类使用 @Mapper 注解),便可完成对服务提供方的接口绑定,简化了使用 SpringCloud Ribbon 时,自动封装服务调用客户端的开发量。也能够说 Feign 就是对 Ribbon 的进一步封装的实现。

使用

一、复制 "microservicecloud-consumer-dept-80" 子工程更名为 "microservicecloud-consumer-dept-feign",新增依赖:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-feign</artifactId>
</dependency>

二、新建接口,使用 Feign 提供的注解标识该接口为 Feign 客户端:

package zze.springcloud.service;

import org.springframework.cloud.netflix.feign.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.RequestMapping;
import zze.springcloud.entities.Dept;

import java.util.List;

@FeignClient(value = "microservicecloud-provider-dept") // 标识调用哪一个微服务
@RequestMapping("/dept")
public interface DeptClientService {
    @GetMapping("/get/{id}")
    public Dept get(@PathVariable Long id);

    @GetMapping("/list")
    public List<Dept> list();

    @PostMapping("/add")
    public boolean add(Dept dept);
}
zze.springcloud.service.DeptClientService

三、修改主启动类,使用注解启用 Feign:

package zze.springcloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.feign.EnableFeignClients;

@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients(basePackages = {"zze.springcloud.service"}) // 指定 Feign 客户端的扫描包
public class Application_80 {

    public static void main(String[] args) {
        SpringApplication.run(Application_80.class, args);
    }
}
zze.springcloud.Application_80

四、修改 Controller,使用 Feign 客户端:

package zze.springcloud.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;
import zze.springcloud.entities.Dept;
import zze.springcloud.service.DeptClientService;

import java.util.List;

@RestController
@RequestMapping("/consumer/dept")
public class DeptController {

    @Autowired
    private DeptClientService deptService;

    @PostMapping("/add")
    public boolean add(@RequestBody Dept dept) {
        return deptService.add(dept);
    }

    @GetMapping("/get/{id}")
    public Dept get(@PathVariable Long id) {
        return deptService.get(id);
    }

    @GetMapping("/list")
    public List<Dept> list() {
        return deptService.list();
    }
}
zze.springcloud.controller.DeptController

五、测试:

一、启动 Eureka 集群 700一、700二、7003
二、启动 Provider 集群 800一、800二、8003
三、启动 microservicecloud-consumer-dept-feign,访问 http://localhost/consumer/dept/get/1,返回结果,成功。

test
Feign 集成了 Ribbon,利用 Ribbon 维护了 microservicecloud-provider-dept 服务的信息,而且经过轮询实现了客户端的负载均衡。与 Ribbon 不一样的是,Feign 无需手动使用 RestTemplate 构建请求,只须要以声明式方法定义服务绑定接口,优雅而简单的实现了服务调用。
相关文章
相关标签/搜索