Ribbon简介
什么是Ribbon?
Ribbon是Netflix发布的负载均衡器,它可以帮我们控制HTTP和TCP客户端的行为。只需为Ribbon配置服务提供者地址列表,Ribbon就可基于负载均衡算法计算出要请求的目标服务地址。
Ribbon默认为我们提供了很多的负载均衡算法,例如轮询、随机、响应时间加权等——当然,为Ribbon自定义负载均衡算法也非常容易,只需实现IRule
接口即可。
spring-cloud-starter-netflix-eureka-client
已经包含 spring-cloud-starter-netfilx-ribbon
,故而无需额外添加依赖。@Bean @LoadBalanced public RestTemplate restTemplate() { return new RestTemplate(); }
只需在RestTemplate
上添加LoadBalanced
注解,即可让RestTemplate整合Ribbon!
调用
@GetMapping("/users/{id}") public User findById(@PathVariable Long id) { // 这里用到了RestTemplate的占位符能力 User user = this.restTemplate.getForObject( "http://microservice-provider-user/users/{id}", User.class, id ); // ...业务... return user; }
我们将请求的目标服务改成了http://microservice-provider-user/users/{id}
,也就是http://{目标服务名称}/{目标服务端点}
的形式,Ribbon会自动在实际调用时,将目标服务名替换为该服务的IP和端口。
WARNING
事实上,这里的目标服务名称,在Ribbon里叫虚拟主机名
,主机名是不能包含_
等特殊字符的——这意味着,一般不建议配置spring.application.name = xxx_xxx
,如果你的应用名称一定带有下划线这种字符,
那么请额外配置eureka.instance.virtual-host-name = 一个合法的主机名
,否则Ribbon将会提示虚拟主机名不合法的异常(在早期的版本则是报空指针)!
什么时候用Ribbon,用在哪?
上面知道Ribbon实现在客户端的负载均衡,所以当我们需要调用多台部署了相同项目的服务器时,就可以使用Ribbon。既然时客户端的负载均衡,那自然是在调用方使用了。
比如服务调用方A调用服务提供方B/C/D,就可以再A使用Ribbon,负载均衡的调用BCD。
深入
@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @Qualifier public @interface LoadBalanced { }
可以看到类里面是空的。那想到在springboot中,有一个申明式的注解,必定在其同名的包下面会有一个这样的(xxxAutoConfiguration)配置类,去配置这个注解,
我们定位到该包下面,看到同名的包下面确实有这样一个配置类LoadBalancerAutoConfiguration
点进去是这样的:
@Configuration(//@Configuration:注解可以用java代码的形式实现spring中xml配置文件配置的效果。 proxyBeanMethods = false ) @ConditionalOnClass({RestTemplate.class})//@ConditionalOnClass:其用途是判断当前classpath下是否存在指定类,若是则将当前的配置装载入spring容器 @ConditionalOnBean({LoadBalancerClient.class})//@ConditionalOnBean:当给定的在bean存在时,则实例化当前Bean @EnableConfigurationProperties({LoadBalancerRetryProperties.class}) public class LoadBalancerAutoConfiguration { @LoadBalanced @Autowired( required = false )
//这里面会注入有@loadbalence注解的所有的restTemplate实例 private List<RestTemplate> restTemplates = Collections.emptyList(); @Autowired( required = false ) //还有很多。。。 }
@EnableConfigurationProperties:如果该类只使用了@ConfigurationProperties注解,然后该类没有在扫描路径下或者没有使用@Component等注解,导致无法被扫描为bean,那么就必须在配置类上使用@EnableConfigurationProperties注解去指定这个类,这个时候就会让该类上的@ConfigurationProperties生效,然后作为bean添加进spring容器中
InterceptingHttpAccessor
public class RestTemplate extends InterceptingHttpAccessor implements RestOperations { //。。。。 }
看来真的有拦截器在起作用,我们可以看到InterceptingHttpAccessor类中有一个setInterceptors方法
public void setInterceptors(List<ClientHttpRequestInterceptor> interceptors) { Assert.noNullElements(interceptors, "'interceptors' must not contain null elements"); if (this.interceptors != interceptors) { this.interceptors.clear(); this.interceptors.addAll(interceptors); AnnotationAwareOrderComparator.sort(this.interceptors); } }
然后我们看看他是在哪里调用这份方法的,我们再回到LoadBalancerAutoConfiguration配置类中,看到这段方法:
@Bean @ConditionalOnMissingBean public RestTemplateCustomizer restTemplateCustomizer(final RetryLoadBalancerInterceptor loadBalancerInterceptor) { return (restTemplate) -> { List<ClientHttpRequestInterceptor> list = new ArrayList(restTemplate.getInterceptors()); list.add(loadBalancerInterceptor); restTemplate.setInterceptors(list); }; }
看到这里在往resttemplate中添加拦截器loadBalancerInterceptor,我们点击这个拦截器进去,主要看这段代码
public ClientHttpResponse intercept(final HttpRequest request, final byte[] body, final ClientHttpRequestExecution execution) throws IOException { URI originalUri = request.getURI();//获取URI String serviceName = originalUri.getHost();//获取serviceName 就是每个微服务的应用名称 Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri); LoadBalancedRetryPolicy retryPolicy = this.lbRetryFactory.createRetryPolicy(serviceName, this.loadBalancer); RetryTemplate template = this.createRetryTemplate(serviceName, request, retryPolicy); return (ClientHttpResponse)template.execute((context) -> { ServiceInstance serviceInstance = null; if (context instanceof LoadBalancedRetryContext) { LoadBalancedRetryContext lbContext = (LoadBalancedRetryContext)context; serviceInstance = lbContext.getServiceInstance(); } if (serviceInstance == null) { serviceInstance = this.loadBalancer.choose(serviceName);//查看这个choose方法,这里通过负载均衡选择一个server } ClientHttpResponse response = (ClientHttpResponse)this.loadBalancer.execute(serviceName, serviceInstance, this.requestFactory.createRequest(request, body, execution)); int statusCode = response.getRawStatusCode(); if (retryPolicy != null && retryPolicy.retryableStatusCode(statusCode)) { byte[] bodyCopy = StreamUtils.copyToByteArray(response.getBody()); response.close(); throw new ClientHttpResponseStatusCodeException(serviceName, response, bodyCopy); } else { return response; } }, new LoadBalancedRecoveryCallback<ClientHttpResponse, ClientHttpResponse>() { protected ClientHttpResponse createResponse(ClientHttpResponse response, URI uri) { return response; } });
查看choose方法:
public ServiceInstance choose(String serviceId, Object hint) { Server server = this.getServer(this.getLoadBalancer(serviceId), hint);//通过serviceID找到对应的服务实例均衡器,loadBalancer 这里面保存了所有的服务实例,可用的,宕机的都在里面 return server == null ? null : new RibbonLoadBalancerClient.RibbonServer(serviceId, server, this.isSecure(server, serviceId), this.serverIntrospector(serviceId).getMetadata(server)); }
使用均衡算法拿到可用的服务实例(从几个中选择一个,达到均衡的作用),返回server,然后拿到这个server就可以继续执行目标请求。
均衡器里面会保存allServerList,里面会有upServeLlist里面是我们启动的服务实例,然后使用负载均衡算法选择一个服务。
ribbon的常见配置
1.禁用eureka
当我们在resttemplate上面添加loadbalence注解后,就可以使用服务名去调用,如果我们想关闭这个功能,可以使用ribbon.eureka.enable=false
2.配置接口地址列表
如果我们关闭了eureka之后,还想用服务名去调用,就需要手动配置服务配置列表
服务名.ribbon.listOfServers=IP:PORT1,IP:PORT2
3.配置负载均衡策略
服务名.ribbon.NFLoadBalencerRuleClassName=策略class全类名
4.超时时间
ribbon中有两种和超时时间相关的配置
ribbon.ConnectTimeout=2000 请求连接的超时时间
ribbon.ReadTimeout=5000 请求处理的超时时间
可以在前面加上具体的服务名,为指定的服务配置
5.并发参数
ribbon.MaxTotalConnections=500 最大连接数
ribbon,MaxConnectionsPerHost=500 每个host最大连接数