Spring Cloud-Ribbon 负载均衡

 之前我们利用Eureka注册服务提供者和消费者,达到了通信的目的,Spring Cloud提供了一个Ribbon的服务,是一个负载均衡客户端,很好的嵌入在其中。

1.Ribbon
1.1 什么是Ribbon

 ribbon是一个负载均衡客户端 类似nginx反向代理,可以很好的控制http和tcp的一些行为。Feign默认集成了ribbon。

1.2 Ribbon工作机制
  1. 第一步有限选择Eureka Server,它优先选择在同一个Zone且负载较少的Server
  2. 第二步在根据用户指定的策略,在从Server取到的服务注册列表中选择一个地址。其中Ribbon提供了多重策略,例如轮询round robin、随机Random、根据相应时间加权等。

 在Riibon中一个非常重要的组件为LoadBalancerClient,它作为负载均衡的一个客户端。它在spring-cloud-commons下的LoadBalancerClient是一个接口,它继承ServiceInstanceChooser,它的实现类是RibbonLoadBalancerClient
 关于Ribbon这位博友写的部分东西比较详细,可以学习深入理解Ribbon之源码解析

2.Ribbon的应用

 首先我们讲之前服务提供者provider-user新增一个区别不同服务的接口

UserController.java
package com.ithzk.spring.cloud.controller;

import com.ithzk.spring.cloud.entity.User;
import com.netflix.appinfo.InstanceInfo;
import com.netflix.discovery.EurekaClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author hzk
 * @date 2018/5/13
 */
@RestController("/")
public class UserController {

    @Autowired
    private EurekaClient eurekaClient;

    @GetMapping("/user/{id}")
    public User getUser(@PathVariable Integer id){
        return new User(id,"zs",20);
    }

    @GetMapping("/eureka/info")
    public String info(){
        InstanceInfo instanceInfo = eurekaClient.getNextServerFromEureka("PROVIDER-USER", false);
        return instanceInfo.getHomePageUrl()+ ":" +instanceInfo.getPort();
    }
}

第一个服务提供者配置:
application.yml
server:
  port: 7900 #程序启动后的端口,也就是tomcat的端口,可自定义
spring:
 application:
   name: provider-user
eureka:
  client:
    service-url:
      defaultZone:  http://user:user@localhost:8888/eureka/
第二个服务提供者配置:
application.yml
server:
  port: 7901 #程序启动后的端口,也就是tomcat的端口,可自定义
spring:
 application:
   name: provider-user
eureka:
  client:
    service-url:
      defaultZone:  http://user:user@localhost:8888/eureka/

 将Eureka-server 和服务提供者启动,此时同一个名为provider-user的服务会存在两个提供入口,然后我们修改一些代码从而开启Ribbon。在服务消费者这边,首先启动时需要加上一个注解@RibbonClient(“PROVIDER-USER”)标识哪个服务需要开启负载均衡,然后在获取模板操作方法上添加@LoadBalanced使之支持负载均衡

OrderRibbonApp .java
package com.ithzk.spring.cloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.ribbon.RibbonClient;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

/**
 * Hello world!
 *
 */
@SpringBootApplication
@EnableEurekaClient
@RibbonClient("PROVIDER-USER")//启用ribbon对provider-user进行负载均衡
public class OrderRibbonApp {

    //相当于xml中的bean标签 用于调用当前方法获取到指定的对象
    @Bean
    @LoadBalanced
    public RestTemplate getRestTemplate(){
        return new RestTemplate();
    }

    public static void main( String[] args )
    {
        SpringApplication.run(OrderRibbonApp.class);
    }
}

 然后我们新增一个方法便于之后测试负载均衡是否生效,此时ribbon默认使用轮询方法,可自定义配置,之后会介绍

OrderController.java
package com.ithzk.spring.cloud.controller;

import com.ithzk.spring.cloud.entity.User;
import com.netflix.appinfo.InstanceInfo;
import com.netflix.discovery.EurekaClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.netflix.ribbon.RibbonLoadBalancerClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

/**
 * @author hzk
 * @date 2018/5/13
 */
@RestController
public class OrderController {

    //spring 提供用于访问rest接口的模板对象
    @Autowired
    private RestTemplate restTemplate;

    @Autowired
    private EurekaClient eurekaClient;

    @Value("${user.url}")
    private String url;

    @GetMapping("/order/{id}")
    public User getOrder(@PathVariable Integer id){
        //访问提供者 获取数据 通过rest访问获取的json数据转换为的User对象
        //PROVIDER-USER 为eureka中提供者注册服务名称
        InstanceInfo instanceInfo = eurekaClient.getNextServerFromEureka("PROVIDER-USER", false);
        //获取接口项目地址
        String homePageUrl = instanceInfo.getHomePageUrl();

        User user = restTemplate.getForObject(homePageUrl+"/user/" +id, User.class);
        return user;
    }

    @GetMapping("/eureka/info")
    public String getInfo(){
        String portInfo = restTemplate.getForObject("http://PROVIDER-USER/eureka/info", String.class);
        return portInfo;
    }

}

 然后启动消费,此时可以看到Eureka-server上同一个服务名称下面挂了两个接口
这里写图片描述
 然后我们多次访问消费接口,通过消费接口内部实现的通信检查使用了哪个服务
这里写图片描述
这里写图片描述
 不难看出规律,此时未配置策略时默认为轮询方式,这样就实现了基本的负载均衡

3.自定义负载均衡算法

 在上面我们利用ribbon实现了负载均衡,我们观察到他默认的算法方式是轮询,那么这里我们介绍一下自定义负载均衡算法如何实现

首先我们需要自定义一个配置类
MyRibbonConfigure.java
package com.ithzk.spring.config;

import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.RandomRule;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * Ribbon负载均衡自定义算法配置
 * @author hzk
 * @date 2018/5/16
 */
@Configuration
public class MyRibbonConfigure {

    @Autowired
    IClientConfig iClientConfig;

    /**
     * 自定义负载均衡算法
     * @param config
     * @return
     */
    @Bean
    public IRule ribbonRule(IClientConfig config){
        //返回随机算法
        return new RandomRule();
    }

}

然后将主程序注解添加一个参数指定自定义算法
OrderRibbonApp.java
package com.ithzk.spring.cloud;

import com.ithzk.spring.config.MyRibbonConfigure;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.ribbon.RibbonClient;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

/**
 * Hello world!
 *
 */
@SpringBootApplication
@EnableEurekaClient
@RibbonClient(name = "PROVIDER-USER",configuration = MyRibbonConfigure.class)//启用ribbon对provider-user进行负载均衡
public class OrderRibbonApp {

    //相当于xml中的bean标签 用于调用当前方法获取到指定的对象
    @Bean
    @LoadBalanced
    public RestTemplate getRestTemplate(){
        return new RestTemplate();
    }

    public static void main( String[] args )
    {
        SpringApplication.run(OrderRibbonApp.class);
    }
}

 此时启动消费者,进行如同之前的操作,多次消费可以发现每次的提供者都为随机的,这样实现了一个简单的自定义算法配置。之后有机会会对其中IRule接口和自定义算法进行详细研究再做补充。
  ps:在这其中部分同学可能会出现一个问题,自定义配置之后无法启动项目,系统会抛出

Description:
Field iClientConfig in com.ithzk.spring.cloud.config.MyRibbonConfigure required a bean of type 'com.netflix.client.config.IClientConfig' that could not be found.

Action:
Consider defining a bean of type 'com.netflix.client.config.IClientConfig' in your configuration.

 对此官网也有解释说如果该配置放在主程序同包目录下会发生未知异常,这是由于Spring boot的主程序会去扫描同一个层级目录下的文件,这里有两种办法可以解决
 第一种:不将自定义配置类放在主程序同包或者其子包中
这里写图片描述
 第二种可以添加一些注解避免主程序扫描到自定义配置类,这里可以参考这位博友的文章Ribbon自定义配置报错

3.多个服务自定义算法(自定义算法类实现)

 首先做个简单的测试,启动四个服务提供者,每两个提供者挂载同一个服务名下面

第一个服务提供者
application.yml
server:
  port: 7900 #程序启动后的端口,也就是tomcat的端口,可自定义
spring:
 application:
   name: provider-user
eureka:
  client:
    service-url:
      defaultZone:  http://user:user@localhost:8888/eureka/
UserController.java
package com.ithzk.spring.cloud.controller;

import com.ithzk.spring.cloud.entity.User;
import com.netflix.discovery.EurekaClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author hzk
 * @date 2018/5/13
 */
@RestController("/")
public class UserController {

    @Autowired
    private EurekaClient eurekaClient;

    @Value("${server.port}")
    private String port;

    @GetMapping("/user/{id}")
    public User getUser(@PathVariable Integer id){
        return new User(id,"zs",20);
    }

    @GetMapping("/eureka/info")
    public String info(){
        //InstanceInfo instanceInfo = eurekaClient.getNextServerFromEureka("PROVIDER-USER", false);
        //return instanceInfo.getHomePageUrl()+ ":" +instanceInfo.getPort();
        return port;
    }
}

第二个服务提供者
application.yml
server:
  port: 7901 #程序启动后的端口,也就是tomcat的端口,可自定义
spring:
 application:
   name: provider-user
eureka:
  client:
    service-url:
      defaultZone:  http://user:user@localhost:8888/eureka/
第三个服务提供者
application.yml
server:
  port: 7902 #程序启动后的端口,也就是tomcat的端口,可自定义
spring:
 application:
   name: provider-user1
eureka:
  client:
    service-url:
      defaultZone:  http://user:user@localhost:8888/eureka/
第四个服务提供者
application.yml
server:
  port: 7903 #程序启动后的端口,也就是tomcat的端口,可自定义
spring:
 application:
   name: provider-user1
eureka:
  client:
    service-url:
      defaultZone:  http://user:user@localhost:8888/eureka/

 启动这四个服务,消费服务和之前一样,只修改接口获取数据区分两个服务名称负载均衡算法

OrderController .java
package com.ithzk.spring.cloud.controller;

import com.ithzk.spring.cloud.entity.User;
import com.netflix.appinfo.InstanceInfo;
import com.netflix.discovery.EurekaClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.netflix.ribbon.RibbonLoadBalancerClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

/**
 * @author hzk
 * @date 2018/5/13
 */
@RestController
public class OrderController {

    //spring 提供用于访问rest接口的模板对象
    @Autowired
    private RestTemplate restTemplate;

    @Autowired
    private EurekaClient eurekaClient;

    @Value("${user.url}")
    private String url;

    @GetMapping("/order/{id}")
    public User getOrder(@PathVariable Integer id){
        //访问提供者 获取数据 通过rest访问获取的json数据转换为的User对象
        //PROVIDER-USER 为eureka中提供者注册服务名称
        InstanceInfo instanceInfo = eurekaClient.getNextServerFromEureka("PROVIDER-USER", false);
        //获取接口项目地址
        String homePageUrl = instanceInfo.getHomePageUrl();

        User user = restTemplate.getForObject(homePageUrl+"/user/" +id, User.class);
        return user;
    }

    @GetMapping("/eureka/info")
    public String getInfo(){
        String portInfo = restTemplate.getForObject("http://PROVIDER-USER/eureka/info", String.class);
        String portInfo1 = restTemplate.getForObject("http://PROVIDER-USER1/eureka/info", String.class);
        System.out.println("PROVIDER-USER:"+portInfo+"==================>PROVIDER-USER1:"+portInfo1);
        return portInfo;
    }

}

 同样多次请求消费服务,结果如下:
这里写图片描述
 可以看出配置了自定义负载均衡算法的PROVIDER-USER仍然是随机访问,而未配置自定义算法的PROVIDER-USER1默认轮询

4.多个服务自定义算法(配置文件实现)

 上面讲到我们可以通过自定义一个配置类去实现负载均衡算法的自定义设置,也可以通过配置文件实现,这里我们将上面的PROVIDER-USER1也变为随机访问

application.yml
server:
  port: 8900
spring:
  application:
    name: consumer-order-ribbon
user:
  url: http://localhost:7900/user/
eureka:
  client:
    service-url:
      defaultZone:  http://user:user@localhost:8888/eureka/
  instance:
    prefer-ip-address: true # 在Eureka中显示IP

PROVIDER-USER1:
  ribbon:
    NFLoadBalacerRuleClassName: com.netflix.loadbalancer.RandomRule #给指定服务配置负载均衡算法

这里写图片描述

 这样就实现了负载均衡算法的配置,相对还是很简单的
 ps: 配置文件中还有一个配置可以禁用ribbon

ribbon:
  eureka:
    enabled: false #在Eureka中禁用riibon,禁用后需要自己负责负载均衡
Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐