SpringCloud之ribbon

2020/06/11 SpringCloud

springcloud中负载均衡工具ribbon用法及原理

1.ribbon用法

在这里我使用restTemplate和ribbon结合使用为例,前提是服务到注册到同一注册中心,本例使用的是nacos。

基本用法

  • 一般情况下,我们使用restTemplate做服务调用,代码如下:

      ResponseEntity<ProductInfo> responseEntity= restTemplate.getForEntity(uri+orderInfo.getProductNo(), ProductInfo.class);
    
      ProductInfo productInfo = responseEntity.getBody();
    
      if(productInfo == null) {
     	   return "无数据";
      } 
    

当调用的服务有多个实例,需要做负载均衡是,则引入了ribbon,使用如下:

 

    使用@LoadBalnaced注解,那么返回的restTemplate注解就是一个具有负载算法的对象;在调用服务时,会从注册中心获取被调用方的所有可调用实例列表,并依据负载均衡算法进行服务调用。

  • 如果需要指定负载均衡算法,则在配置类中返回一个实现IRule接口的负载算法对象即可:
     

问题:那么ribbon是怎么让restTemplate具有负载均衡功能的呢?后面再分析原理


进阶用法

    在一些业务场景中,我们希望对不同的微服务集群使用不同的负载均衡算法,甚至某些服务集群使用自定义的负载均衡算法。比如:我对订单服务集群使用轮询负载算法,对付费服务集群使用随机负载算法,对积分服务集群使用自定义的权重负载算法,这时我们有两种方式可以实现:

  • 基于yml文件配置的方法(推荐的方式): 通过yml配置的方式,对不同的微服务集群采用不同的负载算法,ribbon中自带的负载算法均是实现IRule接口。

    IRule接口是一个ribbon-balancer包下的一个上层接口,大致结构如下,使用的是模板方法模式,其实现类实现了不同的负载均衡算法:

类图如下:

 
    我们可以基于此接口实现自己的负载均衡算法: 如下:

自定义负载算法类:TulingWeightedRule.class (自定义实现权重算法:nacos注册中心可以给集群中的实例设置权重)

package com.tuling.myrule;

import com.alibaba.cloud.nacos.NacosDiscoveryProperties;
import com.alibaba.cloud.nacos.ribbon.NacosServer;
import com.alibaba.nacos.api.exception.NacosException;
import com.alibaba.nacos.api.naming.NamingService;
import com.alibaba.nacos.api.naming.pojo.Instance;
import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.AbstractLoadBalancerRule;
import com.netflix.loadbalancer.BaseLoadBalancer;
import com.netflix.loadbalancer.Server;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;

/**
 */
@Slf4j
public class TulingWeightedRule extends AbstractLoadBalancerRule {

    @Autowired
    private NacosDiscoveryProperties discoveryProperties;

    @Override
    public void initWithNiwsConfig(IClientConfig clientConfig) {
        //读取配置文件并且初始化,ribbon内部的 几乎用不上
    }


    @Override
    public Server choose(Object key) {
        try {
            log.info("key:{}",key);
            BaseLoadBalancer baseLoadBalancer = (BaseLoadBalancer) this.getLoadBalancer();
            log.info("baseLoadBalancer--->:{}",baseLoadBalancer);

            //获取微服务的名称
            String serviceName = baseLoadBalancer.getName();

            //获取Nocas服务发现的相关组件API
            NamingService namingService =  discoveryProperties.namingServiceInstance();

            //获取 一个基于nacos client 实现权重的负载均衡算法
            Instance instance = namingService.selectOneHealthyInstance(serviceName);

            //返回一个server
            return new NacosServer(instance);
        } catch (NacosException e) {
            log.error("自定义负载均衡算法错误");
        }
        return null;
    }
}

在yml配置中只需要指定对应的配置算法包位置即可,如下图所示:

 
这种配置方式不需要再添加其他的java代码,是最常用也是推荐的方式。

    在真实开发中一般我们会使用feign代替restTemplate+ribbon,feign是对ribbon的封装,针对feign调用配置负载均衡算法,其实也是对ribbon的配置,配置方法和上图一致

  • 通过javaConfig的方式配置     定义负载算法的配置类:跟基础用法的原理一样,通过配置类向容器中加入实现IRule接口的负载算法即可。

这种方式需要增加一个配置类。并使用@RibbonClients注解,代码如下:

	package com.tuling.config; 
	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 WebConfig {
	
	@LoadBalanced
	@Bean
	public RestTemplate restTemplate( ) {
		return new RestTemplate();
		}
	} 

负载算法配置总类:

package com.tuling.config;

import com.ribbonconfig.GlobalRibbonConfig;
import com.ribbonconfig.PayCenterRibbonConfig;
import com.ribbonconfig.ProductCenterRibbonConfig;
import org.springframework.cloud.netflix.ribbon.RibbonClient;
import org.springframework.cloud.netflix.ribbon.RibbonClients;
import org.springframework.context.annotation.Configuration;


@Configuration
@RibbonClients(
   		defaultConfiguration = GlobalRibbonConfig.class, 	//全局默认算法
value = {
    @RibbonClient(name = "product-center",configuration ProductCenterRibbonConfig.class),    //服务集群product-center使用的负载算法
    @RibbonClient(name = "pay-center",configuration = PayCenterRibbonConfig.class) //服务集群pay-center使用的负载算法
})

public class CustomRibbonConfig {

}

服务集群product-center对应的算法配置:ProductCenterRibbonConfig.class:

package com.ribbonconfig;

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 ProductCenterRibbonConfig {

	@Bean
	public IRule randomRule() {
		return new RandomRule();
	}
}

服务集群pay-center对应的算法配置:PayCenterRibbonConfig.class:

package com.ribbonconfig;

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


@Configuration
public class PayCenterRibbonConfig {

    public IRule roundRobinRule() {
        return new RoundRobinRule();
    }
} 

全局默认使用自定义的负载算法(比如需要实现权重算法:nacos注册中心可以给集群中的实例设置权重):

全局默认算法对应配置: GlobalRibbonConfig.class

package com.ribbonconfig;

import com.netflix.loadbalancer.IRule;
import com.tuling.myrule.TheSameClusterPriorityRule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;


@Configuration
public class GlobalRibbonConfig {

    @Bean
    public IRule theSameClusterPriorityRule() {
        return new TulingWeightedRule();  //上文中自定义的负载算法	     
	}
}
  • 综上,就通过javaConfig的方式,实现了指定负载均衡算法,并且可以指定使用自定义的负载算法。

2.ribbon的原理

  • 先通过一个例子来说明,也可以理解为手写实现一个简单的ribbon嵌入到restTemplate中:

首先还是基础的restTemplate配置类:

package com.tuling.config;

import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;


@Configuration
public class WebConfig {

    @Bean
    public RestTemplate restTemplate(DiscoveryClient discoveryClient) {
        return new TulingRestTemplate(discoveryClient);
    }
}

    可以看到这次我不是直接返回一个springcloud包中的RestTemplate的实例,而是一个自定义的TulingRestTemplate,该类代码如下:

package com.tuling.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.http.HttpMethod;
import org.springframework.http.client.ClientHttpRequest;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.web.client.*;

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
import java.util.Random;

/**
 * 根据RestTemplate特性自己改造
 */
@Slf4j
public class TulingRestTemplate extends RestTemplate {

    private DiscoveryClient discoveryClient;

    public TulingRestTemplate (DiscoveryClient discoveryClient) {
        this.discoveryClient = discoveryClient;
    }

    protected <T> T doExecute(URI url, @Nullable HttpMethod method, @Nullable RequestCallback requestCallback,
                              @Nullable ResponseExtractor<T> responseExtractor) throws RestClientException {

        Assert.notNull(url, "URI is required");
        Assert.notNull(method, "HttpMethod is required");
        ClientHttpResponse response = null;
        try {

            log.info("请求的url路径为:{}",url);
            //把服务名 替换成我们的IP
            url = replaceUrl(url);

            log.info("替换后的路径:{}",url);

            ClientHttpRequest request = createRequest(url, method);
            if (requestCallback != null) {
                requestCallback.doWithRequest(request);
            }
            response = request.execute();
            handleResponse(url, method, response);
            return (responseExtractor != null ? responseExtractor.extractData(response) : null);
        }
        catch (IOException ex) {
            String resource = url.toString();
            String query = url.getRawQuery();
            resource = (query != null ? resource.substring(0, resource.indexOf('?')) : resource);
            throw new ResourceAccessException("I/O error on " + method.name() +
                    " request for \"" + resource + "\": " + ex.getMessage(), ex);
        } finally {
            if (response != null) {
                response.close();
            }
        }
    }


    /**
     * 方法实现说明:把微服务名称  去注册中心拉取对应IP进行调用
     * http://product-center/selectProductInfoById/1
     * @param url:请求的url
     * @return:
     * @exception:
     */
    private URI replaceUrl(URI url){

        //1:从URI中解析调用的调用的serviceName=product-center
        String serviceName = url.getHost();
        log.info("调用微服务的名称:{}",serviceName);

        //2:解析我们的请求路径 reqPath= /selectProductInfoById/1
        String reqPath = url.getPath();
        log.info("请求path:{}",reqPath);


        //通过微服务的名称去nacos服务端获取 对应的实例列表
        List<ServiceInstance> serviceInstanceList = discoveryClient.getInstances(serviceName);
        if(serviceInstanceList.isEmpty()) {
            throw new RuntimeException("没有可用的微服务实例列表:"+serviceName);
        }

        String serviceIp = chooseTargetIp(serviceInstanceList);

        String source = serviceIp+reqPath;
        try {
            return new URI(source);
        } catch (URISyntaxException e) {
            log.error("根据source:{}构建URI异常",source);
        }
        return url;
    }

    /**
     * 方法实现说明:从服务列表中 随机选举一个ip
     * @param serviceInstanceList 服务列表
     * @return: 调用的ip
     * @exception:
     */
    private String chooseTargetIp(List<ServiceInstance> serviceInstanceList) {
        //采取随机的获取一个
        Random random = new Random();
        Integer randomIndex = random.nextInt(serviceInstanceList.size());
        String serviceIp = serviceInstanceList.get(randomIndex).getUri().toString();
        log.info("随机选举的服务IP:{}",serviceIp);
        return serviceIp;
    }
    
}

    代码很多,思路就是通过继承RestTemplate类,并复写doExecute方法(在RestTemplate源码中可以看到,所有的调用方法最后都会走doExecute方法)。

    大体意思是在调用时,去拉取注册中心该服务的服务实例列表,然后通过random随机算法来选出一个服务实例来进行调用。这样就相当于对RestTemplate进行了一个扩展,让它在调用服务时,拥有了负载均衡的功能。

  • 那么ribbon也是通过扩展restTemplate来实现负载均衡的。

跟踪源码可以看到,ribbon是通过在spring容器启动时,向restTemplate中加入interceptor拦截器的方式来为restTemplate添加负载均衡功能,截图如下:  

大体的思想就是这样!

Search

    我的微信

    闷骚的程序员

    Table of Contents