Ribbon源码阅读

Ribbon是一个客户端负载均衡软件,通过注册到Eureka上的服务名,获取服务列表,缓存到本地,选择负载均衡算法,发送http请求。
在spring cloud可以通过简单配置,即可完成客户端负载均衡,用法如下:配置,注入,请求

 @Bean
@LoadBalanced
RestTemplate restTemplate() {
    return new RestTemplate();

}
@Autowired
private RestTemplate restTemplate;

public Object getUser(Long id) {
    return restTemplate.getForEntity("http://CLOUD-SERVICE-USER/user/persionalInfo?id=" + id, Object.class).getBody();
}

带着问题看源码:
1. Ribbon怎么和RestTemplate整合
2. 怎么样自定义负载均衡算法
3. 底层http客户端怎么修改

common 包

在spring-cloud-common中提供了这样一个接口LoadBalancerClient,注释说明是客户端负载均衡器。

public interface LoadBalancerClient extends ServiceInstanceChooser {
	<T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException;
	/**
	 * 使用来自LoadBalancer的ServiceInstance,对起执行请求
	 * @param serviceId 
	 * @param 服务实例
	 * @param Request。允许在执行前后添加metric
	 * @return 回调结果
	 */
	<T> T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest<T> request) throws IOException;
	/**
	 * 创建一个真正的URL包含主机和端口:
	 * http://myservice/path/to/service --> http://host:port/path/to/service
	 * @param 服务实例
	 * @param 源url,是一个包含serviceId或者dns的URL
	 * @return 重新构造的URL
	 */
	URI reconstructURI(ServiceInstance instance, URI original);
}

此接口提供了三个方法,继承ServiceInstanceChooser,我们再来看下这个接口:

public interface ServiceInstanceChooser {
    ServiceInstance choose(String serviceId);
}

这个接口只提供了一个方法:通过serviceId获取需要访问的实例。而Ribbon则实现了LoadBalancerClient。

下面分析这个类怎么具体实现接口中的choose方法:

choose实现

@Override
public ServiceInstance choose(String serviceId) {
	Server server = getServer(serviceId);
	if (server == null) {
		return null;
	}
	return new RibbonServer(serviceId, server, isSecure(server, serviceId),
			serverIntrospector(serviceId).getMetadata(server));
}

第一行返回一个server,server包含了服务的地址端口和状态区域等信息,通过serviceId获取这个服务是关键。

Server{
	public static final String UNKNOWN_ZONE = "UNKNOWN";
    private String host;
    private int port = 80;
    private volatile String id;
    private volatile boolean isAliveFlag;
    private String zone = "UNKNOWN";
    private volatile boolean readyToServe = true;
}

再看获取服务的方法:

protected Server getServer(String serviceId) {
	return getServer(getLoadBalancer(serviceId));
}
protected Server getServer(ILoadBalancer loadBalancer) {
	if (loadBalancer == null) {
		return null;
	}
	return loadBalancer.chooseServer("default"); // TODO: better handling of key
}

获取ILoadBalancer对象,再传递给另一个同名方法。下面分析ILoadBalancer及其实现成了重点。

追查ILoadBalancer的实现,最终落实到DynamicServerListLoadBalancer,下面从接口开始分析这个实现类。
先看几个抽象类和接口中需要实现的方法:

public interface ILoadBalancer {
    void addServers(List<Server> var1); // 初始化服务列表
    Server chooseServer(Object var1);	// 从列表中选择一个服务
    void markServerDown(Server var1);	// 标记服务为down
    List<Server> getReachableServers();	// 获取up和reachable的服务
    List<Server> getAllServers();		//获取所有服务(reachable and unreachable)
}
 public abstract List<Server> getServerList(ServerGroup serverGroup);
 public abstract LoadBalancerStats getLoadBalancerStats();    
 public void primeCompleted(Server s, Throwable lastException);
 public abstract void initWithNiwsConfig(IClientConfig clientConfig);

接着再看下面调用链:下面列出的代码中省略了很多逻辑。

public ILoadBalancer getLoadBalancer(String name) {
	return getInstance(name, ILoadBalancer.class);
}

//类:DynamicServerListLoadBalancer
@Override
public <C> C getInstance(String name, Class<C> type) {
	C instance = super.getInstance(name, type);
	if (instance != null) {
		return instance; 
	}
	IClientConfig config = getInstance(name, IClientConfig.class);
	return instantiateWithConfig(getContext(name), type, config);
}

//类:DynamicServerListLoadBalancer
protected Server getServer(ILoadBalancer loadBalancer) { 
	return loadBalancer.chooseServer("default"); 
	}

//类:ZoneAwareLoadBalancer
@Override
public Server chooseServer(Object key) {
    return zoneLoadBalancer.chooseServer(key);
}

//类:BaseLoadBalancer
public Server chooseServer(Object key) {
    return rule.choose(key); // 重要方法 rule = new RoundRobinRule()  
}

总之,逻辑是这样的:
1. 获取负载均衡器(LoadBalancer)的实例(getInstance)。
2. 负载均衡器通过IRule实现的算法逻辑实现选择。
3. 最后获得一个Server(服务)

负载均衡的算法主要通过ribbon提供的一些列算法实现IRule,默认是轮询。

Ribbon提供了一些其他算法:
* RetryRule:根据轮询的方式重试
* RandomRule: 随机
* WeightedResponseTimeRule:根据响应时间去分配权重
* ZoneAvoidanceRule:区域可用性

至于切换算法的配置,主要集中在初始化时的配置类里面IClientConfig,而这个bean的信息通过对restTemplate添加注解@LoadBalanced完成。具体的负载均衡算法可以通过bean注入,如下。

@Bean
public IRule ribbonRule() {
    return new RandomRule();//这里配置策略,和配置文件对应
}

这就解决了我在文章开头的第二个问题。如果需要自定义算法,实现IRule,通过bean的配置完成注入。至于服务列表维护,此文跳过。

restTemplate整合

上文提到通过注解即可开启restTemplate,那是如何做到的?

@Configuration
@ConditionalOnClass(RestTemplate.class)
@ConditionalOnBean(LoadBalancerClient.class)
@EnableConfigurationProperties(LoadBalancerRetryProperties.class)
public class LoadBalancerAutoConfiguration {

	@LoadBalanced
	@Autowired(required = false)
	private List<RestTemplate> restTemplates = Collections.emptyList();

	@Bean
	public SmartInitializingSingleton loadBalancedRestTemplateInitializer(
			final List<RestTemplateCustomizer> customizers) {
		return new SmartInitializingSingleton() {
			@Override
			public void afterSingletonsInstantiated() {
				for (RestTemplate restTemplate : LoadBalancerAutoConfiguration.this.restTemplates) {
					for (RestTemplateCustomizer customizer : customizers) {
						customizer.customize(restTemplate);
					}
				}
			}
		};
	}
}

这是一个自动配置类,首先维护了一个restTemplates列表,并通过RestTemplateCustomizer对这些他们添加一个拦截器,如下。

@Bean
@ConditionalOnMissingBean
public RestTemplateCustomizer restTemplateCustomizer(
		final LoadBalancerInterceptor loadBalancerInterceptor) {
	return new RestTemplateCustomizer() {
		@Override
		public void customize(RestTemplate restTemplate) {
			List<ClientHttpRequestInterceptor> list = new ArrayList<>(
					restTemplate.getInterceptors());
			list.add(loadBalancerInterceptor);
			restTemplate.setInterceptors(list);
		}
	};
}

而拦截器的主要就是把serviceId取出来,再使用负载均衡器发起http请求。

@Override
public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
		final ClientHttpRequestExecution execution) throws IOException {
	final URI originalUri = request.getURI();
	String serviceName = originalUri.getHost();
	Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri);
	return this.loadBalancer.execute(serviceName, requestFactory.createRequest(request, body, execution));
}

又回到了common中提到的四个方法中的第一个。至此,很明了,就是通过注入给restTemplate注入一个拦截器达到使用loadBalancer的目的。

RestTemplate 本身并没有做 HTTP 底层的实现,而是利用了现有的第三方库:
1. HttpURLConnection
2. Apache httpclient
3. ok http
4. netty

不同的底层实现,只需要定义直接的RequestFactory,实现ClientHttpRequestFactory。在spring cloud给出了http client切换成okhttp的配置。

CONTENTS