1. 概述
Spring Cloud Ribbon 是基于 Netflix Ribbon 实现的一套客户端-负载均衡的工具。
简单地说,Ribbon 是 Netflix 发布的开源项目,主要功能是提供客户端的软件负载均衡算法和服务调用。Ribbon 客户端组件提供一系列完善的配置项如连接超时、重试等。
Ribbon 目前也进入维护模式,未来的替换方案是 Spring Cloud Starter Loadbalancer。
- 进程内 LB
- 将 LB 逻辑集成到消费方,消费方从服务注册中心获知有哪些地址可用,然后自己再从这些地址中选择出一个合适的服务器;
- Ribbon 就属于进程内 LB,它只是一个类库,集成于消费方进程,消费方通过它来获取到服务提供方的地址(在调用微服务接口的时候,会在注册中心上获取注册信息服务列表之后缓存到 JVM 本地,从而在本地实现 RPC 远程服务调用技术);
- 集中式 LB
- 在服务的消费方和提供方之间使用独立的 LB 设备(可能是硬件,如 F5;也可能是软件,如 Nginx),由该设备负责把访问请求通过某种策略转发至服务的提供方;
2. 说明
Ribbon 架构说明:
Ribbon 在工作时分为两步:
- 先选择 EurekaServer,它优先选择在同一个区域内负载较少的 Server;
- 再根据用户指定的策略,在从 Server 取到的服务注册列表中选择一个地址;
Eureka 已经依赖了 Ribbon:
RestTemplate 说明:
如果需要使用 ip:port
就不需要加 @LoadBalanced
注解,如果需要使用 application.name
访问,那就需要在 restTemplate 配置加上 @LoadBalanced
,因为 @LoadBalanced
这个注解是负载均衡的注解,而负载均衡就是通过访问服务名而实现的。如果你加上这个注解之后使用 restTemplate,那么他就默认你的 localhost 一个模块的服务名称("ip:port"),而不是本机的 IP。总而言之,如果需要使用 ip:port
,就不需要加 @LoadBalanced
;如果需要使用应用名访问,那就需要在 restTemplate 配置加上 @LoadBalanced
。
3. 核心组件 IRule
根据特定算法从服务列表中选取一个要访问的服务。
【测试:当调用指定微服务时使用自定义的 Rule】
- 自定义类放置在主启动类所在包同级的包下(不能放在 @ComponentScan 所扫描的当前包下以及子包下,否则我们自定义的这个配置类就会被所有的 Ribbon 客户端所共享,达不到特殊化定制的目的了);
@Configuration public class MyRibbonConfig { @Bean public IRule myRule(){ return new RandomRule(); } }
- 修改主启动类
@EnableEurekaClient @SpringBootApplication @RibbonClient(name = "CLOUD-PAYMENT-SERVICE", configuration = MyRibbonConfig.class) public class OrderMain8080 { public static void main(String[] args) { SpringApplication.run(OrderMain8080.class, args); } }
4. 负载均衡算法
RoundRobinRule 源码如下:
/*
*
* Copyright 2013 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package com.netflix.loadbalancer;
import com.netflix.client.config.IClientConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
/**
* The most well known and basic load balancing strategy, i.e. Round Robin Rule.
*
* @author stonse
* @author Nikos Michalakis <nikos@netflix.com>
*
*/
public class RoundRobinRule extends AbstractLoadBalancerRule {
private AtomicInteger nextServerCyclicCounter;
private static final boolean AVAILABLE_ONLY_SERVERS = true;
private static final boolean ALL_SERVERS = false;
private static Logger log = LoggerFactory.getLogger(RoundRobinRule.class);
public RoundRobinRule() {
nextServerCyclicCounter = new AtomicInteger(0);
}
public RoundRobinRule(ILoadBalancer lb) {
this();
setLoadBalancer(lb);
}
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;
}
/**
* Inspired by the implementation of {@link AtomicInteger#incrementAndGet()}.
*
* @param modulo The modulo to bound the value of the counter.
* @return The next value.
*/
private int incrementAndGetModulo(int modulo) {
for (;;) {
int current = nextServerCyclicCounter.get();
int next = (current + 1) % modulo;
if (nextServerCyclicCounter.compareAndSet(current, next))
return next;
}
}
@Override
public Server choose(Object key) {
return choose(getLoadBalancer(), key);
}
@Override
public void initWithNiwsConfig(IClientConfig clientConfig) {
}
}
手写一个
- 8001/8002 controller 新增方法:
@GetMapping("/lb") public String testLoadBalance() { return port; }
- 去掉
@LoadBalanced
注解@Configuration public class ApplicationContextConfig { /** * 如果需要使用ip和端口,就不需要加 @LoadBalanced * 如果需要使用应用名访问,那就需要加上 @LoadBalanced * @return */ @Bean // @LoadBalanced public RestTemplate restTemplate() { return new RestTemplate(); } }
- 8080 服务新增接口 MyLoadBalance
public interface MyLoadBalance { /** * 返回下一个要调用的 ServiceInstance * @param serviceInstances 收集服务器总共有多少台能够提供服务的机器,并放到list里面 * @return */ ServiceInstance getNextServer(List<ServiceInstance> serviceInstances); }
- 8080 服务新增接口实现
@Component public class MyLoadBalanceImpl implements MyLoadBalance { private AtomicInteger nextServerIndex = new AtomicInteger(0); private int getNextServerIndex(int serverCount) { int current, next; do { current = this.nextServerIndex.get(); next = (current+1) % serverCount; } while (this.nextServerIndex.compareAndSet(current, next)); return this.nextServerIndex.get(); } @Override public ServiceInstance getNextServer(List<ServiceInstance> serviceInstances) { return serviceInstances.get(getNextServerIndex(serviceInstances.size())); } }
- 8080 服务 controller 新增测试方法
@Resource private MyLoadBalance myLoadBalance; @Resource private DiscoveryClient discoveryClient; @GetMapping("/payment/lb") public CommonResult<String> testLoadBalance() { List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE"); if (instances == null || instances.isEmpty()) { return new CommonResult(HttpStatus.SERVICE_UNAVAILABLE.value(), "无可用服务", null); } ServiceInstance nextServer = myLoadBalance.getNextServer(instances); log.info("LB -> {}", nextServer.getUri()); return new CommonResult(HttpStatus.OK.value(), "负载均衡", restTemplate.getForObject(nextServer.getUri()+"/payment/lb", String.class)); }