vlambda博客
学习文章列表

一文搞懂ribbon负载均衡


在分布式开发中,如果服务的提供者有多个实例(集群),那么服务消费者如何去选择一个服务实例进行调用。那就需要提供一种策略,来帮助消费者选择一个服务实例,这种策略称为负载均衡策略。负载均衡分类:

类型

产品实现

服务端负载均衡

软件负载均衡

Nginx、LVS

硬件负载均衡

F5

客户端负载均衡

Ribbon


在服务端负载均衡中,提供专门的服务器(单个或者集群)来作为负载均衡服务,服务消费者将请求发送到负载均衡服务中,然后通过负载均衡算法来选择其中一个目标服务实例,然后由负载均衡器转发到目标服务中。硬件服务负载均衡器相对比软件负载均衡要安全一点,当然也是比较贵的,一般政府、国企等一些土豪单位才用的起。

客户端负载均衡是指每个服务消费者(客户端)都具有负载均衡的能力,每一个服务消费者通过负载均衡算法从本地保存的服务实例(每个服务消费者都会在本地保存一份服务清单,服务清单一般来源于注册中心(如Eureka、Zookeeper、Consul等)选择一个调用。

本文主要讲解的是客户端负责均衡Ribbon。


一文搞懂ribbon负载均衡

1、Ribbon使用


步骤一:引入springcloud中的ribbon的依赖

<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-ribbon</artifactId></dependency>

步骤二:在没有引入Eureka时,我们可以在配置文件(yaml或者properties文件)中定义服务的serverList,格式

{serverName}.ribbon.listOfServers

其中serverName为spring.application.name定义的服务名,如定义名称为demoServer的服务集合:

demoServer.ribbon.listOfServers=localhost:9527,localhost:9528

步骤三:在springboot的启动类中加入@RibbonClients注解,指定服务名的配置,此步骤可以省略

@RibbonClients({ @RibbonClient(value = "demoServer")})

步骤四:通过的LoadBalancerClient的choose获取服务实例,然后通过ip+port的方式拼接url调用服务

@AutowiredLoadBalancerClient loadBalancerClient;
@Autowiredprivate RestTemplate restTemplate;
@RequestMappingpublic String ribbon(){ ServiceInstance serviceInstance = loadBalancerClient.choose("demoServer"); String ip = serviceInstance.getHost(); int port = serviceInstance.getPort(); return restTemplate.getForObject("http://"+ip+":"+port + "/hello",String.class,"");}


一文搞懂ribbon负载均衡

2、Ribbon的原理


在RibbonAutoConfiguration(org.springframework.cloud.netflix.ribbon包中)自动配置类中,通过@Bean注册了SpringClientFactory和LoadBalancerClient对象到spring容器中(源码2.1):

@Autowired(required = false)private List<RibbonClientSpecification> configurations = new ArrayList<>();
@Beanpublic SpringClientFactory springClientFactory() { SpringClientFactory factory = new SpringClientFactory(); factory.setConfigurations(this.configurations); return factory;}
@Bean@ConditionalOnMissingBean(LoadBalancerClient.class)public LoadBalancerClient loadBalancerClient() { return new RibbonLoadBalancerClient(springClientFactory());}

SpringClientFactory:工厂类(负载均衡器、服务配置类等),内部包含了各种获取对象,如loadBalance,instance等方法。

LoadBalanceClient:负载均衡器的客户端调用入口,定义了各种方法,如果URI获取,服务实例的选择(choose)等,默认是RibbonLoadBalanceClient,包含了SpringClientFactory;


一文搞懂ribbon负载均衡

2.1、服务相关配置类


在源码2.1中第7行中SpringClientFactory 对象设置了RibbonClientSpecification对象的集合,该集合通过@Autowired注入。那该集合是何时添加到IOC容器中的呢?在第一节的第三步中,在启动类中添加了@RibbonClient,并指定了服务名,它的源码(源码2.2)如下

@Configuration(proxyBeanMethods = false)@Import(RibbonClientConfigurationRegistrar.class)@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface RibbonClient { String value() default ""; String name() default ""; Class<?>[] configuration() default {};}

看到上述代码第2行:

@Import(RibbonClientConfigurationRegistrar.class)

RibbonClientConfigurationRegistrar的作用就是将@RibbonClient 注解指定的服务(name属性指定的服务)的配置类(configuration属性)以RibbonClientzaiSpecification对象注册到springIOC容器中,如:

@Configuration@RibbonClient(name = "foo", configuration = FooConfiguration.class)public class TestConfiguration {}

其中name是服务名,FooConfiguration是必须是@Configuration注解指定的类,但请注意,它不在主应用程序上下文的@ComponentScan中,否则将由所有@RibbonClients共享,意思是FooConfiguration不应该被spring容器扫描到。configuration 属性时可以不指定的,默认为RibbonClientConfiguration,在配置类中,可以个性化定义服务的IRule负载均衡策略、IPing机制等,对于IRule和IPing将分别在第3节和第4节中讲解。


一文搞懂ribbon负载均衡

2.2、服务的负载均衡


在第1节第四步中通过LoadBalancerClient(在源码2.1中可以看出,默认的实现类是RibbonLoadBalancerClient) 的choose方法获取服务Server对象(源码2.3):

public ServiceInstance choose(String serviceId, Object hint) { Server server = getServer(getLoadBalancer(serviceId), hint); if (server == null) { return null; } return new RibbonServer(serviceId, server, isSecure(server, serviceId), serverIntrospector(serviceId).getMetadata(server));}

在上述源码中的第2行,在getServer方法(获取服务)之前,首先通过getLoadBalancer方法获取负载均衡器:

protected ILoadBalancer getLoadBalancer(String serviceId) { return this.clientFactory.getLoadBalancer(serviceId);}

其中this.clientFactory即源码2.1清单中通过@Bean注册的SpringClientFactory对象,也就是说负载均衡器是通过SpringClientFactory中获得的,继续跟踪代码,最终到NamedContextFactory(抽象类,SpringClientFactory的父类)类的getInstance方法:


protected AnnotationConfigApplicationContext createContext(String name) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); if (this.configurations.containsKey(name)) { for (Class<?> configuration : this.configurations.get(name) .getConfiguration()) { context.register(configuration); } } ...省略部分代码 context.register(PropertyPlaceholderAutoConfiguration.class, this.defaultConfigType); ...省略部分代码 return context;}

从代码中可以看到,是创建了一个AnnotationConfigApplicationContext 的一个子容器必须(spring容器的子容器,同springMVC子容器),首先判断this.configurations中是否有关于服务的配置类,即2.1节中讲解到@RibbonClient注解指定的的配置,如果不存在则为代码第11行的this.defaultConfigType,它就是RibbonClientConfiguration,这也印证了2.1节。


一文搞懂ribbon负载均衡

2.3、总结


总结下Ribbon负载均衡初始化过程:

  • Ribbon在启动时,会加载@RibbonClient注解指定的服务配置(如果有,并非必须),配置类必须是@Configuration,建议该配置类不应该被spring扫描到容器中,否则就是对所以服务有效

  • Ribbon在启动时,注册LoadBalanceClient和SpringClientFactory对象。LoadBalanceClient默认是RibbonLoadBalanceClient,它提供的choose方法是客户端负载均衡的入口;SpringClientFactory是工厂类,提供了服务实例以及负载均衡器的获取。


一文搞懂ribbon负载均衡

3、Ribbon的IRule策略


在2.1节最后,提到可以为每个服务提供IRule,它是Ribbon提供的负载均衡算法,在默认的配置类RibbonClientConfiguration中,注册了默认的负载均衡算法:

@Bean@ConditionalOnMissingBeanpublic IRule ribbonRule(IClientConfig config) { if (this.propertiesFactory.isSet(IRule.class, name)) { return this.propertiesFactory.get(IRule.class, config, name); } ZoneAvoidanceRule rule = new ZoneAvoidanceRule(); rule.initWithNiwsConfig(config); return rule;}

从上述代码中可以看到,如果不指定IRule算法,则默认使用ZoneAvoidanceRule算法策略,他是区域规避策略。除了ZoneAvoidanceRule算法策略,Ribbon还提供了如下策略:

名称

描述

RoundRobinRule

轮询策略,如果没有配置区域,则默认使用该策略

BestAvailableRule

最小调用数策略

RandomRule

随机数策略

RetryRule

重试策略

WeightedResponseTimeRule

权重策略

AvailabilityFilteringRule

线性抽样策略


当然在项目中,也可以实现自定义的负载均衡策略,只要实现Rule接口,如果用到所以服务中,则可以使用@Bean的方式注册到容器中,如果需要给某个服务中,则可以是@RibbonClient注解应用到目标服务中。


一文搞懂ribbon负载均衡

4、Ribbon的IPing机制


IPing机制,相当于心跳,每隔一段时间判断服务实例是否正常。IPing是一个接口,实现不同的IPing规则,只需要实现IPing接口即可,然后在isAlive中判断服务是否正常。IP机制的装载流程分,在未引入Eureka和引入Eureka时有所不同。


一文搞懂ribbon负载均衡

4.1、未引入Eureka


在RibbonClientConfiguration中,通过@Bean注册了默认的IPing实例对象:

@Bean@ConditionalOnMissingBeanpublic IPing ribbonPing(IClientConfig config) { if (this.propertiesFactory.isSet(IPing.class, name)) { return this.propertiesFactory.get(IPing.class, config, name); } return new DummyPing();}

从代码中看出,也是先判断是否有配置其他的IPing,如果没有,则创建了DummyPing对象,它的isAlive直接返回true,即在没有引入Eureka时,默认使用DummyPing,全部返回true。


一文搞懂ribbon负载均衡

4.2、引入Eureka


在引入Eureka后,在Eureka客户端的EurekaRibbonClientConfiguration(包:

org.springframework.cloud.netflix.ribbon.eureka中),同样的方式注册了IPing:

@Bean@ConditionalOnMissingBeanpublic IPing ribbonPing(IClientConfig config) { if (this.propertiesFactory.isSet(IPing.class, serviceId)) { return this.propertiesFactory.get(IPing.class, config, serviceId); } NIWSDiscoveryPing ping = new NIWSDiscoveryPing(); ping.initWithNiwsConfig(config); return ping;}

从上述代码中,可以看出,如果没有自定义配置IPing,默认使用的是NIWSDiscoveryPing,它通过Eureka的状态返回服务是否正常。


5、Ribbon与RestTemplate


如果调用各服务都需要向第1节的步骤四的方式,肯定比较复杂,更简单的写法是在RestTemplate上加上@LoadBalanced注解:

@Bean@LoadBalancedpublic RestTemplate restTemplate(){ return new RestTemplate();}

然后步骤四的代码就可以改成:

@Autowiredprivate RestTemplate restTemplate;
@RequestMappingpublic String ribbon(){ return restTemplate.getForObject("http://demoServer/hello",String.class,"");}

在restTemplate调用服务的url上直接使用服务名即可,这里留一个思考,为什么RestTemplate加上@LoadBalanced注解之后,就有了负载均衡的功能?我将在下一篇文章中分析它的原理,敬请期待。