客户端负载均衡Ribbon
1、简介
Spring Cloud Ribbon是一个基于HTTP和TCP的客户端负载均衡工具,它基于Netflix Ribbon实现。通过Spring Cloud的封装,可以让我们轻松地将面向服务的REST请求自动转换成客户端负载均衡的服务调用。Spring Cloud Ribbon虽然只是一个工具类框架,它不像服务注册中心、配置中心、API网关那样需要独立部署,但是它几乎存在于每一个Spring Cloud构建的微服务和基础设施中。因为微服务间的调用,API网关的请求转发等内容,实际上都是通过Ribbon来实现的,包括后续我们将要介绍的Feign,它也是基于Ribbon实现的工具。所以,对Spring Cloud Ribbon的理解和使用,对于我们使用Spring Cloud来构建微服务非常重要。Ribbon内部已实现了 随机、轮训、权重、减压(选取压力最小的) 等常见的负载算法,同时也提供了 ILoadBalance 与 IRule 两个接口方便我们自己编写适合自己的负载算法。
2、负载均衡
负载均衡在系统架构中是一个非常重要,并且是不得不去实施的内容。因为负载均衡是对系统的高可用、网络压力的缓解和处理能力扩容的重要手段之一。我们通常所说的负载均衡都指的是服务端负载均衡,其中分为硬件负载均衡和软件负载均衡。硬件负载均衡主要通过在服务器节点之间安装专门用于负载均衡的设备,比如F5等;而软件负载均衡则是通过在服务器上安装一些用于负载均衡功能或模块的软件来完成请求分发工作,比如Nginx,HAproxy等。不论采用硬件负载均衡还是软件负载均衡,只要是服务端都能以类似下图的架构方式构建起来:

硬件负载均衡的设备或是软件负载均衡的软件模块都会维护一个下高可用的服务端清单,通过心跳检测来剔除故障的服务端节点以保证清单中都是可以正常访问的服务端节点。当客户端发送请求到负载均衡设备的时候,该设备按某种算法(比如线性轮询、按权重负载、按流量负载等)从维护的可用服务端清单中取出一台服务端地址,然后进行转发。
客户端负载均衡和服务端负载均衡最大的不同点在于上面所提到服务清单所存储的位置。在客户端负载均衡中,所有客户端节点都维护着自己要访问的服务端清单,而这些服务端端清单来自于服务注册中心,比如Eureka。同服务端负载均衡的架构类似,在客户端负载均衡中也需要心跳去维护服务端清单的健康性,默认会创建针对各个服务治理框架的Ribbon自动化整合配置,比如Eureka中的org.springframework.cloud.netflix.ribbon.eureka.RibbonEurekaAutoConfiguration,Consul中的org.springframework.cloud.consul.discovery.RibbonConsulAutoConfiguration。在实际使用的时候,我们可以通过查看这两个类的实现,以找到它们的配置详情来帮助我们更好地使用它。
通过Spring Cloud Ribbon的封装,我们在微服务架构中使用客户端负载均衡调用非常简单,只需要如下两步:
1.服务提供者只需要启动多个服务实例并注册到一个注册中心或是多个相关联的服务注册中心。2.服务消费者直接通过调用被@LoadBalanced注解修饰过的RestTemplate来实现面向服务的接口调用。
这样,我们就可以将服务提供者的高可用以及服务消费者的负载均衡调用一起实现了。
框架图

3、实现前提
Ribbon实现客户端负载均衡的前提是通过RestTemplate,@LoadBalanced注解就是以客户端负载均衡的方式进行配置,下面的配置类主要实现了自定义序列化规则和配置HTTP连接池。
@Configurationpublic class RestTemplateConfig {Logger logger = LoggerFactory.getLogger(getClass());@Bean@LoadBalanced@SentinelRestTemplate(blockHandler = "handleException", blockHandlerClass = RestExceptionHandler.class)public RestTemplate restTemplate() {RestTemplate restTemplate = new RestTemplate();restTemplate.setRequestFactory(clientHttpRequestFactory());restTemplate.setErrorHandler(new DefaultResponseErrorHandler());List<HttpMessageConverter<?>> converters = restTemplate.getMessageConverters();for (HttpMessageConverter<?> converter : converters) {if (converter instanceof MappingJackson2HttpMessageConverter) {MappingJackson2HttpMessageConverter jsonConverter = (MappingJackson2HttpMessageConverter) converter;jsonConverter.setObjectMapper(new ObjectMapper());//jsonConverter.setSupportedMediaTypes(ImmutableList.of(new MediaType("application", "json", MappingJackson2HttpMessageConverter.DEFAULT_CHARSET), new MediaType("text", "javascript", MappingJackson2HttpMessageConverter.DEFAULT_CHARSET)));}}restTemplate.getMessageConverters().add(new StringHttpMessageConverter(MappingJackson2HttpMessageConverter.DEFAULT_CHARSET));return restTemplate;}@Beanpublic HttpComponentsClientHttpRequestFactory clientHttpRequestFactory() {try {// 信任所有HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(null, new TrustStrategy() {public boolean isTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {return true;}}).build();httpClientBuilder.setSSLContext(sslContext);HostnameVerifier hostnameVerifier = NoopHostnameVerifier.INSTANCE;SSLConnectionSocketFactory sslConnectionSocketFactory = new SSLConnectionSocketFactory(sslContext, hostnameVerifier);Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory>create().register("http", PlainConnectionSocketFactory.getSocketFactory()).register("https", sslConnectionSocketFactory).build();// 注册http和https请求// 开始设置连接池PoolingHttpClientConnectionManager poolingHttpClientConnectionManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry);poolingHttpClientConnectionManager.setMaxTotal(500); // 最大连接数500poolingHttpClientConnectionManager.setDefaultMaxPerRoute(100); // 同路由并发数100httpClientBuilder.setConnectionManager(poolingHttpClientConnectionManager);httpClientBuilder.setRetryHandler(new DefaultHttpRequestRetryHandler(3, true)); // 重试次数HttpClient httpClient = httpClientBuilder.build();HttpComponentsClientHttpRequestFactory clientHttpRequestFactory = new HttpComponentsClientHttpRequestFactory(httpClient); // httpClient连接配置clientHttpRequestFactory.setConnectTimeout(20000); // 连接超时clientHttpRequestFactory.setReadTimeout(30000); // 数据读取超时时间clientHttpRequestFactory.setConnectionRequestTimeout(20000); // 连接不够用的等待时间return clientHttpRequestFactory;} catch (KeyManagementException | NoSuchAlgorithmException | KeyStoreException e) {logger.error("初始化HTTP连接池出错:"+e.getMessage());}return null;}}
关于RestTemplate的详细解释请参考文末的相关链接。
@LoadBalancer默认采用轮训算法
3.1引入依赖
<!-- ribbon support --><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-ribbon</artifactId></dependency>
3.2使用
引入依赖后配置完RestTemplate的@LoadBalancer注解后就可以实现客户端负载均衡了。
简单解释,前提是有多个服务端,客户端负载均衡本质上就是由客户端完成服务端的选择,这样就会产生另一个前提就是必须有一个服务注册中心,这样客户端可以利用服务注册中心获取服务端提供的服务名和地址列表的关系,进而根据服务名和负载均衡算法挑选出符合算法条件的服务端地址,最终依然会根据实际的地址和端口拼接调用的uri,从而实现调用。总结:Ribbon核心主要为以下三点:
•服务发现,发现依赖服务的列表•服务选择规则,在多个服务中如何选择一个有效服务•服务监听,检测失效的服务,高效剔除失效服务
3.3自定义轮训算法
方法一,实现IRule接口
import com.netflix.loadbalancer.IRule;import com.netflix.loadbalancer.RandomRule;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;@Configurationpublic class RibbonRuleConfiguration {@Beanpublic IRule ribbonRule() {return new RandomRule();}}@RibbonClient(name = "ribbonRule",configuration = RibbonRuleConfiguration.class)public class OrderController {}
方式二
继承AbstractLoadBalancerRule、PredicateBasedRule即可
3.4内置负载均衡规则
负载均衡规则是Ribbon的核心,下面来看一下Ribbon内置的负载均衡规则。
•AvailabilityFilteringRule:过滤掉一直连接失败的被标记为circuit tripped的后端Server,并过滤掉那些高并发的后端Server或者使用一个AvailabilityPredicate来包含过滤server的逻辑,其实就就是检查status里记录的各个Server的运行状态;•BestAvailableRule:选择一个最小的并发请求的Server,逐个考察Server,如果Server被tripped了,则跳过。•RandomRule:随机选择一个Server;•ResponseTimeWeightedRule:作用同WeightedResponseTimeRule,二者作用一样;•RetryRule:对选定的负载均衡策略机上重试机制,在一个配置时间段内当选择Server不成功,则一直尝试使用subRule的方式选择一个可用的server;•RoundRobinRule:轮询选择, 轮询index,选择index对应位置的Server;•WeightedResponseTimeRule:根据响应时间加权,响应时间越长,权重越小,被选中的可能性越低;•ZoneAvoidanceRule:复合判断Server所在区域的性能和Server的可用性选择Server;
3.5Ribbon配置自定义
Ribbon可实现精确到目标服务的细粒度配置。例如A服务调用服务B,A服务调用服务C,可以针对B服务一套配置,针对C服务另一套配置。
方式一、代码配置方式
在Spring Cloud中,Ribbon的默认配置如下(格式是BeanType
beanName: ClassName
):
•IClientConfig
ribbonClientConfig: DefaultClientConfigImpl
•IRule
ribbonRule: ZoneAvoidanceRule
•IPing
ribbonPing: NoOpPing
•ServerList<Server>
ribbonServerList: ConfigurationBasedServerList
•ServerListFilter<Server>
ribbonServerListFilter: ZonePreferenceServerListFilter
•ILoadBalancer
ribbonLoadBalancer: ZoneAwareLoadBalancer
•ServerListUpdater
ribbonServerListUpdater: PollingServerListUpdater
创建一个空类,并在其上添加@Configuration
注解和@RibbonClient
注解。
/*** 使用RibbonClient,为特定的目标服务自定义配置。* 使用@RibbonClient的configuration属性,指定Ribbon的配置类。* 可参考的示例:* http://spring.io/guides/gs/client-side-load-balancing/*/@Configuration@RibbonClient(name = "microservice-provider-user", configuration = RibbonConfiguration.class)public class TestConfiguration {}
由代码可知,使用@RibbonClient
注解的configuration属性,即可自定义指定名称Ribbon客户端的配置。
/*** 该类为Ribbon的配置类* 注意:该类不能放在主应用程序上下文@ComponentScan所扫描的包中,否则配置将会被所有Ribbon Client共享。* @author 周立*/@Configurationpublic class RibbonConfiguration {@Beanpublic IRule ribbonRule() {// 负载均衡规则,改为随机return new RandomRule();}}
方式二、属性配置方式
<clientName>.ribbon.
如下属性
•NFLoadBalancerClassName
: should implement ILoadBalancer
•NFLoadBalancerRuleClassName
: should implement IRule
•NFLoadBalancerPingClassName
: should implement IPing
•NIWSServerListClassName
: should implement ServerList
•NIWSServerListFilterClassName
should implement ServerListFilter
microservice-provider-user:ribbon:NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
属性配置的优先级高于代码配置。
3.6Ribbon全局配置
方式一、代码配置
@RibbonClients(defaultConfiguration = DefaultRibbonConfig.class)public class RibbonClientDefaultConfigurationTestsConfig {}@Configurationclass DefaultRibbonConfig {@Beanpublic IRule ribbonRule() {return new RandomRule();}}
方式二、属性配置
和上文细粒度配置类似,只需将目标服务名称前缀去掉即可。
ribbon:NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
3.7Ribbon Eager加载
默认情况下Ribbon是懒加载的——首次请求Ribbon相关类才会初始化,这会导致首次请求过慢的问题,你可以配置饥饿加载,让Ribbon在应用启动时就初始化。
ribbon:eager-load:enabled: true# 多个用,分隔clients: microservice-provider-user
3.8重试机制
Spring Cloud整合了Spring Retry来实现重试逻辑,而对于开发者只需要做一些配置即可。
# 该参数用来开启重试机制,它默认是关闭的spring.cloud.loadbalancer.retry.enabled=true# 指定需要饥饿加载的客户端名称、服务名ribbon.eager-load.clients=hello-service, user-service# 断路器的超时时间需要大于ribbon的超时时间,不然不会触发重试。hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=10000# 请求连接的超时时间ribbon.ConnectTimeout=10000# 请求处理的超时时间ribbon.ReadTimeout=10000# 对所有操作请求都进行重试ribbon.OkToRetryOnAllOperations=true# 切换实例的重试次数ribbon.MaxAutoRetriesNextServer=2# 对当前实例的重试次数(当访问到故障请求的时候,它会再尝试访问一次当前实例)ribbon.MaxAutoRetries=1
3.9zuul饥饿加载
zuul.ribbon.eager-load.enabled=true# zuul直接采用了读取路由配置来进行饥饿加载,对于没有指定的路由规则则需要配置忽略的方式才能生效zuul.ignored-services=*




