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();
}
带着问题看源码:
- Ribbon怎么和RestTemplate整合
- 怎么样自定义负载均衡算法
- 底层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()
}
总之,逻辑是这样的:
- 获取负载均衡器(LoadBalancer)的实例(getInstance)。
- 负载均衡器通过IRule实现的算法逻辑实现选择。
- 最后获得一个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 底层的实现,而是利用了现有的第三方库:
- HttpURLConnection
- Apache httpclient
- ok http
- netty
不同的底层实现,只需要定义直接的RequestFactory,实现ClientHttpRequestFactory。在spring cloud给出了http client切换成okhttp的配置。