Spring Cloud Ribbon 是 个基于 HTTP 和 TCP 的客户端负载均衡工具
RestTemplate
我们己经通过引入Ribbon实现了服务消费者的客户端负载均衡功能,其中,我们使用了一个非常有用的对象RestTemplate。该对象会使用Ribbon的自动化配置,同时通过配置@LoadBalanced还能够开启客户端负载均衡。之前我们演示了通过RestTemplate 实现了最简单的服务访问,下面我们将详细介绍RestTemplate针对几种不同请求类型和参数类型的服务调用实现
GET
在RestTemplate中,对GET请求可以通过如下两个方法进行调用实现。
getForEntity
第一种:getForEntity函数。该方法返回的是ResponseEntity,该对象是Spring对HTTP请求响应的封装,其中主要存储了HTTP的几个重要元素,比如HTTP请求状态码的枚举对象HttpStatusC也就是我们常说的404、50。这些错误码)、在它的父类 HttpEntity中还存储着HTTP请求的头信息对象HttpHeaders以及泛型类型的请求体对象。比如下面的例子,就是访问USER-SERVER服务的/user请求,同时最后一个参数 didi会替换url中的{1 }占位符,而返回的ResponseEntity对象中的body内容类型会根据第二个参数转换为String类型。
1 | RestTemplate restTemplate = new RestTemplate(); |
若我们希望返回的body是一个User对象也可以这样实现
1 | RestTemplate restTemplate = new RestTemplate(); |
上面的例子是比较常用的方法,getForEntity函数实际上提供了以下三种不同的重 载实现。
getForEntity(String url, Class responseType, Object … urlVariables):该方法提供了三个参数,其中url为请求的地址,responseType为请求响应体 body的包装类型,urlVariables为url中的参数绑定。GET请求的参数绑定通常使用url中拼接的方式,比如http://USER-SERVICE/user?name=didi, 我们可以像这样自己将参数拼接到url中,但更好的方法是在url中使用占位符并配合urlVariables参数实现GET请求的参数绑定,比如url定义为http://USER-SERVICE/user?name={l},然后可以这样来调用:getForEntity(”http://USER-SERVICE/user?name={l}”,String.class ,”didi”),其中第三个参数didi会替换url中的{1 }占位符。这里需要注意的是,由于urlVariables参数是一个数组,所以它的顺序会对应url中占位符定义的数字顺序。
getForEntity(String url, Class responseType,Map urlVariables):该方法提供的参数中,只有urlVariables的参数类型与上面的方法不同。这里使用了Map类型,所以使用该方法进行参数绑定时需要在占位符中指定Map中参数的key值,比如url定义为http://USER-SERVICE/user?name={name}, 在Map类型的urlVariables中,我们就需要put一个key为口ame的参数来绑定url中{name}占位符的值,比如:
1
2
3
4RestTemplate restTemplate = new RestTemplate();
Map<String, String> map = new HashMap<>();
map.put("name", name);
ResponseEntity<String> responseEntity = restTemplate.getForEntity("http://spring-cloud-producer/hello?name={name}", String.class, map);
getForObject
第二种:getForObject函数。该方法可以理解为对getForEntity的进一步封装,它通过HttpMessageConverterExtractor对HTTP的请求响应体body内容进行对象转换,实现请求直接返回包装好的对象内容。比如:
1 | RestTemplate restTemplate = new RestTemplate(); |
当body是一个User对象时,可以直接这样实现:
1 | RestTemplate restTemplate = new RestTemplate(); |
当不需要关注请求响应除body外的其他内容时,该函数就非常好用,可以少一个从Response中获取body的步骤。它与getForEntity函数类似,也提供了三种不同的重载 实现。
getForOb〕ect(String url, Class responseType, Object … url Variables):与getForE口tity的方法类似,url参数指定访问的地址,respo口seType参数 定义该方法的返回类型,urlVariables参数为url中占位符对应的参数。
getForObject(String url, Class respo口seType,Map urlVariables):在该函数中,使用Map类型的urlVariables替代上面数组形式的ur1 Variables,因此使用时在url中需要将占位符的名称与Map类型中的key对应设置。
getForObject(URI url, Class responseType):该方法使用URI对象来替代之前的url和urlVariables参数使用。
POST
在RestTemplate中,对POST请求时可以通过如下三个方法进行调用实现。
postForEntity
第一种:postForEntity函数,该方法同GET请求中的getForE口tity类似,会在调用后返回ResponseEntity<T>对象,其中T为请求响应的body类型。比如下面这个例子,使用postForEntity提交POST请求到USER-SERVICE服务的/user接口, 提交的body内容为user对象,请求响应返回的body类型为String。
1 | RestTemplate restTemplate = new RestTemplate(); |
postForEntity函数也实现了三种不同的重载方法。
- postForEnt工ty(Str工ngurl, Object request, Class responseType, Object … uriVariables)
- postForEntity(String url, Object request, Class responseType, Map uriVariables)
- postForEntity(URI url, Object request, Class responseType)
这些函数中的参数用法大部分与 getForEntity 致, 比如, 第 个重载函数和第二个重载函数中的 uriVariables 参数都用来对 url 中的参数进行绑定使用;res ponseType 参数是对请求响应的 body 内容的类型定义。 这里需要注意的是新增加的request 参数, 该参数可以是一个普通对象, 也可以是 个 HttpE口tity 对象。 如果是个普通对象, 而非 HttpEntity 对象的时候, RestTemplate 会将请求对象转换为个 HttpEntity 对象来处理, 其中 Object 就是 request 的类型, request 内容会被视作完整的 body 来处理;而如果 request 是 个 HttpEntity 对象, 那么就会被当作个完成的 HTTP请求对象来处理,这个request中不仅包含了body的内容,也包含了header的内容。
第二种:postForObject函数。该方法也跟getForObject的类型类似,它的作用是简化postForEntity的后续处理。通过直接将请求响应的body内容包装成对象来返回使用,比如下面的例子:
1 | RestTemplate restTemplate = new RestTemplate(); |
postForObject 函数也实现了三种不同的重载方法:
- postForObject(String url, Object request, Class respo口seType, Object … uriVariables)
- postForObject(String url, Object request, Class responseType, Map uriVariables)
- postForObject(URI url, Object request, Class responseType)
这三个函数除了返回的对象类型不同, 函数的传入参数均与 postForEntity 一致,
第三种: postForLocation 函数。 该方法实现了以 POST 请求提交资源, 并返回新资源的 URI,比如下面的例子:
1 |
|
Ribbon 负载均衡大概流程
LoadBalancerClient
其中LoadBalancerClient接口,有如下三个方法,
1 | public interface LoadBalancerClient { |
ServiceInstance choose(String serviceId)
:根据传入的服务名serviceId
,从负载均衡器中挑选一个对应服务的实例。T execute(String serviceId, LoadBalancerRequest request) throws IOException
:使用从负载均衡器中挑选出的服务实例来执行请求内容。URI reconstructURI(ServiceInstance instance, URI original)
:为系统构建一个合适的“host:port”形式的URI。在分布式系统中,我们使用逻辑上的服务名称作为host来构建URI(替代服务实例的“host:port”形式)进行请求,比如:http://myservice/path/to/service
。在该操作的定义中,前者ServiceInstance
对象是带有host和port的具体服务实例,而后者URI对象则是使用逻辑服务名定义为host的URI,而返回的URI内容则是通过ServiceInstance
的服务实例详情拼接出的具体“host:post”形式的请求地址。
LoadBalancerAutoConfiguration
LoadBalancerClient
接口的所属包org.springframework.cloud.client.loadbalancer
,对其内容进行整理,可以得出如下图的关系:
从类的命名上我们知道LoadBalancerAutoConfiguration
为实现客户端负载均衡器的自动化配置类。通过查看源码,我们可以验证这一点假设:
1 |
|
从LoadBalancerAutoConfiguration
类头上的注解可以知道Ribbon实现的负载均衡自动化配置需要满足下面两个条件:
@ConditionalOnClass(RestTemplate.class)
:RestTemplate
类必须存在于当前工程的环境中。@ConditionalOnBean(LoadBalancerClient.class)
:在Spring的Bean工程中有必须有LoadBalancerClient
的实现Bean。
在该自动化配置类中,主要做了下面三件事:
- 创建了一个
LoadBalancerInterceptor
的Bean,用于实现对客户端发起请求时进行拦截,以实现客户端负载均衡。 - 创建了一个
RestTemplateCustomizer
的Bean,用于给RestTemplate
增加LoadBalancerInterceptor
拦截器。 - 维护了一个被
@LoadBalanced
注解修饰的RestTemplate
对象列表,并在这里进行初始化,通过调用RestTemplateCustomizer
的实例来给需要客户端负载均衡的RestTemplate
增加LoadBalancerInterceptor
拦截器。
小结
- 通过注解
@RestTemplate
和@LoadBalanced
组织条件开启自动化配置 - 自动化配置类
LoadBalancerAutoConfiguration
向RestTemplate
中添加LoadBalancerInterceptor
拦截器 - 当
RestTemplate
发送请求时进入拦截器, - 拦截器中注入
LoadBalancerClient
并把请求交给它处理
LoadBalancerInterceptor
接下来,我们看看LoadBalancerInterceptor
拦截器是如何交由LoadBalancerClient
处理的:
1 | public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor { |
认识RibbonClientConfiguration
配置类,可以知道在整合时默认采用了ZoneAwareLoadBalancer
来实现负载均衡器。
1 |
|
LoadBalancerClient
的实现类:RibbonLoadBalancerClient
。
1 | public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException { |
可以看到,在execute
函数的实现中,第一步做的就是通过getServer
根据传入的服务名serviceId
去获得具体的服务实例:
1 | protected Server getServer(ILoadBalancer loadBalancer) { |
经过源码跟踪,最终交给了ILoadBalancer类去选择服务实例
1 | public interface ILoadBalancer { |
- addServers()方法是添加一个Server集合;
- chooseServer()方法是根据key去获取Server;
- markServerDown()方法用来标记某个服务下线;
- getReachableServers()获取可用的Server集合;
- getAllServers()获取所有的Server集合。
apply(final ServiceInstance instance)
函数中传入的ServiceInstance
接口是对服务实例的抽象定义。在该接口中暴露了服务治理系统中每个服务实例需要提供的一些基本信息,比如:serviceId、host、port等,具体定义如下:
1 | public interface ServiceInstance { |
而上面提到的具体包装Server
服务实例的RibbonServer
对象就是ServiceInstance
接口的实现,可以看到它除了包含了Server
对象之外,还存储了服务名、是否使用https标识以及一个Map类型的元数据集合。
1 | protected static class RibbonServer implements ServiceInstance { |
负载均衡器 ILoadBalancer
AbstractLoadBalancer
1 | public abstract class AbstractLoadBalancer implements ILoadBalancer { |
AbstractLoadBalancer
是ILoadBalancer
接口的抽象实现。- 定义了一个关于服务实例的分组枚举类
ServerGroup
,它包含了三种不同类型:ALL-所有服务实例、STATUS_UP-正常服务的实例、STATUS_NOT_UP-停止服务的实例; - 实现了一个
chooseServer()
函数,该函数通过调用接口中的chooseServer(Object key)
实现,其中参数key
为null,表示在选择具体服务实例时忽略key
的条件判断; - 定义了两个抽象函数,
getServerList(ServerGroup serverGroup)
定义了根据分组类型来获取不同的服务实例列表,getLoadBalancerStats()
定义了获取LoadBalancerStats
对象的方法,LoadBalancerStats
对象被用来存储负载均衡器中各个服务实例当前的属性和统计信息,这些信息非常有用,我们可以利用这些信息来观察负载均衡器的运行情况,同时这些信息也是用来制定负载均衡策略的重要依据。
BaseLoadBalancer
定义并维护了两个存储服务实例
Server
对象的列表。一个用于存储所有服务实例的清单,一个用于存储正常服务的实例清单。1
2
3
4
5
6@Monitor(name = PREFIX + "AllServerList", type = DataSourceType.INFORMATIONAL)
protected volatile List<Server> allServerList = Collections
.synchronizedList(new ArrayList<Server>());
@Monitor(name = PREFIX + "UpServerList", type = DataSourceType.INFORMATIONAL)
protected volatile List<Server> upServerList = Collections
.synchronizedList(new ArrayList<Server>());定义了之前我们提到的用来存储负载均衡器各服务实例属性和统计信息的
LoadBalancerStats
对象。定义了检查服务实例是否正常服务的
IPing
对象,在BaseLoadBalancer
中默认为null,需要在构造时注入它的具体实现。定义了检查服务实例操作的执行策略对象
IPingStrategy
,在BaseLoadBalancer
中默认使用了该类中定义的静态内部类SerialPingStrategy
实现。根据源码,我们可以看到该策略采用线性遍历ping服务实例的方式实现检查。该策略在当我们实现的IPing
速度不理想,或是Server
列表过大时,可能变的不是很为理想,这时候我们需要通过实现IPingStrategy
接口并实现pingServers(IPing ping, Server[] servers)
函数去扩展ping的执行策略。
1 | private static class SerialPingStrategy implements IPingStrategy { |
- 定义了负载均衡的处理规则
IRule
对象,从BaseLoadBalancer
中chooseServer(Object key)
的实现源码,我们可以知道负载均衡器实际进行服务实例选择任务是委托给了IRule
实例中的choose
函数来实现。而在这里。
1 | public Server chooseServer(Object key) { |
- 启动ping任务:在
BaseLoadBalancer
的默认构造函数中,会直接启动一个用于定时检查Server
是否健康的任务。该任务默认的执行间隔为:10秒。 markServerDown(Server server)
:标记某个服务实例暂停服务。
1 | public void markServerDown(Server server) { |
DynamicServerListLoadBalancer
从DynamicServerListLoadBalancer
的成员定义中,我们马上可以发现新增了一个关于服务列表的操作对象:ServerList<T> serverListImpl
。其中泛型T
从类名中对于T的限定DynamicServerListLoadBalancer<T extends Server>
可以获知它是一个Server
的子类,即代表了一个具体的服务实例的扩展类。
ServerList
接口定义如下所示:
1 | public interface ServerList<T extends Server> { |
它定义了两个抽象方法:
getInitialListOfServers
用于获取初始化的服务实例清单,-getUpdatedListOfServers
用于获取更新的服务实例清单。搜索源码,我们可以整出如下图的结构:
从图中我们可以看到有很多个ServerList
的实现类,那么在DynamicServerListLoadBalancer
中的ServerList
默认配置到底使用了哪个具体实现呢?既然在该负载均衡器中需要实现服务实例的动态更新,那么势必需要ribbon具备访问eureka来获取服务实例的能力,所以我们从Spring Cloud整合ribbon与eureka的包org.springframework.cloud.netflix.ribbon.eureka
下探索,可以找到配置类EurekaRibbonClientConfiguration
,在该类中可以找到看到下面创建ServerList
实例的内容
1 |
|
可以看到,这里创建的是一个DomainExtractingServerList
实例,从下面它的源码中我们可以看到在它内部还定义了一个ServerList list
。同时,DomainExtractingServerList
类中对getInitialListOfServers
和getUpdatedListOfServers
的具体实现,其实委托给了内部定义的ServerList list
对象,而该对象是通过创建DomainExtractingServerList
时候,由构造函数传入的DiscoveryEnabledNIWSServerList
实现的。
1 | public class DomainExtractingServerList implements ServerList<DiscoveryEnabledServer> { |
那么DiscoveryEnabledNIWSServerList
是如何实现这两个服务实例的获取的呢?我们从源码中可以看到这两个方法都是通过该类中的一个私有函数obtainServersViaDiscovery
来通过服务发现机制来实现服务实例的获取。
1 |
|
obtainServersViaDiscovery
的实现逻辑,主要依靠EurekaClient
从服务注册中心中获取到具体的服务实例InstanceInfo
列表(这里传入的vipAddress
可以理解为逻辑上的服务名,比如“USER-SERVICE”)。接着,对这些服务实例进行遍历,将状态为“UP”(正常服务)的实例转换成DiscoveryEnabledServer
对象,最后将这些实例组织成列表返回。
1 | private List<DiscoveryEnabledServer> obtainServersViaDiscovery() { |
IRule 负载均衡的策略
它有三个方法,其中choose()是根据key 来获取server,setLoadBalancer()和getLoadBalancer()是用来设置和获取ILoadBalancer的
1 | public interface IRule{ |
IRule有很多默认的实现类,这些实现类根据不同的算法和逻辑来处理负载均衡
- BestAvailableRule 选择最小请求数
- ClientConfigEnabledRoundRobinRule 轮询
- RandomRule 随机选择一个server
- RoundRobinRule 轮询选择server
- RetryRule 根据轮询的方式重试
- WeightedResponseTimeRule 根据响应时间去分配一个weight ,weight越低,被选择的可能性就越低
- ZoneAvoidanceRule 根据server的zone区域和可用性来轮询选择
IRule 使用&自定义
全局配置:调用其他微服务,一律使用指定的负载均衡算法
1 | @Configuration |
局部配置:调用指定微服务提供的服务时,使用对应的负载均衡算法
修改application.yml
1 | # 被调用的微服务名 |
自定义负载均衡策略
通过实现 IRule 接口可以自定义负载策略,主要的选择服务逻辑在 choose 方法中。
实现基于Nacos权重的负载均衡策略
1 | @Slf4j |
小结
- 通过LoadBalancerClient来实现的,
- LoadBalancerClient具体交给了BaseLoadBalancer来处理,
- BaseLoadBalancer通过配置IRule、IPing等信息,并获取注册列表的信息,并默认10秒一次发送“ping”,进而检查是否更新服务列表,
- 得到注册列表后,BaseLoadBalancer根据IRule的策略进行负载均衡。
饥饿加载
在进行服务调用的时候,如果网络情况不好,第一次调用会超时。
Ribbon默认懒加载,意味着只有在发起调用的时候才会创建客户端。
开启饥饿加载,解决第一次调用慢的问题
1 | ribbon: |
源码对应属性配置类:RibbonEagerLoadProperties