这是我参与更文挑战的第17天,活动详情查看:更文挑战
一、前言
先以案例(demo
)入手,再来分析 ribbon
流程,最后怼源码。
(1)Ribbon + RestTemplate
案例
简单介绍下,实现原理:
- 通过在
RestTemplate
上增加@LoadBalanced
添加拦截器 - 拦截器中通过
Ribbon
选取服务实例 - 然后将请求地址中的服务名称替换成
Ribbon
选取服务实例的IP
和 端口
pom.xml
配置文件
<!-- `SpringCloud`中`eureka-client` -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
<version>2.1.2.RELEASE</version>
</dependency>
<!-- 直接导入`ribbon` -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
RestTemplate
中使用
@Configuration
public class RestTemplateConfig {
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
(2)图解 Ribbon
流程
大致流程,如图:
大致流程可分为:
- 注解:自动装配
LoadBalancerAutoConfiguration
- 在自动配置类中,为
RestTemplate
添加拦截器LoadBalancerInterceptor
- 调用请求后,拦截器中获取
host
,并在LoadBalancerClient
中对host
信息进行转换,得到真正的服务器地址。 LoadBalancerClient
中从Eureka client
得到服务实例列表,然后通过包含了负载均衡规则IRule
,选出要发起调用的server
。- 交给负责
HTTP
通讯的组件LoadBalancerRequest
执行真正的HTTP
请求。
二、直接怼源码 - 带问题看源码
主要分为:
ribbon
如何获取服务注册列表?- 获得注册表后,如何持续更新?
- 默认负载均衡算法如何选择一个
server
? - 如何发起一个真正的网络请求?
ping
服务检查服务实例是否存活且有效?
1)ribbon
如何获取服务注册列表?
LoadBalancer
要做负载均衡,那必须得知道这个服务的server list
。
流程图,如下:
源码追踪过程:
之前初始化,已经找到默认
ILoadBalancer
:ZoneAwareLoadBalancer
。
-
先在
ZoneAwareLoadBalancer
(默认的LoadBalancer
) 找,貌似并没有获取服务注册列表的信息。 -
去其父类(
DynamicServerListLoadBalancer
)查找:
// 定位:com.netflix.loadbalancer
public class DynamicServerListLoadBalancer<T extends Server> extends BaseLoadBalancer {
// 通过 bean 注入的
volatile ServerList<T> serverListImpl;
... ...
// 其构造方法:
public DynamicServerListLoadBalancer(...) {
super(clientConfig, rule, ping);
... ...
// 重点
restOfInit(clientConfig);
}
void restOfInit(IClientConfig clientConfig) {
... ...
// 通过这个方法,从 eureka-client 那里获取到 ServiceA 的 server list
updateListOfServers();
... ...
}
public void updateListOfServers() {
List<T> servers = new ArrayList<T>();
if (serverListImpl != null) {
// 获取服务列表
servers = serverListImpl.getUpdatedListOfServers();
... ...
}
updateAllServerList(servers);
}
}
但问题来了:ServerList<T> serverListImpl
如何注入的呢?
肯定是某个
XXXAutoConfiguration
或者XXXConfiguration
,实例化了ServerList
的Bean
- 假的:发现这个
serverList
是从配置文件中读取的。
// 定位:org.springframework.cloud.netflix.ribbon
// 在 RibbonClientConfiguration 中找到
@Configuration
@EnableConfigurationProperties
public class RibbonClientConfiguration {
@Bean
@ConditionalOnMissingBean
@SuppressWarnings("unchecked")
public ServerList<Server> ribbonServerList(IClientConfig config) {
if (this.propertiesFactory.isSet(ServerList.class, name)) {
return this.propertiesFactory.get(ServerList.class, config, name);
}
ConfigurationBasedServerList serverList = new ConfigurationBasedServerList();
serverList.initWithNiwsConfig(config);
return serverList;
}
}
那么就只能继续找喽!!!
- 真的:既然与
eureka
相关,那不妨找找ribbon.eureka
的相关的包
spring-cloud-netflix-eureka-client
这个项目里,有eureka
整合ribbon
的代码。
// 定位:org.springframework.cloud.netflix.ribbon.eureka
// 在 EurekaRibbonClientConfiguration 中找到
@Configuration
public class EurekaRibbonClientConfiguration {
... ...
// eureka 和 ribbon 整合相关的代码,并提供的一个ServerList
@Bean
@ConditionalOnMissingBean
public ServerList<?> ribbonServerList(IClientConfig config,
Provider<EurekaClient> eurekaClientProvider) {
... ...
// 重要:包装一下
DiscoveryEnabledNIWSServerList discoveryServerList
= new DiscoveryEnabledNIWSServerList(config, eurekaClientProvider);
DomainExtractingServerList serverList = new DomainExtractingServerList(
discoveryServerList, config, this.approximateZoneFromHostname);
return serverList;
}
... ...
}
// 定位:com.netflix.niws.loadbalancer
public class DiscoveryEnabledNIWSServerList
extends AbstractServerList<DiscoveryEnabledServer>{
... ...
@Override
public List<DiscoveryEnabledServer> getUpdatedListOfServers(){
// 这里面就有一堆 eureka 获取注册表相关代码
return obtainServersViaDiscovery();
}
... ...
}
// 定位:com.netflix.loadbalancer
public class BaseLoadBalancer extends AbstractLoadBalancer implements
PrimeConnections.PrimeConnectionListener, IClientConfigAware {
// 讲收集的服务放在这:
protected volatile List<Server> allServerList = Collections
.synchronizedList(new ArrayList<Server>());
}
2)获得注册表后,如何持续更新?
接着在默认
ILoadBalancer
:ZoneAwareLoadBalancer
,上继续分析。
流程图,如下:
实际上在 restOfInit
方法调用的 enableAndInitLearnNewServersFeature
方法里:
就调用了一个更新器:serverListUpdater
。
它会定时去更新,在构造方法里,构造了
PollingServerListUpdater
的实例,它是在启动1秒后,每隔30秒就会执行一次,去从eureka-client
里将服务列表定时同步到LoadBalancer
的allServerList
中。
// 定位:com.netflix.loadbalancer
public class DynamicServerListLoadBalancer<T extends Server> extends BaseLoadBalancer {
protected volatile ServerListUpdater serverListUpdater;
protected final ServerListUpdater.UpdateAction updateAction
= new ServerListUpdater.UpdateAction() {
@Override
public void doUpdate() {
updateListOfServers();
}
};
// 1. 调用
void restOfInit(IClientConfig clientConfig) {
... ...
// 1.1 开启
enableAndInitLearnNewServersFeature();
... ...
}
public void enableAndInitLearnNewServersFeature() {
// 1.2 开启
// ServerListUpdater 这个实现类是哪个?
// 是 PollingServerListUpdater
// 是在 RibbonClientConfiguration 中生成的。
serverListUpdater.start(updateAction);
}
}
// 可以在对应 ribbon configuration 找到
@Configuration
@EnableConfigurationProperties
@Import({HttpClientConfiguration.class, OkHttpRibbonConfiguration.class, RestClientRibbonConfiguration.class, HttpClientRibbonConfiguration.class})
public class RibbonClientConfiguration {
... ...
// ServerListUpdater 实现类
@Bean
@ConditionalOnMissingBean
public ServerListUpdater ribbonServerListUpdater(IClientConfig config) {
return new PollingServerListUpdater(config);
}
... ...
}
具体实行,如下:
// 定位:com.netflix.loadbalancer
public class PollingServerListUpdater implements ServerListUpdater {
... ...
@Override
public synchronized void start(final UpdateAction updateAction) {
if (isActive.compareAndSet(false, true)) {
final Runnable wrapperRunnable = new Runnable() {
@Override
public void run() {
if (!isActive.get()) {
if (scheduledFuture != null) {
scheduledFuture.cancel(true);
}
return;
}
try {
updateAction.doUpdate();
lastUpdated = System.currentTimeMillis();
} catch (Exception e) {
logger.warn("Failed one update cycle", e);
}
}
};
scheduledFuture = getRefreshExecutor().scheduleWithFixedDelay(
wrapperRunnable,
initialDelayMs, // 1s
refreshIntervalMs, // 30s,每隔30s执行一次
TimeUnit.MILLISECONDS
);
} else {
logger.info("Already active, no-op");
}
}
... ...
}
3)默认负载均衡算法如何选择一个 server
?
流程图,如下:
入手点:LoadBalancerClient
处理,实现负载均衡
拦截器实际上只是简单的封装,把请求直接交给
LoadBalancerClient
去执行事项的请求。
public class RibbonLoadBalancerClient implements LoadBalancerClient {
private SpringClientFactory clientFactory;
... ...
@Override
public <T> T execute(String serviceId, LoadBalancerRequest<T> request)
throws IOException {
// 1. 查找服务对应的负载均衡器
ILoadBalancer loadBalancer = getLoadBalancer(serviceId);
// 这次的重点:
// 会调用 loadBalancer.chooseServer 方法
// 2. 这个 server 就已经包含了具体的 ip 和 port
Server server = getServer(loadBalancer);
... ...
// 执行真正的 HTTP 请求
return execute(serviceId, ribbonServer, request);
}
protected Server getServer(ILoadBalancer loadBalancer) {
if (loadBalancer == null) {
return null;
}
// 由上文知道:默认 LoadBalancer 是 ZoneAwareLoadBalancer
// 所以看下 ZoneAwareLoadBalancer 的实现
return loadBalancer.chooseServer("default"); // TODO: better handling of key
}
... ...
}
ZoneAwareLoadBalancer
的实现:
// 定位:com.netflix.loadbalancer
// 这个类实际是用于多机房的
public class ZoneAwareLoadBalancer<T extends Server>
extends DynamicServerListLoadBalancer<T> {
@Override
public Server chooseServer(Object key) {
// 如果是单机房就调用这个
if (!ENABLED.get() || getLoadBalancerStats().getAvailableZones().size() <= 1) {
// 发现是调用父类的方法
return super.chooseServer(key);
}
Server server = null;
... ...
}
}
// 定位:com.netflix.loadbalancer
public class BaseLoadBalancer extends AbstractLoadBalancer implements
PrimeConnections.PrimeConnectionListener, IClientConfigAware {
// 默认使用轮询策略
protected IRule rule = new RoundRobinRule();
... ...
// 真实调用:
public Server chooseServer(Object key) {
if (counter == null) {
counter = createCounter();
}
counter.increment();
if (rule == null) {
return null;
} else {
try {
return rule.choose(key);
} catch (Exception e) {
return null;
}
}
}
... ...
}
那么 IRule
有默认的,那么会有注入的嘛?那又在哪注入的?
发现在
RibbonClientConfiguration
有生成对应的Bean
。
@Configuration
@EnableConfigurationProperties
@Import({HttpClientConfiguration.class, OkHttpRibbonConfiguration.class, RestClientRibbonConfiguration.class, HttpClientRibbonConfiguration.class})
public class RibbonClientConfiguration {
... ...
// 生成对应的 Bean
@Bean
@ConditionalOnMissingBean
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;
}
... ...
}
先来看下ZoneAvoidanceRule
继承结构:
发现
ZoneAvoidanceRule
并没有choose()
方法,此方法在其父类PredicateBasedRule
里。
// 定位:com.netflix.loadbalancer
// 用来过滤服务列表的核心逻辑,可利用自己的实现进行个性化的实例过滤
public abstract class PredicateBasedRule extends ClientConfigEnabledRoundRobinRule {
@Override
public Server choose(Object key) {
ILoadBalancer lb = getLoadBalancer();
// 重点:
// 1. 先过滤一下 server list
// 2. 再根据算法轮询选择服务列表
Optional<Server> server
= getPredicate().chooseRoundRobinAfterFiltering(lb.getAllServers(), key);
if (server.isPresent()) {
return server.get();
} else {
return null;
}
}
}
// 定位:com.netflix.loadbalancer
public abstract class AbstractServerPredicate implements Predicate<PredicateKey> {
... ...
public Optional<Server> chooseRoundRobinAfterFiltering(List<Server> servers,
Object loadBalancerKey) {
// 1. 过滤一下 server list
List<Server> eligible = getEligibleServers(servers, loadBalancerKey);
if (eligible.size() == 0) {
return Optional.absent();
}
// 2. 轮询算法访问
return Optional.of(eligible.get(incrementAndGetModulo(eligible.size())));
}
// 轮询算法,计算索引
private int incrementAndGetModulo(int modulo) {
for (;;) {
int current = nextIndex.get();
int next = (current + 1) % modulo;
if (nextIndex.compareAndSet(current, next) && current < modulo)
return current;
}
}
}
4)如何发起一个真正的网络请求
选择完
server
之后,就要发起真正的网络请求了。
流程图,如下:
入手点:拦截器,每个 RestTemplate
发送请求,会被拦截器拦截:
// 定位:org.springframework.cloud.client.loadbalancer
public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor {
@Override
public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
final ClientHttpRequestExecution execution) throws IOException {
final URI originalUri = request.getURI();
String serviceName = originalUri.getHost();
// 2. 交给 RibbonLoadBalancerClient 去处理
return this.loadBalancer
.execute(serviceName,
// 1. 创建匿名类
requestFactory.createRequest(request, body, execution));
}
}
入手点:LoadBalancerClient
处理,实现负载均衡
拦截器实际上只是简单的封装,把请求直接交给
LoadBalancerClient
去执行事项的请求。
public class RibbonLoadBalancerClient implements LoadBalancerClient {
private SpringClientFactory clientFactory;
... ...
@Override
public <T> T execute(String serviceId, LoadBalancerRequest<T> request)
throws IOException {
// 1. 查找服务对应的负载均衡器
ILoadBalancer loadBalancer = getLoadBalancer(serviceId);
// 会调用 loadBalancer.chooseServer 方法
// 2. 这个 server 就已经包含了具体的 ip 和 port
Server server = getServer(loadBalancer);
... ...
// 这次的重点:
// 执行真正的 HTTP 请求
return execute(serviceId, ribbonServer, request);
}
@Override
public <T> T execute(String serviceId, ServiceInstance serviceInstance,
LoadBalancerRequest<T> request) throws IOException {
... ...
try {
// 重点处理:
T returnVal = request.apply(serviceInstance);
statsRecorder.recordStats(returnVal);
return returnVal;
}
... ...
}
... ...
}
再进入 LoadBalancerRequest.apply()
:
- 利用
LoadBalancerClient
获取到了真正的请求地址 - 完成
URI
替换 ClientHttpRequestExecution
完成真正HTTP
请求
从
ServiceRequestWrapper
获取到真正的请求URL
地址。
//
return new LoadBalancerRequest<ClientHttpResponse>() {
// 调用:
@Override
public ClientHttpResponse apply(final ServiceInstance instance)
throws Exception {
// 1. 利用 LoadBalancerClient 获取到了真正的请求地址
// 2. 并完成 URI 替换
HttpRequest serviceRequest
= new ServiceRequestWrapper(request, instance, loadBalancer);
if (transformers != null) {
for (LoadBalancerRequestTransformer transformer : transformers) {
serviceRequest = transformer.transformRequest(serviceRequest, instance);
}
}
// 3. ClientHttpRequestExecution 完成真正 HTTP 请求
return execution.execute(serviceRequest, body);
}
};
// 使用:
public class ServiceRequestWrapper extends HttpRequestWrapper {
... ...
// 重写了 HttpRequest 的 getURI 方法
// 利用 LoadBalancerClient 获取到了真正的请求地址
@Override
public URI getURI() {
// 又调用 LoadBalancerClient
URI uri = this.loadBalancer.reconstructURI(
this.instance, getRequest().getURI());
return uri;
}
}
public class RibbonLoadBalancerClient implements LoadBalancerClient {
... ...
@Override
public URI reconstructURI(ServiceInstance instance, URI original) {
// 利用服务实例的host和端口以及path信息,拼接出真正的请求地址
// http://serviceA/sayHello -> http://192.168.10.1:8080/sayHello
Assert.notNull(instance, "instance can not be null");
String serviceId = instance.getServiceId();
RibbonLoadBalancerContext context = this.clientFactory
.getLoadBalancerContext(serviceId);
Server server = new Server(instance.getHost(), instance.getPort());
IClientConfig clientConfig = clientFactory.getClientConfig(serviceId);
ServerIntrospector serverIntrospector = serverIntrospector(serviceId);
URI uri = RibbonUtils.updateToHttpsIfNeeded(original, clientConfig,
serverIntrospector, server);
return context.reconstructURIWithServer(server, uri);
}
... ...
}
5)ping
服务检查服务实例是否存活且有效?
Ribbon
原生有一个 ping
机制,即 IPing
的组件:会时不时的自发 ping
一下服务器,看看服务器是否存活。
Ribbon
对接 Spring Cloud
之后,其 IPing
的工作机制:
// 1. 先注入对应的 Bean
// 定位:org.springframework.cloud.netflix.ribbon
@Configuration
@EnableConfigurationProperties
@Import({HttpClientConfiguration.class, OkHttpRibbonConfiguration.class, RestClientRibbonConfiguration.class, HttpClientRibbonConfiguration.class})
public class RibbonClientConfiguration {
... ...
// 注入 IPing 的Bean
@Bean
@ConditionalOnMissingBean
public IPing ribbonPing(IClientConfig config) {
if (this.propertiesFactory.isSet(IPing.class, name)) {
return this.propertiesFactory.get(IPing.class, config, name);
}
return new DummyPing();
}
... ...
}
// 定位:com.netflix.loadbalancer
// 2. 看对应实现,发现里面什么都没有:
// 即,在 Spring Cloud环境下,默认的情况 IPing 组件不生效
public class DummyPing extends AbstractLoadBalancerPing {
public DummyPing() {
}
public boolean isAlive(Server server) {
return true;
}
@Override
public void initWithNiwsConfig(IClientConfig clientConfig) {
}
}
那对应 ping
的实现在哪呢?
实际上使用的是 eureka
项目中定义的 IPing
组件:EurekaRibbonClientConfiguration
// 定位:org.springframework.cloud.netflix.ribbon.eureka
@Configuration
public class EurekaRibbonClientConfiguration {
@Bean
@ConditionalOnMissingBean
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;
}
}
// 实际上调用这块:
// 对每个 server list 中的 server,都检查一下这个 server 对应的一个 eureka 中的 InstanceInfo 的状态,看这个 InstanceInfo服务实例的 status 是否是正常的。
public class NIWSDiscoveryPing extends AbstractLoadBalancerPing {
// 重要:
public boolean isAlive(Server server) {
boolean isAlive = true;
if (server!=null && server instanceof DiscoveryEnabledServer){
DiscoveryEnabledServer dServer = (DiscoveryEnabledServer)server; // 1. 获取服务实例
InstanceInfo instanceInfo = dServer.getInstanceInfo();
if (instanceInfo!=null){
// 2. 获取服务状态
InstanceStatus status = instanceInfo.getStatus();
if (status!=null){
// 3. 判断服务状态是否存活
isAlive = status.equals(InstanceStatus.UP);
}
}
}
return isAlive;
}
}
最后,在 ZoneAwareLoadBalancer
实例构造时候,会启动一个定时任务,利用 IPing
组件对 server list
中的每个 server
执行 isAlive
操作:
// 在 ZoneAwareLoadBalancer 的父类中
public class BaseLoadBalancer extends AbstractLoadBalancer implements
PrimeConnections.PrimeConnectionListener, IClientConfigAware {
// 构造器
public BaseLoadBalancer(IClientConfig config, IRule rule, IPing ping) {
initWithConfig(config, rule, ping);
}
void initWithConfig(IClientConfig clientConfig, IRule rule, IPing ping) {
setPingInterval(pingIntervalTime);
... ...
}
public void setPingInterval(int pingIntervalSeconds) {
... ...
// 默认 30
this.pingIntervalSeconds = pingIntervalSeconds;
// 设置
setupPingTask(); // since ping data changed
}
void setupPingTask() {
... ...
lbTimer = new ShutdownEnabledTimer("NFLoadBalancer-PingTimer-" + name,
true);
// 所以,默认每隔 30秒 执行 PingTask 调度任务
lbTimer.schedule(new PingTask(), 0, pingIntervalSeconds * 1000);
forceQuickPing();
}
}