自动化配置
由于 Ribbon 中定义的每一个接口都有多种不同的策略实现,同时这些接口之间又有一定的依赖关系,Spring Cloud Ribbon 中的自动化配置能够很方便的自动化构建接口的具体实现,接口如下:
- IClientConfig:Ribbon 的客户端配置,默认采用 com.netflix.client.config.DefaultClientConfigImpl 实现。
- IRule:Ribbon 的负载均衡策略,默认采用 com.netflix.loadbalancer.ZoneAvoidanceRule 实现,该策略能够在多区域环境下选择最佳区域的实例进行访问
- IPing:Ribbon 的实例检查策略,默认采用 com.netflix.loadbalancer.NoOpPing 实现,该检查策略是一个特殊的实现,实际上他并不会检查实例是否可用,而是始终返回 true ,默认认为所有服务实例都是可以使用
- ServerList<Server>:服务实例清单的维护机制,默认采用 com.netflix.loadbalancer.ConfigurationBasedServerList 实现。
- ServerListFilter<Server>:服务实例清单过滤机制,默认采用 org.springframework.cloud.netflix.ribbon.ZonePreferenceServerListFilter 实现,该策略能够优先过滤出与请求调用方处理同区域的服务实现
- ILoadBalancer:负载均衡器,默认采用 com.netflix.loadbalancer.ZoneAwareLoadBalancer 实现,他具备了区域感知的能力
这些自动化配置内容是在没有引入 Spring Cloud Eureka 等服务治理框架时如此,在同时引入 Eureka 和 Ribbon 依赖时,自动化配置会有一些不同。通过自动化配置的实现,我们可以轻松的实现客户端的负载均衡,同时,针对一些个性化的需求,我们也可以方便的替换上面的这些默认实现,示例代码:
@Configurable
public class RibbonConfiguration {
@Bean
public IRule ribbonRule(IClientConfig clientConfig) {
return new WeightedResponseTimeRule();
}
}
可以使用 @RibbonClient 注解来实现更细粒度的客户端配置,创建 ConsumerHelloserviceApplication 主类,使用 @RibbonClient 注解来指定服务使用 WebServiceRibbonConfiguration 配置,代码如下:
@RibbonClient (name = "ORG.DRSOFT.WEBSERVICE.HELLOSERVICE", configuration = RibbonConfiguration.class)
@EnableDiscoveryClient
@SpringBootApplication
public class ConsumerHelloserviceApplication {
public static void main(String[] args) {
SpringApplication.run(ConsumerHelloserviceApplication.class, args);
}
@LoadBalanced
@Bean
public RestTemplate createRestTemplate() {
return new RestTemplate();
}
}
注意:如果需要使服务单独配置,那么配置类不能放在@ComponentScan所扫描的当前包下以及子包下,否则我们自定义的这个配置类就会被所有的Ribbon客户端所共享,@SpringBootApplication 注解中就包含了 @ComponentScan 注解,因此必须使配置类处于不同的包以及子包。
如果需要设置默认配置或者多个@RibbonClient 注解,可以使用 @RibbonClients 注解,其 defaultConfiguration 可以设置默认的自定义配置,配置类不限定所属包,value 可以设置多个 @RibbonClient 注解,示例代码如下:
@RibbonClients (defaultConfiguration = RibbonConfiguration.class)
@EnableDiscoveryClient
@SpringBootApplication
public class ConsumerHelloserviceApplication {
public static void main(String[] args) {
SpringApplication.run(ConsumerHelloserviceApplication.class, args);
}
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
与 Eureka 结合
当在 Spring Cloud 的应用中同时应用 Spring Cloud Ribbon 和 Spring Cloud Eureka 依赖时,会触发 Eureka 中实现的对 Ribbon 的自动化配置,接口实现如下:
- ServerList<Server>:服务实例清单的维护机制,默认采用 com.netflix.niws.loadbalancer.DiscoveryEnabledNIWSServerList实现,该实现会将服务清单列表交给 Eureka 的服务治理机制来进行维护
- IPing:Ribbon 的实例检查策略,默认采用 com.netflix.niws.loadbalancer.NIWSDiscoveryPing 实现,该实现将实例检查的任务交给了服务治理框架来进行维护
负载均衡策略
IRule 接口其Ribbon 实现了非常多的选择策略,下面我们来详细说明一下IRule 接口的定义如下:
public interface IRule{
public Server choose(Object key);
public void setLoadBalancer(ILoadBalancer lb);
public ILoadBalancer getLoadBalancer();
}
IRule 接口的具体实现:
- AbstractLoadBalancerRule:负载均衡策略的抽象类,在该抽象类中定义了负载均衡器 ILoadBalancer 对象,该对象能够根据在具体实现选择服务策略时,获取到一些负载均衡器中维护的信息来作为分配依据,并以此设计一些算法来实习针对特定场景的高效策略
- RandomRule:该策略实现了从服务实例清单中随机选择一个服务实例的功能,查看源码可以看到 choose 函数实现,使用 Random类的 nextInt 函数来随机获取一个随机数,并将随机数作为座位 upList 的索引值来返回具体实例
public Server choose(ILoadBalancer lb, Object key) {
if (lb == null) {
return null;
}
Server server = null;
while (server == null) {
if (Thread.interrupted()) {
return null;
}
List<Server> upList = lb.getReachableServers();
List<Server> allList = lb.getAllServers();
int serverCount = allList.size();
if (serverCount == 0) {
return null;
}
int index = rand.nextInt(serverCount);
server = upList.get(index);
if (server == null) {
Thread.yield();
continue;
}
if (server.isAlive()) {
return (server);
}
server = null;
Thread.yield();
}
return server;
}
- RoundRobinRule:该策略实现了按照线性轮询的方式依次选择每个服务实例的功能,其详细结构和 RandomRule类似,具体实现如下:
public Server choose(ILoadBalancer lb, Object key) {
if (lb == null) {
log.warn("no load balancer");
return null;
}
Server server = null;
int count = 0;
while (server == null && count++ < 10) {
List<Server> reachableServers = lb.getReachableServers();
List<Server> allServers = lb.getAllServers();
int upCount = reachableServers.size();
int serverCount = allServers.size();
if ((upCount == 0) || (serverCount == 0)) {
log.warn("No up servers available from load balancer: " + lb);
return null;
}
int nextServerIndex = incrementAndGetModulo(serverCount);
server = allServers.get(nextServerIndex);
if (server == null) {
/* Transient. */
Thread.yield();
continue;
}
if (server.isAlive() && (server.isReadyToServe())) {
return (server);
}
// Next.
server = null;
}
if (count >= 10) {
log.warn("No available alive servers after 10 tries from load balancer: " + lb);
}
return server;
}
- WeightedResponseTimeRule:该策略是对 RoundRobinRule的扩展,增加了根据实例的运行情况来计算权重,并根据权重来挑选实例,以达到更优的分配效果,实现主要有三个核心内容:
- 定时任务:在初始化的时候会通过 serverWeightTimer.schedule(new DynamicServerWeightTask(), 0, serverWeightTaskTimerInterval); 启动一个定时任务,用来为每个服务实例计算权重,该任务默认30秒执行一次。
- 权重计算:维护实例的权重计算过程通过 maintainWeights 函数实现,具体代码如下:
public void maintainWeights() {
ILoadBalancer lb = getLoadBalancer();
if (lb == null) {
return;
}
if (!serverWeightAssignmentInProgress.compareAndSet(false, true)) {
return;
}
try {
logger.info("Weight adjusting job started");
AbstractLoadBalancer nlb = (AbstractLoadBalancer) lb;
LoadBalancerStats stats = nlb.getLoadBalancerStats();
if (stats == null) {
return;
}
double totalResponseTime = 0;
for (Server server : nlb.getAllServers()) {
ServerStats ss = stats.getSingleServerStat(server);
totalResponseTime += ss.getResponseTimeAvg();
}
Double weightSoFar = 0.0;
List<Double> finalWeights = new ArrayList<Double>();
for (Server server : nlb.getAllServers()) {
ServerStats ss = stats.getSingleServerStat(server);
double weight = totalResponseTime - ss.getResponseTimeAvg();
weightSoFar += weight;
finalWeights.add(weightSoFar);
}
setWeights(finalWeights);
} catch (Exception e) {
logger.error("Error calculating server weights", e);
} finally {
serverWeightAssignmentInProgress.set(false);
}
}
计算规则为 weightSoFar + totalResponseTime - 实例的平均响应时间,其中 weightSoFar 初始化为零,并且每计算好一个权重值需要累加到 weightSoFar 上供下一次计算使用,假设有4个实例A、B、C、D,他们的平均响应时间为10、40、80、100,所以总响应时间是 230,每个实例的权重为总响应时间与实例自身的平均响应时间的差的累积所得,所以A、B、C、D的权重分别如下所示:
- 实例A:230-10=220
- 实例B:220+(230-40)=410
- 实例C:410+(230-80)=560
- 实例D:560+(230-100)=690
需要注意的是,这里的权重值只表示各实例权重区间的上限,每个实例的权重区间如下:
- 实例A:[0,220]
- 实例B:[220,410]
- 实例C:[410,560]
- 实例D:[560,690]
权重区间的宽度越大,而权重区间的宽度越大,被选中的概率越高
- 实例选择:生成一个[0,最大权重值]区间内的随机数,然后遍历权重列表的索引值去服务实例列表中获取具体的实例。
- ClientConfigEnabledRoundRobinRule:该策略比较特殊,他本身并没有实现什么特殊的处理策略逻辑,在内部定义了一个RoundRobinRule 策略,我们不会直接使用该策略,但可以通过继承该策略,默认的choose实现了线性轮询策略,在子类中做一些高级策略时通常有可能存在一些无法实施的情况,那么就可以用父类的实现作为备选,后面的高级策略都是基于该类的扩展
- BestAvailableRule:该策略继承自 ClientConfigEnabledRoundRobinRule ,在实现中他注入了负载均衡器的统计对象 LoadBalancerStats,同时在具体的 choose 方法中利用 LoadBalancerStats 保存的实例统计信息来选择满足要求的实例,通过负载均衡器中维护的所有服务实例,会过滤掉故障的实例,并找出并发请求最小的一个,所以该策略的特性是可选出最空闲的实例,代码如下:
public Server choose(Object key) {
if (loadBalancerStats == null) {
return super.choose(key);
}
List<Server> serverList = getLoadBalancer().getAllServers();
int minimalConcurrentConnections = Integer.MAX_VALUE;
long currentTime = System.currentTimeMillis();
Server chosen = null;
for (Server server: serverList) {
ServerStats serverStats = loadBalancerStats.getSingleServerStat(server);
if (!serverStats.isCircuitBreakerTripped(currentTime)) {
int concurrentConnections = serverStats.getActiveRequestsCount(currentTime);
if (concurrentConnections < minimalConcurrentConnections) {
minimalConcurrentConnections = concurrentConnections;
chosen = server;
}
}
}
if (chosen == null) {
return super.choose(key);
} else {
return chosen;
}
}