Spring Cloud Ribbon 是一个客户端负载均衡框架,可以结合 Spring Cloud Eureka 使用。
主要特点
- 负载均衡: Ribbon 提供了多种内置的负载均衡策略(如轮询、随机等),也可以自定义这些策略,以满足不同的业务需求。
- 与 Eureka 集成: Ribbon 可以无缝地与 Spring Cloud Eureka 集成,自动从 Eureka Server 获取服务列表,并根据负载均衡策略选择合适的服务实例进行调用。
- 重试机制: 支持配置请求失败后的重试逻辑,提高系统可靠性。
简单使用示例
pom.xml 添加依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
在 application.yml 中配置 Eureka 客户端,注册到 Eureka Server:
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
创建一个 RestTemplate Bean,并通过 @LoadBalanced 注解来启用 Ribbon 负载均衡:
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class Config {
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
使用 RestTemplate 进行服务调用:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
@Service
public class GreetingService {
private final RestTemplate restTemplate;
@Autowired
public GreetingService(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
public String getGreeting() {
// 注意这里直接使用服务名
return restTemplate.getForObject("http://greeting-service/api/greeting", String.class);
}
}
核心原理
下文源码均基于 spring-cloud-starter-netflix-ribbon : 2.1.6.RELEASE
核心组件
ILoadBalancer:这是 Ribbon 中用于管理服务实例列表并根据某种策略选择实例的核心接口。它负责维护所有已知的服务实例,并提供方法来获取这些实例。IRule:定义了从可用的服务实例列表中选择一个实例的逻辑。Ribbon 提供了多种规则实现,如轮询(RoundRobinRule)、随机(RandomRule)等。IPing:用于检查服务实例是否存活。通过定期 ping 服务实例来确定它们的状态是 UP 还是 DOWN 。ServerListUpdater:负责更新服务实例列表。当使用与服务发现组件(如 Eureka )集成时,它会定期刷新服务器列表以反映最新的服务状态。
// BaseLoadBalancer 继承自 AbstractLoadBalancer。 AbstractLoadBalancer是实现了 ILoadBalancer 接口抽象基类
public class BaseLoadBalancer extends AbstractLoadBalancer implements
PrimeConnections.PrimeConnectionListener, IClientConfigAware {
// 默认负载均衡规则:轮询策略
private final static IRule DEFAULT_RULE = new RoundRobinRule();
// 负载均衡规则
protected IRule rule = DEFAULT_RULE;
// 默认 Ping 策略:SerialPingStrategy,按顺序逐一 Ping 所有 Server
private final static SerialPingStrategy DEFAULT_PING_STRATEGY = new SerialPingStrategy();
// Ping 策略
protected IPingStrategy pingStrategy = DEFAULT_PING_STRATEGY;
// IPing 接口:定义如何判断 Server 是否“存活”。比如 NIWSDiscoveryPing:结合 Eureka 注册中心判断是否在线
protected IPing ping = null;
// allServerList:所有已知的 Server 列表(包括 Up 和 Down)
@Monitor(name = PREFIX + "AllServerList", type = DataSourceType.INFORMATIONAL)
protected volatile List<Server> allServerList = Collections
.synchronizedList(new ArrayList<Server>());
// upServerList:当前健康的 Server 列表
@Monitor(name = PREFIX + "UpServerList", type = DataSourceType.INFORMATIONAL)
protected volatile List<Server> upServerList = Collections
.synchronizedList(new ArrayList<Server>());
// (省略其他)...
}
// DynamicServerListLoadBalancer 继承自 BaseLoadBalancer
// 扩展功能:支持动态更新服务列表,适用于服务注册中心场景(如 Eureka、Consul 等)
// DynamicServerListLoadBalancer 的工作流程:
// [定时任务/事件通知] -> ServerListUpdater
// -> 触发updateAction.doUpdate()
// -> updateListOfServers()
// -> serverListImpl.getServerList() 获取原始服务列表
// -> filter.getFilteredListOfServers(...) 过滤
// -> BaseLoadBalancer.setServers(List<Server>) 更新内部服务实例列表
public class DynamicServerListLoadBalancer<T extends Server> extends BaseLoadBalancer {
// 负责从服务注册中心拉取最新的服务实例列表。支持定期刷新机制,保证本地缓存与注册中心一致
// ServerList 常见实现:DiscoveryEnabledNIWSServerList(结合 Eureka 使用)、ConfigurationBasedServerList(基于静态配置的服务列表)
volatile ServerList<T> serverListImpl;
// 服务列表过滤器,用于在从注册中心获取原始服务列表后,对其进行筛选,只保留符合要求的实例,例如:剔除不健康的节点、按 Zone 过滤等。
// ServerListFilter 常见实现:ZoneAffinityServerListFilter(根据区域亲和性过滤)、NoOpServerListFilter(不过滤)
volatile ServerListFilter<T> filter;
// UpdateAction 接口定义了当需要更新服务列表时要执行的动作,由 ServerListUpdater 触发调用
protected final ServerListUpdater.UpdateAction updateAction = new ServerListUpdater.UpdateAction() {
@Override
public void doUpdate() {
// 实际更新服务列表
updateListOfServers();
}
};
// 负责定时触发服务列表的更新
// 主要实现:PollingServerListUpdater(默认实现,通过定时任务周期性地更新服务列表)
protected volatile ServerListUpdater serverListUpdater;
// (省略其他)...
}
public interface ServerList<T extends Server> {
public List<T> getInitialListOfServers();
/**
* Return updated list of servers. This is called say every 30 secs
* (configurable) by the Loadbalancer's Ping cycle
*
*/
public List<T> getUpdatedListOfServers();
}
组件关系:
工作流程
- 初始化阶段:
- 拦截器注册:
RestTemplate上的@LoadBalanced注解会触发相应的自动配置逻辑,这些逻辑包括注册一个特殊的拦截器 ——LoadBalancerInterceptor,这个拦截器会在每次使用RestTemplate发起HTTP请求时被调用,实现与负载均衡器集成,使得我们可以直接使用服务名而不是具体的 URL 来发起 HTTP 请求。LoadBalancerInterceptor内部使用了LoadBalancerClient接口的实现类(如RibbonLoadBalancerClient)来解析服务名并选择合适的服务实例,在这个过程中,它利用了ILoadBalancer、IRule等组件来进行负载均衡决策。 - 负载均衡组件初始化:创建
ILoadBalancer实例,实例内部会初始化一个ServerList,这通常是通过服务发现组件(如 Eureka )获取的一组服务实例。同时,依据配置或默认设置,初始化IRule和IPing对象。
- 拦截器注册:
- 服务调用阶段:
- 当通过
RestTemplate发起一个 HTTP 请求,并且 URL 中的host部分是一个服务名而不是具体的 IP 地址或域名时,会触发LoadBalancerInterceptor拦截器。LoadBalancerInterceptor会将服务名传递给LoadBalancerClient,后者内部会通过ILoadBalancer获取该服务的所有可用实例列表(通过查询 Eureka 或其他服务发现机制获取)。ILoadBalancer使用IPing来过滤掉不可用的服务实例,然后应用IRule负载均衡策略(默认是轮询)来从剩余的可用服务实例中选择一个实例。 - 选定实例后,最后会将原始请求的 URL 替换为所选实例的具体地址( IP + 端口),然后执行 HTTP 请求。
- 当通过
- 健康检查与更新:
IPing会定期检查每个服务实例的健康状况,并将不健康的实例标记为不可用。ServerListUpdater也会定期更新服务实例列表,确保其反映最新的服务状态。
public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor {
private LoadBalancerClient loadBalancer;
// (省略其他) ...
@Override
public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
final ClientHttpRequestExecution execution) throws IOException {
// 获取原始请求的 URI
final URI originalUri = request.getURI();
// 从 URI 中提取主机名部分作为服务名。在微服务架构中,这个“主机名”实际上是服务的逻辑名称,而非实际的物理地址。
String serviceName = originalUri.getHost();
Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri);
// 通过 LoadBalancerClient 执行负载均衡逻辑
return this.loadBalancer.execute(serviceName, requestFactory.createRequest(request, body, execution));
}
}
public class RibbonLoadBalancerClient implements LoadBalancerClient {
// (省略其他) ...
/**
* 执行负载均衡策略, 然后发起请求
*/
@Override
public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {
// 获取 ILoadBalancer 实例(比如:继承了DynamicServerListLoadBalancer 的 ZoneAwareLoadBalancer )
// ILoadBalancer 对象包含了特定服务的所有可用实例及其健康状态等信息
ILoadBalancer loadBalancer = getLoadBalancer(serviceId);
// 从 loadBalancer 中选择一个服务实例。此方法内部会通过 ILoadBalancer 使用 IRule 策略来选择具体的 Server 实例(即服务实例)。
// IRule 定义了不同的负载均衡算法,例如轮询(RoundRobinRule)、随机(RandomRule)等。默认情况下,使用的是轮询策略。
Server server = getServer(loadBalancer);
// 如果没有可用的服务实例,则抛出异常
if (server == null) {
throw new IllegalStateException("No instances available for " + serviceId);
}
// 创建一个 RibbonServer 对象,封装了服务ID、选定的服务实例、安全标志以及元数据信息
RibbonServer ribbonServer = new RibbonServer(serviceId, server, isSecure(server,
serviceId), serverIntrospector(serviceId).getMetadata(server));
// 实际执行请求
return execute(serviceId, ribbonServer, request);
}
/**
* 对服务实例发起请求
* @param serviceId 服务名,如 greeting-service
* @param serviceInstance 中的服务实例,通常为 RibbonServer 类型
* @param request 封装了实际请求逻辑的函数式接口
*/
public <T> T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest<T> request) throws IOException {
Server server = null;
// 获取 Server 实例,包含了服务实例的主机名、端口等信息
if(serviceInstance instanceof RibbonServer) {
server = ((RibbonServer)serviceInstance).getServer();
}
if (server == null) {
throw new IllegalStateException("No instances available for " + serviceId);
}
// 获取负载均衡上下文,该上下文对象封装了 Ribbon 的核心组件(如 ILoadBalancer, IClientConfig, ServerIntrospector 等),并在执行请求时提供上下文支持。
RibbonLoadBalancerContext context = this.clientFactory
.getLoadBalancerContext(serviceId);
// 创建统计记录器,用于记录请求的成功、失败、响应时间等统计信息。它会更新 LoadBalancerStats 中的数据,供后续的健康检查、负载均衡策略使用。
RibbonStatsRecorder statsRecorder = new RibbonStatsRecorder(context, server);
try {
// 实际执行 HTTP 请求的逻辑
T returnVal = request.apply(serviceInstance);
statsRecorder.recordStats(returnVal);
return returnVal;
}
// catch IOException and rethrow so RestTemplate behaves correctly
catch (IOException ex) {
statsRecorder.recordStats(ex);
throw ex;
}
catch (Exception ex) {
statsRecorder.recordStats(ex);
ReflectionUtils.rethrowRuntimeException(ex);
}
return null;
}
}
流程总结: