一文搞懂ribbon负载均衡
在分布式开发中,如果服务的提供者有多个实例(集群),那么服务消费者如何去选择一个服务实例进行调用。那就需要提供一种策略,来帮助消费者选择一个服务实例,这种策略称为负载均衡策略。负载均衡分类:
类型 |
产品实现 |
|
服务端负载均衡 |
软件负载均衡 |
Nginx、LVS |
硬件负载均衡 |
F5 |
|
客户端负载均衡 |
Ribbon |
在服务端负载均衡中,提供专门的服务器(单个或者集群)来作为负载均衡服务,服务消费者将请求发送到负载均衡服务中,然后通过负载均衡算法来选择其中一个目标服务实例,然后由负载均衡器转发到目标服务中。硬件服务负载均衡器相对比软件负载均衡要安全一点,当然也是比较贵的,一般政府、国企等一些土豪单位才用的起。
客户端负载均衡是指每个服务消费者(客户端)都具有负载均衡的能力,每一个服务消费者通过负载均衡算法从本地保存的服务实例(每个服务消费者都会在本地保存一份服务清单,服务清单一般来源于注册中心(如Eureka、Zookeeper、Consul等)选择一个调用。
本文主要讲解的是客户端负责均衡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调用服务
LoadBalancerClient loadBalancerClient;
private RestTemplate restTemplate;
public String ribbon(){
ServiceInstance serviceInstance = loadBalancerClient.choose("demoServer");
String ip = serviceInstance.getHost();
int port = serviceInstance.getPort();
return restTemplate.getForObject("http://"+ip+":"+port + "/hello",String.class,"");
}
2、Ribbon的原理
在RibbonAutoConfiguration(org.springframework.cloud.netflix.ribbon包中)自动配置类中,通过@Bean注册了SpringClientFactory和LoadBalancerClient对象到spring容器中(源码2.1):
false) (required =
private List<RibbonClientSpecification> configurations = new ArrayList<>();
public SpringClientFactory springClientFactory() {
SpringClientFactory factory = new SpringClientFactory();
factory.setConfigurations(this.configurations);
return factory;
}
(LoadBalancerClient.class)
public LoadBalancerClient loadBalancerClient() {
return new RibbonLoadBalancerClient(springClientFactory());
}
SpringClientFactory:工厂类(负载均衡器、服务配置类等),内部包含了各种获取对象,如loadBalance,instance等方法。
LoadBalanceClient:负载均衡器的客户端调用入口,定义了各种方法,如果URI获取,服务实例的选择(choose)等,默认是RibbonLoadBalanceClient,包含了SpringClientFactory;
2.1、服务相关配置类
在源码2.1中第7行中SpringClientFactory 对象设置了RibbonClientSpecification对象的集合,该集合通过@Autowired注入。那该集合是何时添加到IOC容器中的呢?在第一节的第三步中,在启动类中添加了@RibbonClient,并指定了服务名,它的源码(源码2.2)如下
public RibbonClient {
String value() default "";
String name() default "";
Class<?>[] configuration() default {};
}
看到上述代码第2行:
@Import(RibbonClientConfigurationRegistrar.class)
RibbonClientConfigurationRegistrar的作用就是将@RibbonClient 注解指定的服务(name属性指定的服务)的配置类(configuration属性)以RibbonClientzaiSpecification对象注册到springIOC容器中,如:
public class TestConfiguration {
}
其中name是服务名,FooConfiguration是必须是@Configuration注解指定的类,但请注意,它不在主应用程序上下文的@ComponentScan中,否则将由所有@RibbonClients共享,意思是FooConfiguration不应该被spring容器扫描到。configuration 属性时可以不指定的,默认为RibbonClientConfiguration,在配置类中,可以个性化定义服务的IRule负载均衡策略、IPing机制等,对于IRule和IPing将分别在第3节和第4节中讲解。
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节。
2.3、总结
总结下Ribbon负载均衡初始化过程:
Ribbon在启动时,会加载@RibbonClient注解指定的服务配置(如果有,并非必须),配置类必须是@Configuration,建议该配置类不应该被spring扫描到容器中,否则就是对所以服务有效
Ribbon在启动时,注册LoadBalanceClient和SpringClientFactory对象。LoadBalanceClient默认是RibbonLoadBalanceClient,它提供的choose方法是客户端负载均衡的入口;SpringClientFactory是工厂类,提供了服务实例以及负载均衡器的获取。
3、Ribbon的IRule策略
在2.1节最后,提到可以为每个服务提供IRule,它是Ribbon提供的负载均衡算法,在默认的配置类RibbonClientConfiguration中,注册了默认的负载均衡算法:
public 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注解应用到目标服务中。
4、Ribbon的IPing机制
IPing机制,相当于心跳,每隔一段时间判断服务实例是否正常。IPing是一个接口,实现不同的IPing规则,只需要实现IPing接口即可,然后在isAlive中判断服务是否正常。IP机制的装载流程分,在未引入Eureka和引入Eureka时有所不同。
4.1、未引入Eureka
在RibbonClientConfiguration中,通过@Bean注册了默认的IPing实例对象:
public 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。
4.2、引入Eureka
在引入Eureka后,在Eureka客户端的EurekaRibbonClientConfiguration(包:
org.springframework.cloud.netflix.ribbon.eureka中),同样的方式注册了IPing:
public 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注解:
public RestTemplate restTemplate(){
return new RestTemplate();
}
然后步骤四的代码就可以改成:
private RestTemplate restTemplate;
public String ribbon(){
return restTemplate.getForObject("http://demoServer/hello",String.class,"");
}
在restTemplate调用服务的url上直接使用服务名即可,这里留一个思考,为什么RestTemplate加上@LoadBalanced注解之后,就有了负载均衡的功能?我将在下一篇文章中分析它的原理,敬请期待。