【Ribbon】源码解析(下)

898 阅读8分钟

这是我参与更文挑战的第17天,活动详情查看:更文挑战

一、前言

先以案例(demo)入手,再来分析 ribbon 流程,最后怼源码。


(1)Ribbon + RestTemplate 案例

简单介绍下,实现原理:

  • 通过在 RestTemplate 上增加 @LoadBalanced 添加拦截器
  • 拦截器中通过 Ribbon 选取服务实例
  • 然后将请求地址中的服务名称替换成 Ribbon 选取服务实例的 IP 和 端口
  1. 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>
  1. RestTemplate中使用
@Configuration
public class RestTemplateConfig {

    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {

        return new RestTemplate();
    }
}

(2)图解 Ribbon 流程

大致流程,如图:

ribbon-流程.png

大致流程可分为:

  1. 注解:自动装配 LoadBalancerAutoConfiguration
  2. 在自动配置类中,为 RestTemplate 添加拦截器 LoadBalancerInterceptor
  3. 调用请求后,拦截器中获取 host,并在 LoadBalancerClient 中对 host 信息进行转换,得到真正的服务器地址。
  4. LoadBalancerClient 中从 Eureka client 得到服务实例列表,然后通过包含了负载均衡规则 IRule,选出要发起调用的 server
  5. 交给负责 HTTP 通讯的组件 LoadBalancerRequest 执行真正的 HTTP 请求。


二、直接怼源码 - 带问题看源码

主要分为:

  1. ribbon 如何获取服务注册列表?
  2. 获得注册表后,如何持续更新?
  3. 默认负载均衡算法如何选择一个 server
  4. 如何发起一个真正的网络请求?
  5. ping 服务检查服务实例是否存活且有效?

1)ribbon 如何获取服务注册列表?

LoadBalancer 要做负载均衡,那必须得知道这个服务的 server list

流程图,如下:

获取服务注册列表.png

源码追踪过程:

之前初始化,已经找到默认 ILoadBalancerZoneAwareLoadBalancer

  1. 先在 ZoneAwareLoadBalancer(默认的 LoadBalancer) 找,貌似并没有获取服务注册列表的信息。

  2. 去其父类(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,实例化了 ServerListBean

  1. 假的:发现这个 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;
	}
}

那么就只能继续找喽!!!

  1. 真的:既然与 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)获得注册表后,如何持续更新?

接着在默认 ILoadBalancerZoneAwareLoadBalancer,上继续分析。

流程图,如下:

更新服务列表.png

实际上在 restOfInit 方法调用的 enableAndInitLearnNewServersFeature 方法里:

就调用了一个更新器:serverListUpdater

它会定时去更新,在构造方法里,构造了 PollingServerListUpdater 的实例,它是在启动1秒后,每隔30秒就会执行一次,去从 eureka-client 里将服务列表定时同步到 LoadBalancerallServerList 中。

// 定位: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

流程图,如下:

默认负载均衡算法.png


入手点: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 里。

2021-06-1714-53-59.png

// 定位: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 之后,就要发起真正的网络请求了。

流程图,如下:

真正网络请求.png

入手点:拦截器,每个 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()

  1. 利用 LoadBalancerClient 获取到了真正的请求地址
  2. 完成 URI 替换
  3. 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();
    }
}