spring cloud ribbon 是一个基于HTTP和TCP的客户端负载均衡工具,它是基于 Netflix Ribbon实现。通过spring cloud的封装,可以让我们轻松地将面向服务的REST模板请求自动转换成客户端负载均衡的服务调用 Spring Cloud Ribbon是一个工具类的框架。
在使用spring cloud ribbon时候,通常会利用他的RestTemplate的请求拦截来实现对依赖服务的接口调用,而RestTemplate已经实现了对HTTP请求的封装处理形成了一套模板化的调用方法。
开发中,由于对服务依赖的调用可能不止一处,往往一个接口会被多处调用,所以我们通常都会针对各个微服务自行封装一些客户端来包装这些依赖服务的调用。这个时候我们会发现,由于RestTemplate的封装,几乎每一处调用都是简单的模板化内容,综合上述SpringCloudFeign再次基础上做了进一步封装,由它来帮助我们定义 和实现依赖服务接口的定义.在SpringCloudFeign的实现下,我们只需创建一个接口并用注解的方式配置它,即可完成对服务提供方的接口绑定,简化了在使用SpringCloudRibbon时自行封装服务调用客户端的开发量
负载均衡是对系统的高可用,网络压力的缓解和处理能力扩容的重要手段之一,我们通常所说的负载均衡指的是服务端负载均衡。
分为硬件负载均衡和软件负载均衡。硬件负载均衡主要通过在服务器节点之间安装专门用于负载均衡的设备,比如F5等;而软件负载均衡则是通过在服务器上安装一些具有均衡负载功能或模块的软件来完成请求分发工作,比如Nginx等
硬件负载均衡的设备或是软件负载均衡的软件模块都会维护一个下挂可用的服务端清单
客户端负载均衡和服务端复杂均衡最大的不通电在与上面所提到的服务清单所存储的位置
RestTemplate对象会用Ribbon的自动化配置,同时通过使用@LoadBalanced还能够开启客户端负载均衡。
RestTemplate针对集中不同请求类型和参数类型的服务调用实现
GET请求
1.getForEntity() 该方法返回的是ResponseEntity该对象是Spring对HTTP请求响应的封装。
ResponseEntity主要存储了HTTP的几个重要元素,例:HTTP请求状态码的枚举对象HttpStatus(也就是我们常说的400 ,404这些错误码),
在他的父类HttpEntity中还存储着HTTP请求的头信息对象HttpHeaders以及泛型类型的请求体对象
例:RestTemplate restTemplate = new RestTemplate();
ResponseEntity<User> re = restTemplate.getForEntity("http://user-server/user?name={1}",String.class,"didi);
String body = re.getBody();
最后一个参数didi会替换url中的{1}占位符,而返回的ResponseEntity对象中的body内容类型会根据第二个参数转换为String类型
若想返回的body是一个User对象类型把String.class改为User.class
getForEntity函数实际上提供了3中不同的重载实现(可以去看,不做详解)
2.getForObject()该方法可以理解为对getForEntity的进一步封装,他通过HTTPMessageConvertExtractor对HTTP的请求响应体body内容进行对象转换,实现请求直接返回包装好的对象内容.当不需要关注请求响应除body外的其他内容时,该函数就非常好用,可以少一个从Reponse中获取body的步骤,
也有三个重载方法(可以去看,不做详解)
POST请求
1.postForEntity()该方法和getForEntity()类似,会返回一个ResponseEntity<T>对象,有三种不同的重载方法(不做详解)
2.postForObject()该方法和postForObject()类似,作用是简化postForEntity的后续处理,通过直接将请求响应的body内容包装成对象来返回使用,有三种不痛的重载方法(不做详解)
例:
User user = new User("didi",20);
String a = restTemplate.postForObject(url,user,String.class);
3.postForLocation()该方法实现了以POST请求提交资源,并返回新资源URI,这个函数实现了三种不同的重载方法(不做详解)
例:
User user = new User("didi",20);
URI responseUri = restTemplate.postForLocation(url,user)
URI,通一资源标志符(Universal Resource Identifier, URI),表示的是web上每一种可用的资源,如 HTML文档、图像、视频片段、程序等都由一个URI进行定位的。
RestTemplate也有PUT 和DELETE请求的这里不做详解有需要的在看
@LoadBalanced注解源码中的注释知道该注解用来给RestTemplate做标记,以使用负载均衡的客户端(LoadBalancerClient)来配置它
LoadBalancerClient接口(Represents a client side load balancer表示客户端负载平衡器)
ServiceInstance choose(String serviceId);根据传入的服务名serviceId,从负载均衡器中调选一个对应服务的实例
<T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException;使用从负载均衡器中挑选出的服务实例来执行请求内容
URI reconstructURI(ServiceInstance instance, URI original);为系统构建一个合适的server:port形式的URI,在分布式系统中
顺着LoadBalancerClient接口所属包,整理出,从类名初步判断LoadBalancerAutoConfiguration为实现客户端负载均衡器的自动化配置类源码如下:
/**
* Auto configuration for Ribbon (client side load balancing).
*
* @author Spencer Gibb
* @author Dave Syer
*/
@Configuration
@ConditionalOnClass(RestTemplate.class)
@ConditionalOnBean(LoadBalancerClient.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);
}
}
}
};
}
Ribbon实现的负载均衡自动化配置需要满足下面俩个条件:
1.@ConditionalOnClass(RestTemplate.class) ,RestTemplate类必须存在当前工程的环境中
2.@ConditionalOnBean(LoadBalancerClient.class) ,在Spring的Bean工程中必须有LoadBalancerClient的实现Bean
在该自动化配置类中,主要做了三件事
1.创建了一个LoadBalancerInterceptor的Bean,用于实现对客户端发起请求时拦截以实现客户端负载均衡
2.创建了一个RestTemplateCustomizer的Bean,用于给RestTemplate增加LoadBalancerInterceptor拦截器
3.维护一个被@LoadBalanced注解修饰的RestTemplate对象列表,并在这里进行初始化,通过调用RestTemplateCustomizer的实例来给需要客户端负载均衡的RestTemplate增加LoadBalancerInterceptor拦截器
LoadBalancerInterceptor拦截器是如何将一个普通的RestTemplate变成客户端负载均衡的
通过源码以及之前的自动化配置类,我们可以看到在拦截器中注入了LoadBalancerClient实现。当一个被@LoadBalanced注释修饰的RestTemplate对象向外发起HTTP请求时会被LoadBalancerInterceptor类中的intercept拦截由于我们在使用RestTemplate时采用了服务名作为host,所以直接从HTTPRequest的URI对象中通过getHost()就可以拿到服务名,然后调用execute()去根据服务名来选择实例并发起实际的请求。
LoadBalancerClient还只是一个抽象的负载均衡接口,所以我们还需要找到他的具体实现类来分析
通过Ribbon源码,org.springframework.cloud.netflix.ribbon包下对应的实现类Ribbon-LoadBalancerClient
在execute函数的实现中,第一步就是通过getServer根据传入的服务名serviceId去获得具体的服务实例
通过getServer函数的实现源码,我们可以看到这里获取具体服务实例的时候并没有使用LoadBalancerClient接口中的choose函数,而是使用了Netflix Ribbon 自身的ILoadBalancer接口中的chooseSever函数
ILoadBalancer该接口定义了一个客户端负载均衡需要的一系列抽象操作
addServers:像负载均衡器中维护的实例列表增加服务实例
chooseServer:通过某种策略,从负载均衡器中挑选出一个具体的服务实例
markServerDown:用来通知标识负载均衡器中某个具体实例已经停止服务,不然负载均衡器在下一次获取服务实例清单前都会认为服务实例均是正常服务的
getReachableServers:获取当前正常服务的实例列表
getAllServers:获取所有已知的服务实力列表,包括正常服务和停止服务的实例
在该接口定义中涉及的Server对象定义是一个传统的服务端节点,在该类中存储了服务端节点的一些元数据信息,包括host,port以及一些部署信息等
在整合Ribbon的时候Spring Cloud 默认采用了哪个具体实现呢?(实现有ZoneAwareLoadBalancer(可以避免DynamicServerListLoadBalancer的问题)和DynamicServerListLoadBalancer(采用的是线性轮询的方式来选择调用服务实例,该算法实现简单并没有区域Zone的概念,所以它会把所有实例视为一个Zone下的节点来看待,这样就会周期性地产生跨区域访问的情况,由于跨区域会产生更高的延迟,这些实例主要以防止区域故障实现高可用的目的而不能作为常规访问的实例。所以在多区域部署的情况下会有一定的性能问题
)我们通过在RibbonClientConfiguration配置类,,默认采用了ZoneAwareLoadBalancer来实现负载均衡器
下面,回到RibbonLoadBalancerClient的execute函数逻辑,在通过ZoneAwareLoadBalancer的chooseServer函数获取了负载均衡策略分配到服务实例对象Server之后,将其内容包装成RibbonServer对象,(该对象除了存储了服务实例的信息之外,还增加了服务名serviceId,是否需要HTTPS等其他信息),然后使用该对象再回调LoadBalancerInterceptor请求拦截器中LoadBalancerRequest的apply(final ServiceInstance instance)函数,向一个实际的具体服务实例发起请求,从而实现一开始以服务名为host的URI请求到host:post形式的实例访问地址的转换。
在apply(final ServiceInstance instance )函数中传入的ServiceInstance接口对象是对服务实例的抽象定义。再该接口中暴露了服务治理系统中每个服务实例需要提供的一些基本信息,比如:serviceId,host,port等
上面提到的具体包装Server服务实例的RibbonServer对象就是ServiceInstance接口的实现,可以看到它除了包含Server对象之外,还存出来服务名,是否使用HTTPS标识以及一个Map类型的元数据集合。那么apply(final ServiceInstance instance)函数在接受到了具体ServiceInstance实例后,是如何通过LoadBalancerClient接口中的reconstructURI操作来组织具体请求地址的呢?从apply的实现中,可以看到他具体执行的时候,还传入了ServiceRequest_Wrapper对象,该对象继承了HttpRequestWrapper并重写了getURI函数,重写后的getURI通过调用LoadBalancerClient接口的reconstructURI函数来重新构建一个URI来进行访问。在LoadBalancerInterceptor拦截器中,ClientHttpRequestExecution的实例具体执行execution.execute(serviceRequest,body)时,会调用Intercepting-ClientHttpRequest下InterceptingRequestExcution类的execute函数,代码,,,,可以看到,在创建请求的时候requestfactuary.creatRequest(request.getURI(),request.getMethod()),这里的request.getURI()会调用之前介绍的ServiceReuestWrapper对象中重写的getURI函数,此时他就会使用RibbonLoadBalancerClient中实现的reconstructURI来组织具体请求的服务实例地址。。。。。。
配置详解:Ribbon在使用时的各种配置方式
自动化配置:
创建了对应的实现实例就能覆盖这些默认的配置实现,
//hello-service这个服务使用MyRibbonConfiguration中的配置
@RibbonClient(name = "hello-service",configuration = MyRibbonConfiguration.class)
camden版本对RibbonClient配置优化,加注解的方式可以定义为<clientName>.ribbon.NFLoadBalancerPingClassName=<value>(全包名)在application.yml里面添加配置信息
参数配置:对于Ribbon的参数配置通常有俩种方式:全局配置以及指定客户端配置
1.全局配置:ribbon.<key>=<value>格式进行配置就可,其中<key>代表ribbon客户端配置的参数名,<value>代表对应参数的值,全局配置可以作为默认值进行设置,当指定的客户端配置响应的key值,将覆盖全局配置的内容。
2.指定客户端的方式:<clientName>.ribbon.NFLoadBalancerPingClassName=<value>(全包名)例:有一个服务消费者通过RestTemplate来访问hello-service服务的/hello接口,这时我们会这样调用restTemplate.getForEntity("http://hello-service/hello,String.class").getBody().如果没有服务治理框架的帮助,我们需要为该客户端指定具体的实例清单,可以指定服务名来做详细的配置具体如下:hello.service.ribbon.listOfServers=localhost:8001,localhost:8002
重试机制:
spring cloud eureka 实现的服务治理机制强调了CAP原理中的AP(可用性,可靠性)他与zooKeeper这类强调CP(一致性,可靠性)的服务治理框架最大的区别就是,eureka为了实现更高的服务可用性,牺牲了一定的一致性,在极端的情况下他宁愿接受故障的实例也不要丢掉"健康"实例,比如,当服务注册中心的网络发生故障断开时,由于所有的服务实例无法维持正常的心跳,在强调AP的服务治理中会把多有服务实例都剔除掉,而eureka则会因为超过85%的实例丢失心跳而触发保护机制,注册中心将会保留此时的所有节点,以实现服务间依然可以进行互相调用的场景及时其实有部分故障节点,但这样做可以继续保障大多数的服务正常消费。
由于sring cloud eureka在可用性与一致性上的取舍,不论是由于触发了保护机制还是服务剔除的延迟,引起服务调用到故障实例的时候。我们还是希望能够增强对这类问题的容错。所以我们在实现服务调用的时候通常会加入一些重试机制。从Camden SR2版本开始,spring cloud政和路Spring Retry来增强RestTemplate的重试能力,对于开发者来说只需要简单的配置,原来那些通过RestTemplate实现的服务访问就会自动根据配置来实现重试策略。