Ribbon原理-RestTemplate使用@LoadBalanced负载均衡源码详解

Ribbon原理-RestTemplate使用@LoadBalanced负载均衡源码详解

      • 1. @LoadBalanced
      • 2. RibbonLoadBalancerClient
      • 3. Rule
        • 3.1 配置负载均衡规则
        • 3.2 自己写负载均衡
      • 4. 代码清单

1. @LoadBalanced

示例代码概述:

  1. 使用Eureka作为注册中心
  2. 服务提供者
  3. 服务消费者(使用Ribbon实现负载均衡)

前戏:在使用RPC框架实现远程调用的时候都会用到RestTemplate,如果想实现负载均衡还会添加一个@LoadBalanced注解,看下面这个例子:

@Configuration
@ComponentScan(basePackageClasses = {OrderMain.class})
public class ApplicaitonContextConfig {
    @Bean
    @LoadBalanced
    public RestTemplate getRestTemplate() {
        return new RestTemplate();
    }
}

点进LoadBalanced注解看一下:

/**
 * Annotation to mark a RestTemplate bean to be configured to use a LoadBalancerClient.
 * @author Spencer Gibb
 */
@Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Qualifier
public @interface LoadBalanced {

}

翻译一下上面的注释:该注解配置当前这个RestTemplate可以用一个LoadBalancerClient
那什么是LoadBalancerClient,我们看一下它的定义:

/**
 * Represents a client-side load balancer.
 *
 * @author Spencer Gibb
 */
public interface LoadBalancerClient extends ServiceInstanceChooser {
//方法省略
}

再看上面的注解:代表一个负载均衡器,并且它是一个接口,继承了ServiceInstanceChooser接口,这个ServiceInstanceChooser接口是干嘛的呢,可以再看下ServiceInstanceChooser接口

/**
 * Implemented by classes which use a load balancer to choose a server to send a request
 * to.
 *
 * @author Ryan Baxter
 */
public interface ServiceInstanceChooser {

	/**
	 * Chooses a ServiceInstance from the LoadBalancer for the specified service.
	 * @param serviceId The service ID to look up the LoadBalancer.
	 * @return A ServiceInstance that matches the serviceId.
	 */
	ServiceInstance choose(String serviceId);
}

看上面注释:表明实现这个接口的类需要去实现这个choose方法去选择一个server发送request,别急,后面会看到这个choose方法是怎么发挥作用的

好了,到此为止我们知道了LoadBalanced 注解是去给RestTemplate配置一个LoadBalancerClient实现它的负载均衡功能

那么接下来看下实现LoadBalancerClient的有什么东东
Ribbon原理-RestTemplate使用@LoadBalanced负载均衡源码详解_第1张图片

看到没有,在这个工程里面使用实现这个LoadBalancerClient的有RibbonLoadBalancerClient ,这个是起主要作用的一个负载均衡的Client,所以重头戏在这个RibbonLoadBalancerClient里面。

接下来讲这个RibbonLoadBalancerClient


2. RibbonLoadBalancerClient

我们仅挑几个重要的方法
这个类中有下面这个方法:


	protected Server getServer(ILoadBalancer loadBalancer) {
		return getServer(loadBalancer, null);
	}

	protected Server getServer(ILoadBalancer loadBalancer, Object hint) {
		if (loadBalancer == null) {
			return null;
		}
		// Use 'default' on a null hint, or just pass it on?
		return loadBalancer.chooseServer(hint != null ? hint : "default");
	}

可以看到这个Client是通过一个ILoadBalancer选择一个指定的Server来处理请求的,这个ILoadBalancer有个抽象实现类AbstractLoadBalancer ,这个抽象实现类有两个重要的方法:

    /**
     * delegate to {@link #chooseServer(Object)} with parameter null.
     */
    public Server chooseServer() {
    	return chooseServer(null);
    }

    
    /**
     * List of servers that this Loadbalancer knows about
     * 
     * @param serverGroup Servers grouped by status, e.g., {@link ServerGroup#STATUS_UP}
     */
    public abstract List<Server> getServerList(ServerGroup serverGroup);

Ribbon原理-RestTemplate使用@LoadBalanced负载均衡源码详解_第2张图片
它的这两个方法在子类BaseLoadBalancer中有实现,看下

    @Override
    public List<Server> getServerList(ServerGroup serverGroup) {
        switch (serverGroup) {
        case ALL:
            return allServerList;
        case STATUS_UP:
            return upServerList;
        case STATUS_NOT_UP:
            ArrayList<Server> notAvailableServers = new ArrayList<Server>(
                    allServerList);
            ArrayList<Server> upServers = new ArrayList<Server>(upServerList);
            notAvailableServers.removeAll(upServers);
            return notAvailableServers;
        }
        return new ArrayList<Server>();
    }


    public Server chooseServer(Object key) {
        if (counter == null) {
            counter = createCounter();
        }
        counter.increment();
        if (rule == null) {
            return null;
        } else {
            try {
                return rule.choose(key);
            } catch (Exception e) {
                logger.warn("LoadBalancer [{}]:  Error choosing server for key {}", name, key, e);
                return null;
            }
        }
    }

getServerList方法是获取指定的服务列表了,chooseServer是根据特定的规则从服务列表中选取一个服务并返回,那么另一个重头戏就是chooseServer方法中的Rule了 。
下面讲一下这个Rule


3. Rule

这个Rule是一个接口:IRule

/**
 * Interface that defines a "Rule" for a LoadBalancer. A Rule can be thought of
 * as a Strategy for loadbalacing. Well known loadbalancing strategies include
 * Round Robin, Response Time based etc.
 * 
 * @author stonse
 * 
 */
public interface IRule{
    /*
     * choose one alive server from lb.allServers or
     * lb.upServers according to key
     * 
     * @return choosen Server object. NULL is returned if none
     *  server is available 
     */

    public Server choose(Object key);
    
    public void setLoadBalancer(ILoadBalancer lb);
    
    public ILoadBalancer getLoadBalancer();    
}

看看Netflix实现了多少负载均衡规则吧:
Ribbon原理-RestTemplate使用@LoadBalanced负载均衡源码详解_第3张图片
默认是RouundRobinRule:轮询,看下它的实现逻辑

public class RoundRobinRule extends AbstractLoadBalancerRule {
    private AtomicInteger nextServerCyclicCounter;    
    
        public RoundRobinRule() {
        nextServerCyclicCounter = new AtomicInteger(0);
    }
	public Server choose(ILoadBalancer lb, Object key) {
        if (lb == null) {
            log.warn("no load balancer");
            return null;
        }

        Server server = null;
        int count = 0;
        while (server == null && count++ < 10) {//控制十次轮询,如果十次还找不到可用的服务就返回空并发出警告
            List<Server> reachableServers = lb.getReachableServers();//获取所有上线且可用的服务
            List<Server> allServers = lb.getAllServers();
            int upCount = reachableServers.size();
            int serverCount = allServers.size();

            if ((upCount == 0) || (serverCount == 0)) {
                log.warn("No up servers available from load balancer: " + lb);
                return null;
            }

            int nextServerIndex = incrementAndGetModulo(serverCount);//计算下标
            server = allServers.get(nextServerIndex);//获取服务

            if (server == null) {
                /* Transient. */
                Thread.yield();
                continue;
            }

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

            // Next.
            server = null;
        }

        if (count >= 10) {
            log.warn("No available alive servers after 10 tries from load balancer: "
                    + lb);
        }
        return server;
    }

    /**
     * Inspired by the implementation of {@link AtomicInteger#incrementAndGet()}.
     *
     * @param modulo The modulo to bound the value of the counter.
     * @return The next value.
     */
    private int incrementAndGetModulo(int modulo) {
        for (;;) {
            int current = nextServerCyclicCounter.get();
            int next = (current + 1) % modulo;
            if (nextServerCyclicCounter.compareAndSet(current, next))
                return next;
        }
    }
}

它的功能实现主要是这两个方法,轮询算法是incrementAndGetModulo方法,逻辑很简单,这个类一开始用一个AtomicInteger 变量记录了当前请求次数,获取所有的服务实例数量,当前的请求次数对所有服务实例个数取余即得到所选取的服务实例在所有服务实例的下标,最后将选取的服务实例(提供者)返回

其他的规则如下:
Ribbon原理-RestTemplate使用@LoadBalanced负载均衡源码详解_第4张图片
我们怎么在代码中配置Netflix已经实现的Rule呢,下面就这个例子中的服务消费者讲解一下:

3.1 配置负载均衡规则
  1. 新建一个配置类,其实也就是将这个规则注入到IOC中
@Configuration
public class MyRibbonRule {
    @Bean
    public IRule getMyRule(){
        return new RandomRule();//随机
    }
}

但是需要注意的是这个配置类所在的包不能在包扫描能扫描到的位置,所以这里我的目录结构是这样的
Ribbon原理-RestTemplate使用@LoadBalanced负载均衡源码详解_第5张图片
在OrderMain中我开启了包扫描,但是扫不到ribbonrule包中的内容

@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class, DataSourceTransactionManagerAutoConfiguration.class })
@EnableEurekaClient
@RibbonClient(configuration = MyRibbonRule.class,name = "springcloud-payment-service")
public class OrderMain {
    public static void main(String[] args) {
        SpringApplication.run(OrderMain.class,args);
    }

}

同时要使用@RibbonClient注解加载这个MyRibbonRule配置,并且指定服务提供方的应用名。

3.2 自己写负载均衡

知道了轮询规则的逻辑之后,其实我们也可以写一个负载均衡规则,前提是要禁用掉Bobbin自己的规则,即去掉RestTemplate 上的LoadBalanced注解,然后利用DiscoveryClient即可
思路是:

  1. 获取某个服务的所有提供者
  2. 按照某种规则从这个所有提供者中选取一个提供者进行调用

详情可参考贫僧另一篇文章《用DiscoveryClient手写负载均衡》


4. 代码清单

消费者调用Controller

@Slf4j
@RestController
public class OrderController {
    @Autowired
    private RestTemplate restTemplate;
    public static final String PAYMENT_URL = "http://springcloud-payment-service";

    @GetMapping("/consumer/payment/create")
    public ResultModel<Payment> create(Payment payment) {
        return restTemplate.postForObject(PAYMENT_URL + "/payment/create", payment, ResultModel.class);
    }
    @GetMapping("/consumer/payment/get/{id}")
    public ResultModel<Payment> getPayment(@PathVariable("id") Long id){
        return restTemplate.getForObject(PAYMENT_URL+"/payment/get/"+id,ResultModel.class);
    }

    /**
     * 用RestTemplate获取响应头信息
     * @param id
     * @return
     */
    @GetMapping("/consumer/paymentEntity/get/{id}")
    public ResultModel<Payment> getPaymentEntity(@PathVariable("id") Long id){
        ResponseEntity<ResultModel> entity=restTemplate.getForEntity(PAYMENT_URL+"/payment/get/"+id,ResultModel.class);
        if (entity.getStatusCode().is2xxSuccessful()){
            return entity.getBody();
        }else {
            return new ResultModel<>(CommonRespEnum.COMMON_FAIL);
        }
    }
}

这里给下服务消费者和服务提供者的应用配置:
服务消费者

server:
  port: 80
debug: true
spring:
  application:
    name: cloud-order-service
eureka:
  client:
    register-with-eureka: true
    fetch-registry: true #是否从EurekaServer获取已有的注册信息,默认为ture
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/

服务提供者

server:
  port: 8001
spring:
  application:
    name: springcloud-payment-service #服务名,也就是放进注册中心的名字
  http:
    encoding:
      force: true
      charset: UTF-8
      enabled: true
  # 一定要有端口号和服务名称
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource  #当前数据源操作类型
    driver-class-name: com.mysql.jdbc.Driver #数据库驱动包
    url: jdbc:mysql://localhost:3306/test?characterEncoding=utf8&useSSL=false&serverTimezone=UTC&rewriteBatchedStatements=true
    username: root
    password: root

mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.yolyn.springcloud.entities  #所有entity别名所在包


eureka:
  client:
    register-with-eureka: true
    fetch-registry: true #是否从EurekaServer获取已有的注册信息,默认为ture
    service-url:
     defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/
  instance:
    instance-id: payment8001 #id别名
    prefer-ip-address: true # 显示ip信息

你可能感兴趣的:(SpringCloud源码,ribbon,分布式,负载均衡器)