Ribbon 客户端负载均衡

Ribbon 客户端负载均衡

负载均衡

负载均衡 建立在现有网络结构之上,它提供了一种廉价有效透明的方法扩展网络设备服务器的带宽、增加吞吐量、加强网络数据处理能力、提高网络的灵活性和可用性。

分类

分类实现方式优点缺点
软件在一台或多台服务器相应的操作系统上安装一个或多个附加软件基于特定环境,配置简单,使用灵活,成本低廉软件本身耗费资源、可扩展性不好、受操作系统限制、安全问题
硬件直接在服务器和外部网络间安装负载均衡设备独立于操作系统、多样化的负载均衡策略、智能化的流量管理、性能高成本昂贵
本地对本地的服务器群做负载均衡能有效地解决数据流量过大、网络负荷过重的问题、不需要购置昂贵的服务器
全局对分别放置在不同的地理位置、有不同网络结构的服务器群间作负载均衡使全球用户只以一个IP地址或域名就能访问到离自己最近的服务器

客户端与服务端级别的负载均衡

  • 服务器端负载均衡:例如Nginx,通过Nginx进行负载均衡过程如下:先发送请求给nginx服务器,然后通过负载均衡算法,在多个业务服务器之间选择一个进行访问;即在服务器端再进行负载均衡算法分配。
  • 客户端负载均衡:客户端会有一个服务器地址列表,在发送请求前通过负载均衡算法选择一个服务器,然后进行访问,即在客户端就进行负载均衡算法分配。

Ribbon

Netflix Ribbon 以及 被SpringCloud 封装过的Ribbon,本质上都是客户端负载均衡的方式。

原理概述

但凡客户端负载均衡,大抵可以分为两个过程:

  • 获取服务注册列表的信息
  • 根据均衡策略进行负载均衡

在SpringCloud中,服务注册列表的信息来自 EurekaClient。

源码分析

在Ribbon的整个调度过程中,LoadBalancerClient,是负责整个过程的执行者。

获取服务注册列表

抛开SpringCloud中一大堆的框架代码,直接找到 DynamicServerListLoadBalancer 类中的 updateListOfServers() 方法:

@VisibleForTesting
    public void updateListOfServers() {
        List<T> servers = new ArrayList();
        if (this.serverListImpl != null) {
            servers = this.serverListImpl.getUpdatedListOfServers();
            LOGGER.debug("List of Servers for {} obtained from Discovery client: {}", this.getIdentifier(), servers);
            if (this.filter != null) {
                servers = this.filter.getFilteredListOfServers((List)servers);
                LOGGER.debug("Filtered List of Servers for {} obtained from Discovery client: {}", this.getIdentifier(), servers);
            }
        }

        this.updateAllServerList((List)servers);
    }

继续根据 serverListImpl.getUpdatedListOfServers() 顺藤摸瓜,找到类 DiscoveryEnabledNIWSServerList ,其中有 getInitialListOfServers()getUpdatedListOfServers() 方法:

@Override
    public List<DiscoveryEnabledServer> getInitialListOfServers(){
        return obtainServersViaDiscovery();
    }
@Override
    public List<DiscoveryEnabledServer> getUpdatedListOfServers(){
        return obtainServersViaDiscovery();
    }

这里定义了两个方法,一个是获取初始服务注册列表的方法,一个是获取更新后的服务注册列表的方法,其实是同一个方法……具体实现:

private List<DiscoveryEnabledServer> obtainServersViaDiscovery() {
        List<DiscoveryEnabledServer> serverList = new ArrayList();
        if (this.eurekaClientProvider != null && this.eurekaClientProvider.get() != null) {
            EurekaClient eurekaClient = (EurekaClient)this.eurekaClientProvider.get();
            if (this.vipAddresses != null) {
                String[] var3 = this.vipAddresses.split(",");
                int var4 = var3.length;

                for(int var5 = 0; var5 < var4; ++var5) {
                    String vipAddress = var3[var5];
                    List<InstanceInfo> listOfInstanceInfo = eurekaClient.getInstancesByVipAddress(vipAddress, this.isSecure, this.targetRegion);
                    //省略部分代码
                }
            }
        }
    }

上述方法分两个部分:

  • 通过 eurekaClientProvider.get() 获取 EurekaClient
  • 通过 EurekaClient 来获取服务注册列表信息

其中,eurekaClientProvider 的实现类是 LegacyEurekaClientProvider,它是一个获取 eurekaClient 类,通过静态的方法去获取 eurekaClient ,其代码如下:

class LegacyEurekaClientProvider implements Provider<EurekaClient> {
    private volatile EurekaClient eurekaClient;
    @Override
    public synchronized EurekaClient get() {
        if (eurekaClient == null) {
            eurekaClient = DiscoveryManager.getInstance().getDiscoveryClient();
        }
        return eurekaClient;
    }
}
获取服务注册列表的时间间隔

在BaseLoadBalancer类下,BaseLoadBalancer的构造函数,该构造函数开启了一个PingTask任务:

public BaseLoadBalancer(String name, IRule rule, LoadBalancerStats stats,
            IPing ping, IPingStrategy pingStrategy) {
        //省略部分代码
        setupPingTask();
         //省略部分代码
    }
void setupPingTask() {
        //省略部分代码
        lbTimer.schedule(new PingTask(), 0, pingIntervalSeconds * 1000);
       //省略部分代码
    }
判断服务可用性

在BaseLoadBalancer的构造函数中有一个参数IPing ping,它定义了服务的可用性:

public interface IPing {
    boolean isAlive(Server var1);
}

这里仅仅设置定义了一个布尔值,真正使用的地方,在Pinger的runPinger()方法中:

public void runPinger() throws Exception {
            //省略部分代码
                    boolean[] resultsx = this.pingerStrategy.pingServers(BaseLoadBalancer.this.ping, allServers);
            //省略部分代码    
        }

简单来说,pingerStrategy.pingServers()方法会返回一个布尔值,代表服务注册列表是否有变化

public interface IPingStrategy {
    boolean[] pingServers(IPing var1, Server[] var2);
}

实现其实很简单,获取新的服务注册列表的 Ping 列表,与已经获取的进行比较:

public boolean[] pingServers(IPing ping, Server[] servers) {
            int numCandidates = servers.length;
            boolean[] results = new boolean[numCandidates];
           //省略部分代码
            for(int i = 0; i < numCandidates; ++i) {
                results[i] = false;
                try {
                    if (ping != null) {
                        results[i] = ping.isAlive(servers[i]);
                    }
                } //省略部分代码
            return results;
        }

由此可见,LoadBalancerClient 会向Eureka 获取服务注册列表,并且通过10s一次向EurekaClient发送“ping”,来判断服务的可用性,如果服务的可用性发生了改变或者服务数量和之前的不一致,则更新或者重新拉取。LoadBalancerClient有了这些服务注册列表,就可以进行负载均衡。

负载均衡

简而言之,真正的负载均衡是委托给 IRule 实现的:

public Server chooseServer(Object key) {
        //省略部分代码
        this.counter.increment();
        //省略部分代码
        return this.rule.choose(key);
        //省略部分代码
    }

在这里插入图片描述

IRule 通过 AbstractLoadBalancerRule 加载过滤规则:

public abstract class AbstractLoadBalancerRule implements IRule, IClientConfigAware {
    private ILoadBalancer lb;

    public AbstractLoadBalancerRule() {
    }

    public void setLoadBalancer(ILoadBalancer lb) {
        this.lb = lb;
    }

    public ILoadBalancer getLoadBalancer() {
        return this.lb;
    }
}
RandomRule

本质上通过随机数随机获取一个服务实例,在index上随机,选择index对应位置的server,但如果根据随机数获取不到实例,则会出现BUG

RoundRobinRule

按照线性轮询的方式依次选择每个服务的实例,如果超过10次获取不到服务,则尝试结束,并抛出警告。

RetryRule

RoundRobinRule 基础上增加了重试机制,在一个配置时间段内当选择server不成功,则一直尝试使用subRule的方式选择一个可用的server

WeightedResponseTimeRule

该方式是 RoundRobinRule 的扩展,增加了根据实例的运行情况来计算权值,并根据权值来选择实例,其实现主要分3个步骤:

  • 定时任务

    • WeightedResponseTimeRule 策略在初始化时会启动一个定时任务,每隔30s会执行权值更新的操作
  • 权值计算

    • 权值的值是总响应时间与实例自身平均响应时间的差所累加而得

    • 举例:假设有四个实例A、B、C、D,他们的平均响应时间为10、40、80、100,所以总响应时间是230,每个实例的权值如下:

      • A:230-10=220
      • B:220+(230-40)=410
      • C:410+(230-80)=560
      • D:560+(230-100)=690

      这些权值代表的一个区间,总区间是 [0 ,690)

      • A:[0,220]
      • B:(220,410]
      • C:(410,560]
      • B:(560,690)
  • 实例选择

    • 实例的选择是随机一个数字,然后将这个数字所在的权值区间对应的服务实例作为被选择的实例
    • 根据上述权值计算发现,平均响应时间越短,权重区间宽度越大,因此被选中的概率越大
BestAvailableRule

逐个考察实例,如果实例被过滤掉了,则忽略,再选择其中并发请求数最小的实例。

AvailabilityFilteringRule

该策略有两个过程,先以线性方式选择一个实例,再判断是否满足,如果满足就选择,不满足就继续下一个

  • 过滤:
    • 过滤掉那些因为一直连接失败而被标记为circuit tripped的服务实例
    • 过滤掉那些高并发的的服务实例(active connections 超过配置的阈值),阀值默认为2^32-1,可通过参数<clientName>.<nameSpace>.ActiveConnectionsLimit 来修改
ZoneAvoidanceRule

和 AvailabilityFilteringRule 不一样的是,ZoneAvoidanceRule是先进行过滤,再轮询选择,过滤的条件和 AvailabilityFilteringRule 一样,不过是先通过过滤把所有的服务找出来,然后再去轮询选择。

  • 使用主过滤条件过滤所有实例并返回过滤的清单
  • 依次使用次过滤条件对返回的过滤清单进行过滤
  • 每次过滤完成都需要判断下面两个条件,只要有一个符合就不再过滤,并将当前结果返回供线性轮询选择:
    • 过滤后是实例总数 >= 最小过滤实例数(默认为1)
    • 过滤后的实例比例 > 最小过滤百分百(默认为0)

SpringCloud 对 Ribbon 的封装

与RestTemplate的结合

使用,增加一个注解 @LoadBalance

@LoadBalanced
    RestTemplate restTemplate() {
        return new RestTemplate();
    }

源码分析,SpringBoot的一个核心功能就是自动配置,其中以 XXXAutoConfiguration 类为最关键:

找到 LoadBalanceAutoConfiguration 类定义如下:

@Configuration
@ConditionalOnClass(RestTemplate.class)
@ConditionalOnBean(LoadBalancerClient.class)
@EnableConfigurationProperties(LoadBalancerRetryProperties.class)
public class LoadBalancerAutoConfiguration {

	@LoadBalanced
	@Autowired(required = false)
	private List<RestTemplate> restTemplates = Collections.emptyList();
    
    @Bean
	public SmartInitializingSingleton loadBalancedRestTemplateInitializer(
			final List<RestTemplateCustomizer> customizers) {
		return new SmartInitializingSingleton() {
			@Override
			public void afterSingletonsInstantiated() {
				for (RestTemplate restTemplate : LoadBalancerAutoConfiguration.this.restTemplates) {
					for (RestTemplateCustomizer customizer : customizers) {
						customizer.customize(restTemplate);
					}
				}
			}
		};
	}
}

在该类中,首先维护了一个被@LoadBalanced修饰的RestTemplate对象的List,在初始化的过程中,通过调用customizer.customize(restTemplate)方法来给RestTemplate增加拦截器LoadBalancerInterceptor。

而LoadBalancerInterceptor,用于实时拦截,在LoadBalancerInterceptor实现来负载均衡。LoadBalancerInterceptor的拦截方法如下:

@Override
    public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
            final ClientHttpRequestExecution execution) throws IOException {
        final URI originalUri = request.getURI();
        String serviceName = originalUri.getHost();
        Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri);
        return this.loadBalancer.execute(serviceName, requestFactory.createRequest(request, body, execution));
    }

总结

综上所述,Ribbon的负载均衡,主要通过LoadBalancerClient来实现的,而LoadBalancerClient具体交给了ILoadBalancer来处理,ILoadBalancer通过配置IRule、IPing等信息,并向EurekaClient获取注册列表的信息,并默认10秒一次向EurekaClient发送“ping”,进而检查是否更新服务列表,最后,得到注册列表后,ILoadBalancer根据IRule的策略进行负载均衡。

而RestTemplate 被@LoadBalance注解后,能过用负载均衡,主要是维护了一个被@LoadBalance注解的RestTemplate列表,并给列表中的RestTemplate添加拦截器,进而交给负载均衡器去处理。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
SpringCloud的复杂均衡 属于客户端;nginx辅助均衡属于服务器端 ,restTemplate 进行RPC调用.对外使用 rest http方式。DiscoveryClient 可以自己实现一个负载均衡的算法(使用请求总数 % 服务列表个数).算法文件跟新一下 @SpringBootApplication @EnableDiscoveryClient @RestController public class AppConsulClient { @Autowired private DiscoveryClient discoveryClient; @RequestMapping("/getServer") public String getServerInstance(){ List<ServiceInstance> list = discoveryClient.getInstances("consul-order"); for (ServiceInstance serviceInstance : list) { if(serviceInstance !=null ){ return serviceInstance.getUri().toString()+"---"+serviceInstance.getServiceId(); } } return null; } @Resource private RestTemplate restTemplate; @RequestMapping("/disconverClient") public String getDiscoveryClient(){ //获取服务列表 String serverurl = getServerurl(); if(StringUtils.isEmpty(serverurl)){ return "请求列表为null"; } System.out.println("请求服务地址:"+serverurl); return restTemplate.getForObject(serverurl,String.class); } private Integer requestCount = 1; @RequestMapping("/getServerurl") public String getServerurl(){ List<ServiceInstance> list = discoveryClient.getInstances("consul-order"); if(list == null && list.size() ==0){ return null; } //获取服务注册列表的个数 int size = list.size(); int index = requestCount % size; requestCount++; return list.get(index).getUri().toString(); } /** * @LoadBalanced 开启本地复杂均衡 * @return */ @Bean @LoadBalanced public RestTemplate restTemplate(){ return new RestTemplate(); } public static void main(String[] args) { SpringApplication.run(AppConsulClient.class); } }

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值