四、如何自定义Ribbon负载均衡策略
4.1 自定义算法
import com.netflix.client.config.IClientConfig; import com.netflix.loadbalancer.AbstractLoadBalancerRule; import com.netflix.loadbalancer.ILoadBalancer; import org.springframework.stereotype.Component; import com.netflix.loadbalancer.Server; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; @Component public class RandomRule_ZY extends AbstractLoadBalancerRule { //定义一个原子类,以保证原子性 private AtomicInteger atomicInteger =new AtomicInteger(0); public void initWithNiwsConfig(IClientConfig iClientConfig) { } public Server choose(ILoadBalancer lb, Object key) { if (lb == null){ return null; } //用于统计获取次数,当达到一定数量就不再去尝试 int count = 0; Server server = null; //当服务还没获取到,并且尝试没有超过8次 while (server == null && count++ < 8){ //获取服务 List<Server> allServers = lb.getAllServers(); List<Server> reachableServers = lb.getReachableServers(); int allServersSize = allServers.size(); int reachableServersSize = reachableServers.size(); //如果获取的服务list都为0就返回null if(allServersSize == 0 || reachableServersSize == 0){ return null; } //获取服务下标 int next = getServerIndex(allServersSize); //获取服务 server = reachableServers.get(next); //如果服务为空直接跳过下面的 if (server == null){ continue; } //如果获取到的这个服务是活着的就返回 if (server.isAlive()){ return server; } //如果获取到的服务不是空,但是不是存活状态,需要重新获取 server=null; } //最后这里可能会返回null return server; } //获取服务下标,为了保证原子性,使用了CAS public int getServerIndex(int allServersSize){ //自旋锁 for (;;) { //获取当前值 int current = this.atomicInteger.get(); //设置期望值 int next = (current + 1) % allServersSize; //调用Native方法compareAndSet,执行CAS操作 if (this.atomicInteger.compareAndSet(current, next)) //成功后才会返回期望值,否则无线循环 return next; } } public Server choose(Object key) { return choose(getLoadBalancer(),key); } }
4.2 自定义配置类
@Configuration //针对全局修改 //@RibbonClients(defaultConfiguration = MyRuleConfig.class) //针对某个服务修改 @RibbonClient(name = "nacos-app-a", configuration = MyRuleConfig.class) public class MyRuleConfig { @Bean public IRule rule() { //返回上面自定义的规则类 return new RandomRule_ZY();
4.3 设置加载自定义Ribbon配置类
@SpringBootApplication @EnableEurekaClient @RibbonClient(name = "Client",configuration = MyRuleConfig.class) public class GoyeerCloudRibbonAppliction { public static void main(String[] args){ SpringApplication.run(GoyeerCloudRibbonAppliction.class); } }
五、LoadBalancer–负载均衡器的核心
Ribbon实现负载均衡是通过LoadBalancer注解来给RestTemplate标记,来实现负载均衡。LoadBalancer是如何实现负载均衡哪?查看源码我们可以看到有一个接口LoadBalancerClinet来实现的。
5.1 什么是LoadBalancerClient
LoadBalancerClient 是 SpringCloud 提供的一种负载均衡客户端,Ribbon 负载均衡组件内部也是集成了 LoadBalancerClient 来实现负载均衡。
5.2 LoadBalancerClient原理
LoadBalancerClinet在初始化时会通过Euraka Clinet向Eureka服务端获取所有的服务实例的注册信息并缓存到本地,并且每10秒向EurakaClinet发送“Ping”请求,来判断服务的可用性。如果服务的可用性发生了改变或者服务数量和之前的不一致,则更新或重新拉取最新到本地。在得到最新服务注册信息后,ILoadBalancer根据IRule的策略进行负载均衡(默认策略为轮询)。
当使用LoadBalancerClient进行远程调用的负载均衡时,LoadBalancerClient先通过目标服务名在本地服务注册清单中获取服务提供方的某个实例,如多个服务器节点,LoadBalancerClient会通过choose()方法获取到多个节点中一个服务,拿到服务的信息之后取出服务IP信息,就可以得到完整的想要访问的IP地址和端口号,最后通过RestTempate访问具体的服务信息。
5.3 LoadBabancerClient源码解析
5.3.1 LoadBalancerClient 类图
LoadBalancerClient 是 Spring Cloud 提供的一个非常重要的接口,它继承ServiceInstanceChooser 接口,该接口的实现类是 RibbonLoadBalanceClient,它们之间的关系如下图所示:
5.3.2 LoadBalancerClient 接口源码
public interface LoadBalancerClient extends ServiceInstanceChooser { <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException; <T> T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest<T> request) throws IOException; URI reconstructURI(ServiceInstance instance, URI original); }
可以发现 LoadBalancerClient 接口继承了 ServiceInstanceChooser 接口,包含两个方法:execute和reconstructURI
- execute() 用来执行Request请求
- reconstructURI() 用来重构URL
5.3.3 ServiceInstanceChooser 接口源码
public interface ServiceInstanceChooser { ServiceInstance choose(String serviceId); }
ServiceInstanceChooser 接口中的主要方法为 choose(),该方法用于根据服务的名称 serviceId 来选择其中一个服务实例,即根据 serviceId 获取ServiceInstance。
5.3.4 RibbonLoadBalanceClient 实现类源码
LoadBalancerClient 的实现类 RibbonLoadBalanceClient,它用来执行最终的负载均衡请求。其中,RibbonLoadBalanceClient 的一个 choose() 方法用于选择具体的服务实例,其内部是通过 getServer() 方法交给 ILoadBalancer 完成的。
@Override public ServiceInstance choose(String serviceId) { return choose(serviceId, null); } /** * New: Select a server using a 'key'. * @param serviceId of the service to choose an instance for * @param hint to specify the service instance * @return the selected {@link ServiceInstance} */ 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)); }
protected Server getServer(ILoadBalancer loadBalancer) { return getServer(loadBalancer, null); } protected Server getServer(ILoadBalancer loadBalancer, Object hint) { if (loadBalancer == null) { return null; } // 最终通过 loadBalancer 去做服务实例的选择。 // Use 'default' on a null hint, or just pass it on? return loadBalancer.chooseServer(hint != null ? hint : "default"); }
5.3.5 BaseLoadBalancer 源码
5.3.6 IRule 接口源码
public interface IRule{ public Server choose(Object key); public void setLoadBalancer(ILoadBalancer lb); public ILoadBalancer getLoadBalancer(); }
IRule 接口定义了3个方法,分别是:choose、setLoadBalancer和getLoadBalancer
- choose() 是用来选择实例的
- setLoadBalancer()用来设置负载均衡规则
- getLoadBalancer()获取负载均衡规则
实现IRule有个类,分别定义不同的负载均衡规则:
- 随机策略 RandomRule
- 轮询策略 RoundRobinRule
- 重试策略 RetryRule
- 可用过滤策略 PredicateBaseRule
- 响应时间权重策略 WeightedRespinseTimeRule
- 并发量最小可用策略 BestAvailableRule
- 区域权重策略 ZoneAvoidanceRule
5.3.7 ILoadBalancer 源码
ILoadBalancer 是一个接口,该接口定义了一系列实现负载均衡的方法,LoadBalancerClient 的实现类 RibbonLoadBalanceClient 也将负载均衡的具体实现交给了 ILoadBalancer 来处理,ILoadBalancer 通过配置 IRule、IPing 等,向 EurekaClient 获取注册列表信息,默认每10秒向 EurekaClient 发送一次 “ping”,进而检查是否需要更新服务的注册列表信息。最后,在得到服务注册列表信息后,ILoadBalancer 根据 IRule 的策略进行负载均衡。
查看 BaseLoadBalancer 和 DynamicServerListLoadBalancer 源码,默认情况下实现了以下配置:
- IClientConfig clientConfig:用于配置负载均衡客户端,默认实现类是 DefaultClientConfigImpl。
- IRule rule:用于配置负载均衡的策略,默认使用的是 RoundRobinRule 轮询策略。
- IPing ping:用于检查当前服务是否有响应,从而判断当前服务是否可用,默认实现类是 DummyPing,该实现类的 isAlive() 方法返回值是 true,默认所有服务实例都是可用的。
- ServerList serverList:用于获取所有 Server 注册列表信息。通过跟踪源码会发现,ServerList 的实现类是 DiscoveryEnabledNIWSServerList,该类定义的 obtainServersViaDiscovery() 方法是根据 eurekaClientProvider.get() 方法获取 EurekaClient,再根据 EurekaClient 获取服务注册列表信息。EurekaClient 的实现类是DiscoveryClient,DiscoveryClient 具有服务注册、获取服务注册列表等功能。
- ServerListFilter filter:定义了根据配置过滤或者动态获取符合条件的服务列表,默认实现类是 ZonePreferenceServerListFilter,该策略能够优先过滤出与请求调用方处于同区域的服务实例。
六、Ribbon的配置参数
控制参数 | 说明 | 默认值 |
<service-name> .ribbon.NFLoadBalancerPingInterval |
Ping定时任务周期 | 30s |
service-name> .ribbon.NFLoadBalancerMaxTotalPingTime |
Ping超时时间 | 2s |
<service-name> .ribbon.NFLoadBalancerRuleClassName |
IRule实现类 | RoundRobinRule,基于轮询调度算法规则选择服务实例 |
<service-name> .ribbon.NFLoadBalancerPingClassName |
IPing实现类 | DummyPing,直接返回true |
<service-name> .ribbon.NFLoadBalancerClassName |
负载均衡器实现类 | 2s |
<service-name> .ribbon.NIWSServerListClassName |
ServerList实现类 | ConfigurationBasedServerList,基于配置的服务列表 |
<service-name> .ribbon.ServerListUpdaterClassName |
服务列表更新类 | PollingServerListUpdater |
<service-name> .ribbon.NIWSServerListFilterClassName |
服务实例过滤器 | 2s |
<service-name> .ribbon.NIWSServerListFilterClassName |
服务实例过滤器 | 2s |
<service-name> .ribbon.ServerListRefreshInterval |
服务列表刷新频率 | 2s |
<service-name> .ribbon.NFLoadBalancerClassName |
自定义负载均衡器实现类 | 2s |
七、总结
Ribbon是Spring cloud的核心,负载微服务内负载调用;Ribbon可以脱离Spring Cloud的单独使用。
Ribbon是微服务整个微服务组件最复杂的一环,控制流程上为保证服务的高可用性,有比较多的细节参数控制,在使用的过程中需要深入理清每个环节的处理机制,使之发挥稳定且高效的作用。