首页 > 技术文章 > SpringCloud笔记四:Ribbon

yunquan 2019-04-11 16:24 原文

什么是Ribbon?

Ribbon是一个客户端的负载均衡。

我举个例子就明白了,我去超市买东西付钱,收银台有3个,一个收银台有10个人排队,一个收银台有5个人排队,一个收银台有2个人排队。只要我不是傻子,我就知道我该去2个人排队的那个收银台。

我是客户,我知道选择人最少的收银台。这就是客户端的负载均衡。这就是Ribbon

提一句,负载均衡有两种方式:

  1. 集中式:这个就是有一个硬件,比如F5,提供者和消费者之间通过硬件负载均衡,缺点是硬件一般都很贵
  2. 进程内:这个是软件的形式,把负载均衡的逻辑放到消费方,让消费方根据逻辑去选择一台合适的服务器。

Ribbon的配置

Maven引入

由于Ribbon是客户端的负载均衡,我们在consumer项目里面引入Maven配置

       <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-ribbon</artifactId>
            <version>1.4.6.RELEASE</version>
        </dependency>

开启注解

注解这有两个地方需要标注,我们的consumer是使用restTemplate来访问的,在获取restTemplate的方法上开启负载均衡注解,@LoadBalanced

   @Bean
    @LoadBalanced
    public RestTemplate getRestTemplate()
    {
        return new RestTemplate();
    }

顺便,consumer里面的Controller里面,访问的地址我们原来写的是localhost,既然是微服务,我们肯定使用服务名称了,改一下

//    private static final String REST_URL_PREFIX="http://localhost:8001";
    private static final String REST_URL_PREFIX="http://PROVIDER-DEPT";

第二个注解就是主方法那里,加一个@EnableEurekaClient

@SpringBootApplication
@EnableEurekaClient
public class Consumer80Application {
    public static void main(String[] args) {
        SpringApplication.run(Consumer80Application.class, args);
    }
}

再次重启启动eureka7001,eureka7002,eureka7003和provider,consumer,你会发现还是完全ok的。

Ribbon负载均衡

上面说了,Ribbon是客户端的负载均衡,所以我们要加在consumer项目上,Ribbon默认的负载均衡方式的轮询。我们可以新建两个provider来测试一下

新建provider8002和8003

新建项目就不多说了,记得把8001的yml,Mybatis,代码啥的复制过去。如下

server:
  port: 8002

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

spring:
  application:
    name: provider-dept
  datasource:
#   type: com.alibaba.druid.pool.DruidDataSource
    url: jdbc:mysql://localhost:3306/vae?serverTimezone=UTC
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: 123
    dbcp2:
      min-idle: 5                                           # 数据库连接池的最小维持连接数
      initial-size: 5                                       # 初始化连接数
      max-total: 5                                          # 最大连接数
      max-wait-millis: 200                                 # 等待连接获取的最大超时时间

eureka:
  client: #客户端注册进eureka服务列表内
    service-url:
#      defaultZone: http://localhost:7001/eureka
      defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/
  instance:
    instance-id: provider-8002  #这个是修改Eureka界面的Status名称
    prefer-ip-address: true    #这个是设置鼠标放在status上的时候,出现的提示,设置ip地址显示


info:
  app.name: provider-8001
  company.name: www.vae.com
  build.artifactId: $project.artifactId$
  build.version: $project.version$


server:
  port: 8003

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

spring:
  application:
    name: provider-dept
  datasource:
#   type: com.alibaba.druid.pool.DruidDataSource
    url: jdbc:mysql://localhost:3306/jj?serverTimezone=UTC
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: 123
    dbcp2:
      min-idle: 5                                           # 数据库连接池的最小维持连接数
      initial-size: 5                                       # 初始化连接数
      max-total: 5                                          # 最大连接数
      max-wait-millis: 200                                 # 等待连接获取的最大超时时间

eureka:
  client: #客户端注册进eureka服务列表内
    service-url:
#      defaultZone: http://localhost:7001/eureka
      defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/
  instance:
    instance-id: provider-8003  #这个是修改Eureka界面的Status名称
    prefer-ip-address: true    #这个是设置鼠标放在status上的时候,出现的提示,设置ip地址显示


info:
  app.name: provider-8001
  company.name: www.vae.com
  build.artifactId: $project.artifactId$
  build.version: $project.version$


看到没,yml文件,我只修改了端口号、数据库和instance-id状态id。因为每一个微服务都可以都一个独立的数据库,所以,三个provider的数据库我分别使用的是shuyunquan,vae,jj三个数据库,当然,表结构都是一样的没改

代码直接复制就行,没什么需要改的。

现在我们启动eureka7001,eureka7002,eureka7003,provider8001,provider8002,provider8003和consumer。算一下7个项目了,运行之后,你可以通过consumer访问试试,结果每次刷新都换了数据库,这刚好表明了Ribbon的默认负载均衡方式是轮询。

Ribbon核心组件IRule

Ribbon的核心组件IRule作用是定义以什么样的方式去负载均衡,比如轮询,比如随机。要想使用IRule,我们需要写一个类。

注意:IRule组件的类不能在有@ComponentScan组件的类的包以及子包下存在,主方法的@SpringBootApplication注解里面有@ComponentScan注解,所以,我们不能在主方法的所在包或者子包下新建IRule组件

我们在vae包下新建myrule包,包下新建MySelfRule类,代码如下:

package com.vae.myrule;

import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.RandomRule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MySelfRule {

    @Bean
    public IRule myRule(){
        return new RandomRule(); //这个是负载均衡的随机访问
//      return new RoundRobinRule();  这个是负载均衡的默认的轮询访问
    }
}

看一下项目目录图

Ribbon自定义

上面讲的都太简单了,完全体现不出技术含量,所以Ribbon负载均衡的方式只有轮询和随机这还不够,我们要学会自定义负载均衡的方式,先看一张图

要想自定义Ribbon负载均衡,我们要自己写个类,首先要继承AbstractLoadBalancerRule这个类,然后剩下的代码嘛,你可以去Ribbon的官方GitHub看看,Ribbon官方随机代码,把这里面的方法复制出来,我们看看里面的内容都是啥,我们只摘取方法

package com.vae.myrule;

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

import java.util.List;
import java.util.concurrent.ThreadLocalRandom;


public class RandomRule extends AbstractLoadBalancerRule {


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

     while (server == null) {
         //线程是否中断,中断返回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 = chooseRandomInt(serverCount);
         server = upList.get(index);

         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;

 }

    protected int chooseRandomInt(int serverCount) {
        return ThreadLocalRandom.current().nextInt(serverCount);
    }

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

    @Override
    public void initWithNiwsConfig(IClientConfig iClientConfig) {

    }
}



可以看到啊,大概就是这样子的,initWithNiwsConfig方法是我们继承了AbstractLoadBalancerRule实现的,上面的方法中,很容易看出choose方法才是真正有内容的,我们现在就可以对这个随机算法进行修改了,比如,我想修改为每个服务访问5次,然后再随机的访问下一个服务。肯定是修改choose方法了

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;

	}

可以看到,我就定义了两个变量,自定义算法这个是很主观的,你需要什么样的方式来负载均衡就自己改代码就好了。

推荐阅读