Ribbon介绍
Spring Cloud Ribbon是一个基于HTTP和TCP的客户端负载均衡工具,它基于Netflix Ribbon实现。通过Spring Cloud的封装,可以让我们轻松地将面向服务的REST模版请求自动转换成客户端负载均衡的服务调用。Spring Cloud Ribbon虽然只是一个工具类框架,它不像服务注册中心、配置中心、API网关那样需要独立部署,但是它几乎存在于每一个Spring Cloud构建的微服务和基础设施中。
源码分析(RestTemplate)
-
首先我的RestTemplate是一个http调用的模版(spring-web中)
1.RestTemplate类 public class RestTemplate extends InterceptingHttpAccessor implements RestOperations 2. 父类支持的拦截器 List<ClientHttpRequestInterceptor> interceptors = getInterceptors(); 3. 创建Request对象(选择工厂方法) public ClientHttpRequestFactory getRequestFactory() { List<ClientHttpRequestInterceptor> interceptors = getInterceptors(); if (!CollectionUtils.isEmpty(interceptors)) { ClientHttpRequestFactory factory = this.interceptingRequestFactory; if (factory == null) { factory = new InterceptingClientHttpRequestFactory(super.getRequestFactory(), interceptors); this.interceptingRequestFactory = factory; } return factory; } else { return super.getRequestFactory(); } } 4.带有拦截器的会走的execute方法 @Override public ClientHttpResponse execute(HttpRequest request, byte[] body) throws IOException { if (this.iterator.hasNext()) { ClientHttpRequestInterceptor nextInterceptor = this.iterator.next(); return nextInterceptor.intercept(request, body, this); } else { HttpMethod method = request.getMethod(); Assert.state(method != null, "No standard HTTP method"); ClientHttpRequest delegate = requestFactory.createRequest(request.getURI(), method); request.getHeaders().forEach((key, value) -> delegate.getHeaders().addAll(key, value)); if (body.length > 0) { if (delegate instanceof StreamingHttpOutputMessage) { StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage) delegate; streamingOutputMessage.setBody(outputStream -> StreamUtils.copy(body, outputStream)); } else { StreamUtils.copy(body, delegate.getBody()); } } return delegate.execute(); } } 5.拦截器走的方法 public ClientHttpResponse intercept(final HttpRequest request, final byte[] body, final ClientHttpRequestExecution execution) throws IOException { final URI originalUri = request.getURI(); final String serviceName = originalUri.getHost(); Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri); final LoadBalancedRetryPolicy retryPolicy = lbRetryPolicyFactory.create(serviceName, loadBalancer); RetryTemplate template = this.retryTemplate == null ? new RetryTemplate() : this.retryTemplate; template.setThrowLastExceptionOnExhausted(true); template.setRetryPolicy( !lbProperties.isEnabled() || retryPolicy == null ? new NeverRetryPolicy() : new InterceptorRetryPolicy(request, retryPolicy, loadBalancer, serviceName)); return template .execute(new RetryCallback<ClientHttpResponse, IOException>() { @Override public ClientHttpResponse doWithRetry(RetryContext context) throws IOException { ServiceInstance serviceInstance = null; if (context instanceof LoadBalancedRetryContext) { LoadBalancedRetryContext lbContext = (LoadBalancedRetryContext) context; serviceInstance = lbContext.getServiceInstance(); } if (serviceInstance == null) { serviceInstance = loadBalancer.choose(serviceName); } ClientHttpResponse response = RetryLoadBalancerInterceptor.this.loadBalancer.execute( serviceName, serviceInstance, requestFactory.createRequest(request, body, execution)); int statusCode = response.getRawStatusCode(); if(retryPolicy != null && retryPolicy.retryableStatusCode(statusCode)) { response.close(); throw new RetryableStatusCodeException(serviceName, statusCode); } return response; } }); }
加上@LoadBalanced (spring-cloud-common)注解,意味着是要对我们的http请求做一个balance
RestTemplate 父类有一个拦截器的集合,加上这个注解必然注入了一个拦截器,拦截器怎么注入的注入了什么拦截器
流程图
放进去了什么类型的拦截器,怎么放进去了 -
放入拦截器的注解 LoadBalancerAutoConfiguration
怎么知道是这个注解的? 我们使用RestTemplate 上面加了一个注解 @LoadBalanced 这个在我们的spring-cloud-commons包里面 我就看这个包的一个自动注入的文件(spring.factories)
里面有一个LoadBalancerAutoConfiguration 我们进去看源码:
LoadBalancerAutoConfiguration 类上的注解
@ConditionalOnClass(RestTemplate.class)
// 依赖RestTemplate 所以我们的ribbon是依赖我们的RestTemplate 也是为这个服务的
@ConditionalOnBean(LoadBalancerClient.class)
//同时也要依赖LoadBalancerClient 这个类 ,这个类实在common包里面的 他的注释表示支持负载均衡的client端
//公共的区域有一个LoadBalancerRequestFactory bean (和上面的ClientHttpRequestFactory 没关系的)用在哪里 从名字来就是负载均衡时候用的,拦截器里面用的
public LoadBalancerRequestFactory loadBalancerRequestFactory
LoadBalancerInterceptorConfig 类 上的注解 @ConditionalOnMissingClass("org.springframework.retry.support.RetryTemplate")
当没有下面那个RetryTemplate的时候那就使用这个 可以说是一个默认的
这个生成的拦截器就是 RetryLoadBalancerInterceptor 同时也是放入上面的LoadBalancerRequestFactory
RetryAutoConfiguration 类上的注解@ConditionalOnClass(RetryTemplate.class)
RetryTemplate 这个是例外一个包里面的spring的 如果你引入了这个包 那就是使用这个类初始化我们的拦截器
这个生成的拦截器就是 RetryLoadBalancerInterceptor 同时也是放入LoadBalancedRetryFactory(这个和上面是不一样的)
上面解释是什么类型的,但是是怎么注入到RestTemplate里面去的
@Bean
public SmartInitializingSingleton loadBalancedRestTemplateInitializerDeprecated(
final ObjectProvider<List<RestTemplateCustomizer>> restTemplateCustomizers) {
return () -> restTemplateCustomizers.ifAvailable(customizers -> {
for (RestTemplate restTemplate : LoadBalancerAutoConfiguration.this.restTemplates) {
for (RestTemplateCustomizer customizer : customizers) {
customizer.customize(restTemplate);
}
}
});
}
@Bean
@ConditionalOnMissingBean
public RestTemplateCustomizer restTemplateCustomizer(
final RetryLoadBalancerInterceptor loadBalancerInterceptor) {
return restTemplate -> {
List<ClientHttpRequestInterceptor> list = new ArrayList<>(
restTemplate.getInterceptors());
list.add(loadBalancerInterceptor);
restTemplate.setInterceptors(list);
};
}
@LoadBalanced
@Autowired(required = false)
private List<RestTemplate> restTemplates = Collections.emptyList();
可能觉得奇怪 ,首先spring 会把 所有的RestTemplate实例 放到这个集合里面,上面的方法就是遍历这个实例然后放进去,是通过RestTemplateCustomizer 辅助的
RestTemplateCustomizer 类的作用是 : 可以自己定义这个类的实现,然后判断我的拦截器要不要放进去
到上面为止都没用到 ribbon里面的类
- 负载均衡的调用
在初始化拦截器会注入一个LoadBalancerClient loadBalancerClient 这个实现类是哪里的bean 我们可以看到就RibbonLoadBalancerClient 这个类是ribbon的,这里面就用到了ribbon的初始化了
RibbonAutoConfiguration 类
上面的注解
@Conditional(RibbonAutoConfiguration.RibbonClassesConditions.class)
@RibbonClients
@AutoConfigureAfter(name = "org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration")
//EurekaClientAutoConfiguration 在这个之后呢 肯定是依赖这里面的东西 eureka就是一个注册中心也是 ribbon的数据来源
@AutoConfigureBefore({LoadBalancerAutoConfiguration.class, AsyncLoadBalancerAutoConfiguration.class})
//要在 LoadBalancerAutoConfiguration 之前肯定要先生成一些bean给他用
@EnableConfigurationProperties({RibbonEagerLoadProperties.class, ServerIntrospectorProperties.class})
@Bean
@ConditionalOnMissingBean(LoadBalancerClient.class)
public LoadBalancerClient loadBalancerClient() {
return new RibbonLoadBalancerClient(springClientFactory());
}
首先我们分析一个不带重试策略的(带了重试策略的比较复杂)
public <T> T execute(String serviceId, LoadBalancerRequest<T> request, Object hint) throws IOException {
ILoadBalancer loadBalancer = getLoadBalancer(serviceId);
Server server = getServer(loadBalancer, hint);
if (server == null) {
throw new IllegalStateException("No instances available for " + serviceId);
}
RibbonServer ribbonServer = new RibbonServer(serviceId, server, isSecure(server,
serviceId), serverIntrospector(serviceId).getMetadata(server));
return execute(serviceId, ribbonServer, request);
}
ILoadBalancer 类(服务的选择和操作)的在哪边初始化的
重点是在 getLoadBalancer(serviceId); 里面 进入里面的内
protected AnnotationConfigApplicationContext getContext(String name) {
if (!this.contexts.containsKey(name)) {
synchronized (this.contexts) {
if (!this.contexts.containsKey(name)) {
this.contexts.put(name, createContext(name)); // 注意 createContext(name)
}
}
}
return this.contexts.get(name);
}
protected AnnotationConfigApplicationContext createContext(String name) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
// 创建一个注解的容器
if (this.configurations.containsKey(name)) {
for (Class<?> configuration : this.configurations.get(name)
.getConfiguration()) {
context.register(configuration);
}
}
for (Map.Entry<String, C> entry : this.configurations.entrySet()) {
if (entry.getKey().startsWith("default.")) {
for (Class<?> configuration : entry.getValue().getConfiguration()) {
context.register(configuration);
}
}
}
context.register(PropertyPlaceholderAutoConfiguration.class,
this.defaultConfigType);//defaultConfigType 的 是RibbonClientConfiguration 类型的在SpringClientFactory传进来的
context.getEnvironment().getPropertySources().addFirst(new MapPropertySource(
this.propertySourceName,
Collections.<String, Object> singletonMap(this.propertyName, name)));
if (this.parent != null) {
// Uses Environment from parent as well as beans
context.setParent(this.parent);
}
context.setDisplayName(generateDisplayName(name));
context.refresh(); //加载容器
return context;
}
**springcloud 本身不会加载(RibbonClientConfiguration )只会默认加载spring.factories(ribbon里面只有一个RibbonAutoConfiguration) 所以RibbonClientConfiguration 不会加载 在首次调用的时候会自定义一个注解容器加载 会有点慢 当然会缓存容器的 (这个容器会对每个serviceId) 都会创建一个 **
RibbonClientConfiguration 类 加载了ILoadBalancer 对应的bean 默认是ZoneAwareLoadBalancer
Server server = getServer(loadBalancer, hint); 获取真正的服务信息 这里面会调用rule默认是RoundRobinRule 去获取server 所以需要重写的话就去修改这个
自定义配置(配置参考类CommonClientConfigKey)
支持的属性如下所示,应以.ribbon.下列属性。(属性文件中定义的优先于默认的代码配置的
NFLoadBalancerClassName:应实施 ILoadBalancer
NFLoadBalancerRuleClassName:应实施 IRule
NFLoadBalancerPingClassName:应实施 IPing
NIWSServerListClassName:应实施 ServerList
NIWSServerListFilterClassName:应实施 ServerListFilter
对于每个服务类型都会设置一种配置
怎么实现一个简单服务的高可用的方式
高可用的实现点
- 把你们需要高可用的节点地址注册到注册中心(eureka zk redis 数据库)
- 实现高可用的节点断开的一个失效方式(redis采用的key的失效时间 数据库可以采用时间戳间隔设置)
- 实现客户端的负载均衡算法(ribbon 来做)
扩展
ribbon 怎么做一个平滑升级 版本管理
- 怎么做到平滑升级(服务的平滑替换)
我们需要对服务进行多状态管理 (停用,维护 ,上线等)
对于我们要平滑升级的服务我们管理端需要提供一个维护窗口(需要等流量全部没有才能下线)
重写一个Irule的实现就可以了
当然了我们客户端可以采用重试机制 - 版本管理
目前支持的默认的注册中心是没有服务管理的,而spring cloud是面向资源的,那就可以通过资源uri
去进行版本的管理标识