Sping Cloud Ribbon是一个基于HTTP和TCP的客户端负载均衡工具,它基于Netfix Ribbon实现。通过Spring Cloud的封装,可以让我们轻松地将面向服务的Rest模板请求自动转换成客户端负载均衡的服务调用。
Rest VS Rpc
Rest
严格意义上说接口很规范,操作对象即为资源,对资源的四种操作(post、get、put、delete),并且参数都放在URL上,但是不严格的说Http+json、Http+xml,常见的http api都可以称为Rest接口。
Rpc
我们常说的远程方法调用,就是像调用本地方法一样调用远程方法,通信协议大多采用二进制方式。
对比
http相对更规范,更标准,更通用,无论哪种语言都支持http协议。如果你是对外开放API,例如开放平台,外部的编程语言多种多样,你无法拒绝对每种语言的支持,相应的,如果采用http,无疑在你实现SDK之前,支持了所有语言,所以,现在开源中间件,基本最先支持的几个协议都包含RESTful。
RPC协议性能要高的多,例如Protobuf、Thrift、Kyro等,(如果算上序列化)吞吐量大概能达到http的二倍。响应时间也更为出色。千万不要小看这点性能损耗,公认的,微服务做的比较好的,例如,netflix、阿里,曾经都传出过为了提升性能而合并服务。如果是交付型的项目,性能更为重要,因为你卖给客户往往靠的就是性能上微弱的优势。你可以看看,无论是Google、Amazon、netflix(据说很可能转向grpc),还是阿里,实际上内部都是采用性能更高的RPC方式。而对外开放的才是RESTful。
总结
RPC使用高性能二进制协议,会让数据包更小,可以进行TCP长连接,性能更高。但是随之而来的是编解码问题、序列号问题、链接协议问题。
RestTemplate和Ribbon相结合
Ribbon在Netflix组件是非常重要的一个组件,在Zuul中使用Ribbon做负载均衡,以及Feign组件的结合等。在Spring Cloud 中,作为开发中,做的最多的可能是将RestTemplate和Ribbon相结合,你可能会这样写,只需要在RestTemplate的bean上面加入@LoadBalanced注解即可在使用RestTemplate发送HTTP请求时,自动实现负载均衡调用
@LoadBalanced
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
由此可见,主要的实现应该就在@LoadBalanced上,那我们就来分析下其源码。
源码解析
从@LoadBalaced注解源码的注释上可以知道,该注解用来给RestTemplate做标记,以使用负载均衡的客户端(LoadBalancerClient)来配置它。
通过搜索LoadBalancerClient可以发现,这是Spring Cloud中定义的一个接口。
public interface LoadBalancerClient extends ServiceInstanceChooser {
ServiceInstance choose(String var1);
<T> T execute(String var1, LoadBalancerRequest<T> var2) throws IOException;
<T> T execute(String var1, ServiceInstance var2, LoadBalancerRequest<T> var3) throws IOException;
URI reconstructURI(ServiceInstance var1, URI var2);
}
从该接口中,我们可以通过定义的抽象方法来了解客户端负载均衡器中应该具备的能力。
- ServiceInstance choose(String var1): 根据传入的服务名serviceId,从负载均衡器中挑选一个对应服务的实例。
- ServiceInstance choose(String var1): 使用从负载均衡器中挑选出的服务实例来执行请求内容。
- URI reconstructURI(ServiceInstance var1, URI var2): 为系统构建一个合适的host:post形式的URI。
LoadBalancerAutoConfiguration为实现客户端负载均衡器的自动化配置类。通过查看源码,我们可以验证这一点:
@Configuration
@ConditionalOnClass({RestTemplate.class})
@ConditionalOnBean({LoadBalancerClient.class})
@EnableConfigurationProperties({LoadBalancerRetryProperties.class})
public class LoadBalancerAutoConfiguration {
// 1.@AutoWired也会自动装载集合类list,会将合适的RestTemplate添加到restTemplates中
// 而至于加载哪些RestTemplate,就是标注了@LoadBalanced的RestTemplate
// 上面我们看到@LoadBalanced有一个@Qualifier就是特殊标注的含义,所以普通的没有添加@LoadBalanced
// 则不会被添加到restTemplates中的
@LoadBalanced
@Autowired(
required = false
)
private List<RestTemplate> restTemplates = Collections.emptyList();
@Autowired(
required = false
)
private List<LoadBalancerRequestTransformer> transformers = Collections.emptyList();
public LoadBalancerAutoConfiguration() {
}
// 2.SmartInitializingSingleton接口的实现类会在项目初始化之后被调用其afterSingletonsInstantiated方法
@Bean
public SmartInitializingSingleton loadBalancedRestTemplateInitializer(final List<RestTemplateCustomizer> customizers) {
return new SmartInitializingSingleton() {
public void afterSingletonsInstantiated() {
Iterator var1 = LoadBalancerAutoConfiguration.this.restTemplates.iterator();
while(var1.hasNext()) {
RestTemplate restTemplate = (RestTemplate)var1.next();
Iterator var3 = customizers.iterator();
while(var3.hasNext()) {
RestTemplateCustomizer customizer = (RestTemplateCustomizer)var3.next();
customizer.customize(restTemplate);
}
}
}
};
}
// 3.LoadBalancerRequestFactory被创建
@Bean
@ConditionalOnMissingBean
public LoadBalancerRequestFactory loadBalancerRequestFactory(LoadBalancerClient loadBalancerClient) {
return new LoadBalancerRequestFactory(loadBalancerClient, this.transformers);
}
@Configuration
@ConditionalOnClass({RetryTemplate.class})
public static class RetryInterceptorAutoConfiguration {
public RetryInterceptorAutoConfiguration() {
}
@Bean
@ConditionalOnMissingBean
public RetryLoadBalancerInterceptor ribbonInterceptor(LoadBalancerClient loadBalancerClient, LoadBalancerRetryProperties properties, LoadBalancedRetryPolicyFactory lbRetryPolicyFactory, LoadBalancerRequestFactory requestFactory) {
return new RetryLoadBalancerInterceptor(loadBalancerClient, properties, lbRetryPolicyFactory, requestFactory);
}
@Bean
@ConditionalOnMissingBean
public RestTemplateCustomizer restTemplateCustomizer(final RetryLoadBalancerInterceptor loadBalancerInterceptor) {
return new RestTemplateCustomizer() {
public void customize(RestTemplate restTemplate) {
List<ClientHttpRequestInterceptor> list = new ArrayList(restTemplate.getInterceptors());
list.add(loadBalancerInterceptor);
restTemplate.setInterceptors(list);
}
};
}
}
@Configuration
@ConditionalOnClass({RetryTemplate.class})
public static class RetryAutoConfiguration {
public RetryAutoConfiguration() {
}
@Bean
public RetryTemplate retryTemplate() {
RetryTemplate template = new RetryTemplate();
template.setThrowLastExceptionOnExhausted(true);
return template;
}
@Bean
@ConditionalOnMissingBean
public LoadBalancedRetryPolicyFactory loadBalancedRetryPolicyFactory() {
return new NeverRetryFactory();
}
}
@Configuration
@ConditionalOnMissingClass({"org.springframework.retry.support.RetryTemplate"})
static class LoadBalancerInterceptorConfig {
LoadBalancerInterceptorConfig() {
}
//4、创建拦截器 ,将LoadBalancerClient接口的实现类和3方法中创建的LoadBalancerRequestFactory
// 注入到该方法中,同时成为LoadBalancerInterceptor的参数
@Bean
public LoadBalancerInterceptor ribbonInterceptor(LoadBalancerClient loadBalancerClient, LoadBalancerRequestFactory requestFactory) {
return new LoadBalancerInterceptor(loadBalancerClient, requestFactory);
}
//5、给RestTemplate增加LoadBalancerInterceptor,方法4中创建的LoadBalancerInterceptor会被作为方法参数注入进来
@Bean
@ConditionalOnMissingBean
public RestTemplateCustomizer restTemplateCustomizer(final LoadBalancerInterceptor loadBalancerInterceptor) {
return new RestTemplateCustomizer() {
public void customize(RestTemplate restTemplate) {
List<ClientHttpRequestInterceptor> list = new ArrayList(restTemplate.getInterceptors());
list.add(loadBalancerInterceptor);
restTemplate.setInterceptors(list);
}
};
}
}
}
从LoadBalancerAutoConfiguration类头上的注解可以知道,Ribbon实现的负载均衡自动化配置需要满足下面两个条件:
- @ConditionalOnClass({RestTemplate.class}): RestTemplate类必须存在于当前工程的环境中。
- @ConditionalOnBean({LoadBalancerClient.class}): 在Spring的Bean工程中必须有LoadBalancerClient的实现Bean。
总结:经过以上的一堆注释可知,该类的主要作用就是给添加了@LoadBalanced注解的RestTemplate类,添加拦截器LoadBalancerInterceptor,该拦截器拦截到请求后将请求重新处理,就在这个拦截器中实现了负载均衡的相关功能。
LoadBalancerInterceptor.intercept()执行负载均衡拦截器方法
public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
final ClientHttpRequestExecution execution) throws IOException {
final URI originalUri = request.getURI();
String serviceName = originalUri.getHost();
Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri);
// 真正执行的方法
// private LoadBalancerClient loadBalancer; LoadBalancerClient默认实现类为RibbonLoadBalancerClient
return this.loadBalancer.execute(serviceName, requestFactory.createRequest(request, body, execution));
}
// RibbonLoadBalancerClient.execute()
public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {
// 1.根据用户请求的serviceId来获取具体的LoadBalanced
ILoadBalancer loadBalancer = getLoadBalancer(serviceId);
// 2.获取具体的server(也就是定位到哪台服务器的哪个端口号的具体服务信息)
Server server = getServer(loadBalancer);
if (server == null) {
throw new IllegalStateException("No instances available for " + serviceId);
}
RibbonServer ribbonServer = new RibbonServer(serviceId, server, isSecure(server,
serviceId), serverIntrospector(serviceId).getMetadata(server));
// 3.执行HTTP请求
return execute(serviceId, ribbonServer, request);
注意:通过这个方法的分析可以看到,里面通过一系列的算法根据用户输入的serviceId(也就是服务名)来获取到具体的服务所在host、port,然后重新封装HTTP请求,最后执行该HTTP请求即可。
下面逐个方法来分析:
getLoadBalancer()
protected ILoadBalancer getLoadBalancer(String serviceId) {
return this.clientFactory.getLoadBalancer(serviceId);
}
// SpringClientFactory.getLoadBalancer()
public ILoadBalancer getLoadBalancer(String name) {
return getInstance(name, ILoadBalancer.class);
}
// SpringClientFactory.getInstance()
public <C> C getInstance(String name, Class<C> type) {
C instance = super.getInstance(name, type);
if (instance != null) {
return instance;
}
IClientConfig config = getInstance(name, IClientConfig.class);
return instantiateWithConfig(getContext(name), type, config);
}
// NamedContextFactory.getInstance()
public <T> T getInstance(String name, Class<T> type) {
AnnotationConfigApplicationContext context = getContext(name);
if (BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context,
type).length > 0) {
// 主要就是这句,从容器中获取ILoadBalancer的实现,目前默认实现为
// ZoneAwareLoadBalancer
return context.getBean(type);
}
return null;
}
总结:通过上述一连串的方法调用可知,在最终是从容器中获取ILoadBalancer的实现,目前默认实现为ZoneAwareLoadBalancer。
getServer(loadBalancer)
protected Server getServer(ILoadBalancer loadBalancer) {
if (loadBalancer == null) {
return null;
}
// 具体执行为ZoneAwareLoadBalancer.chooseServer()
return loadBalancer.chooseServer("default"); // TODO: better handling of key
}
// ZoneAwareLoadBalancer.chooseServer()
public Server chooseServer(Object key) {
// 1.由于笔者测试的server,可用的zone为1个,所以会直接走super.chooseServer()
if (!ENABLED.get() || getLoadBalancerStats().getAvailableZones().size() <= 1) {
logger.debug("Zone aware logic disabled or there is only one zone");
return super.chooseServer(key);
}
//如果是多region,则会走下面的方法,暂时注释掉
...
}
//BaseLoadBalancer.chooseServer()
public Server chooseServer(Object key) {
if (counter == null) {
counter = createCounter();
}
counter.increment();
if (rule == null) {
return null;
} else {
try {
// rule为ZoneAvoidanceRule
return rule.choose(key);
} catch (Exception e) {
logger.warn("LoadBalancer [{}]: Error choosing server for key {}", name, key, e);
return null;
}
}
}
//PredicateBasedRule.choose(key)
public Server choose(Object key) {
ILoadBalancer lb = getLoadBalancer();
// 重点在这里,从所有的server中根据对应的rule来获取一个具体的server
Optional<Server> server = getPredicate().chooseRoundRobinAfterFiltering(lb.getAllServers(), key);
if (server.isPresent()) {
return server.get();
} else {
return null;
}
}
总结:getServer()的主要功能就是根据具体的rule来选择特定的Server,重要的实现实际都在这个方法里。
RibbonLoadBalancerClient.execute(serviceId, ribbonServer, request)执行HTTP请求
public <T> T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest<T> request) throws IOException {
...
RibbonLoadBalancerContext context = this.clientFactory
.getLoadBalancerContext(serviceId);
RibbonStatsRecorder statsRecorder = new RibbonStatsRecorder(context, server);
try {
// 核心方法在这里,是一个回调方法,
// 具体就是回调LoadBalancerRequestFactory.createRequest()中的apply()方法
T returnVal = request.apply(serviceInstance);
statsRecorder.recordStats(returnVal);
return returnVal;
}
...
}
// 回调方法
public LoadBalancerRequest<ClientHttpResponse> createRequest(final HttpRequest request,
final byte[] body, final ClientHttpRequestExecution execution) {
return new LoadBalancerRequest<ClientHttpResponse>() {
@Override
// 回调方法在这里
public ClientHttpResponse apply(final ServiceInstance instance)
throws Exception {
HttpRequest serviceRequest = new ServiceRequestWrapper(request, instance, loadBalancer);
if (transformers != null) {
for (LoadBalancerRequestTransformer transformer : transformers) {
serviceRequest = transformer.transformRequest(serviceRequest, instance);
}
}
// 真正要执行的方法
return execution.execute(serviceRequest, body);
}
};
}
//InterceptingRequestExecution.execute()
public ClientHttpResponse execute(HttpRequest request, byte[] body) throws IOException {
if (this.iterator.hasNext()) {
ClientHttpRequestInterceptor nextInterceptor = this.iterator.next();
return nextInterceptor.intercept(request, body, this);
}
// 注意:此时已经没有iterator,直接执行request请求
else {
// 1.根据URI获得请求,并封装头部
ClientHttpRequest delegate = requestFactory.createRequest(request.getURI(), request.getMethod());
for (Map.Entry<String, List<String>> entry : request.getHeaders().entrySet()) {
List<String> values = entry.getValue();
for (String value : values) {
delegate.getHeaders().add(entry.getKey(), value);
}
}
if (body.length > 0) {
StreamUtils.copy(body, delegate.getBody());
}
// 2.本质就是对HttpURLConnection的执行
return delegate.execute();
}
}
}
到此为止,URI的请求基本已经结束了。
总结
1)用户创建RestTemplate
2)添加了ribbon依赖后,会在项目启动的时候自动往RestTemplate中添加LoadBalancerInterceptor拦截器
3)用户根据RestTemplate发起请求时,会将请求转发到LoadBalancerInterceptor去执行,该拦截器会根据指定的负载均衡方式获取该次请求对应的应用服务端IP、port
4)根据获取到的IP、port重新封装请求,发送HTTP请求,返回具体响应。
负载均衡器
通过之前的分析,我们已经对Sping Cloud 如何使用Ribbon有了基本的了解。虽然Spring Cloud 中定义了LoadBalancerClient 作为负载均衡器的通用接口,并且针对Ribbon实现了RibbonLoadBalancerClient,但是它再具体实现客户端负载均衡时,是通过Ribbon的ILoadBalancer接口实现的。下面我们根据ILoadBalancer接口的实现类逐个看看它是如何实现客户端负载均衡的。
DynamicServerListLoadBalancer
它的继承类为BaseLoadBalancer,它的实现类为DynamicServerListLoadBalancer,这三者之间的关系如下:
查看上述三个类的源码,可用发现,配置以下信息,IClientConfig、IRule、IPing、ServerList、ServerListFilter和ILoadBalancer,查看BaseLoadBalancer类,它默认的情况下,实现了以下配置:
- IClientConfig ribbonClientConfig: DefaultClientConfigImpl配置
- IRule ribbonRule: RoundRobinRule 路由策略
- IPing ribbonPing: DummyPing
- ServerList ribbonServerList: ConfigurationBasedServerList
- ServerListFilter ribbonServerListFilter: ZonePreferenceServerListFilter
- ILoadBalancer ribbonLoadBalancer: ZoneAwareLoadBalancer
IClientConfig: 用于对客户端或者负载均衡的配置,它的默认实现类为DefaultClientConfigImpl。
IRule: 用于复杂均衡的策略,它有三个方法,其中choose()是根据key 来获取server,setLoadBalancer()和getLoadBalancer()是用来设置和获取ILoadBalancer的,它的源码如下:
public interface IRule{
public Server choose(Object key);
public void setLoadBalancer(ILoadBalancer lb);
public ILoadBalancer getLoadBalancer();
}
IRule有很多默认的实现类,这些实现类根据不同的算法和逻辑来处理负载均衡。Ribbon实现的IRule有一下。在大多数情况下,这些默认的实现类是可以满足需求的,如果有特性的需求,可以自己实现。
-
BestAvailableRule:选择一个最小的并发请求的Server,逐个考察Server,如果Server被tripped了,则跳过。
-
RoundRobinRule:轮询选择,轮询index,选择index对应位置的Server。
-
RandomRule:随机选择一个server
-
AvailabilityFilteringRule:过滤掉一直失败的被标记为circuit tripped的后端server,并过滤掉哪些高并发的后端Server或者使用一个AvailabilityPredicate来包含过滤server的逻辑,其实就是检查status里记录的各个Server的运行状态。
-
RetryRule:对选定的负载均衡策略加上重试机制,在一个配置时间段内当选择Server不成功,则一直尝试使用subRule的方式选择一个可用的Server。
-
WeightedResponseTimeRule:根据响应时间加权,响应时间越长,权重越小,被选中的可能性越低。
-
ZoneAvoidanceRule:复合判断Server所Zone的性能和Server的可用性选择Server,在没有Zone的环境下,类似于轮询(RoundRobinRule)。
IPing是用来想server发生"ping",来判断该server是否有响应,从而判断该server是否可用。它有一个isAlive()方法,它的源码如下:
public interface IPing {
public boolean isAlive(Server server);
}
IPing的实现类有PingUrl、PingConstant、NoOpPing、DummyPing和NIWSDiscoveryPing。它门之间的关系如下:
- PingUrl 真实的去ping 某个url,判断其是否alive
- PingConstant 固定返回某服务是否可用,默认返回true,即可用
- NoOpPing 不去ping,直接返回true,即可用。
- DummyPing 直接返回true,并实现了initWithNiwsConfig方法。
- NIWSDiscoveryPing,根据DiscoveryEnabledServer的InstanceInfo的InstanceStatus去判断,如果为InstanceStatus.UP,则为可用,否则不可用。
ServerList: 是定义获取所有的server的注册列表信息的接口,它的代码如下:
public interface ServerList<T extends Server> {
public List<T> getInitialListOfServers();
public List<T> getUpdatedListOfServers();
}
ServerListFilter: 定于了可根据配置去过滤或者根据特性动态获取符合条件的server列表的方法,代码如下:
public interface ServerListFilter<T extends Server> {
public List<T> getFilteredListOfServers(List<T> servers);
}
总结: 负载均衡器是从EurekaClient获取服务信息,并根据IRule去路由,并且根据IPing去判断服务的可用性。LoadBalancerClient是在初始化的时候,会向Eureka回去服务注册列表,并且向通过10s一次向EurekaClient发送“ping”,来判断服务的可用性,如果服务的可用性发生了改变或者服务数量和之前的不一致,则更新或者重新拉取。LoadBalancerClient有了这些服务注册列表,就可以根据具体的IRule来进行负载均衡。
懒加载
ribbon默认是使用懒加载,第一次请求才会创建RibbonClient
会导致第一次请求比较慢。我们可以关闭懒加载,开启饥饿加载。
ribbon:
eager-load:
enabled: true
clients: fs-goods
自定义配置
-
代码中配置
增加配置类,指定要单独配置的服务名。
@Configuration @RibbonClient(name = "fs-order", configuration = RibbonConfiguration.class) public class GoodsRibbonConfiguration { }
自定义配置类。这里要注意的是这个类不能被
@SpringBootApplication
的扫描注解扫描到。否则会出现父子上下文问题,导致配置从单独对某一个服务的配置,改变成对全局的配置。@Configuration public class RibbonConfiguration { @Bean public IRule ribbonRule() { return new RandomRule(); } }
-
通过配置文件
fs-goods: ribbon: NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
ribbon支持Nacos权重
我们知道nacos可以对节点进行权重分配,权重较高的会响应到更多请求。ribbon想要按照nacos的权重规则,需要自己实现方法。
@Slf4j
public class NacosWeightedRule extends AbstractLoadBalancerRule {
@Autowired
private NacosDiscoveryProperties nacosDiscoveryProperties;
@Override
public void initWithNiwsConfig(IClientConfig iClientConfig) {
// 读取配置文件,并初始化NacosWeightedRule
}
@Override
public Server choose(Object o) {
try {
BaseLoadBalancer loadBalancer = (BaseLoadBalancer)this.getLoadBalancer();
log.info("lb:{}", loadBalancer);
// 想要请求的微服务的名称
String name = loadBalancer.getName();
// 拿到服务发现的相关API
NamingService namingService = nacosDiscoveryProperties.namingServiceInstance();
// nacos client自动通过基于权重的负载均衡算法,给我们选择一个实例
Instance instance = namingService.selectOneHealthyInstance(name);
return new NacosServer(instance);
} catch (NacosException e) {
return null;
}
}
}
在配置类中指定规则。
@Configuration
public class RibbonConfiguration {
@Bean
public IRule ribbonRule() {
return new NacosWeightedRule();
}
}
扩展ribbon同一集群优先调用
如果要实现ribbon同一集群优先调用,还需要增加ribbon的rule
/**
* @program spring-alibaba
* @description:
* @author: xyhua
* @create: 2020/06/17 17:28
*/
@Slf4j
public class NacosSameClusterWeightedRule extends AbstractLoadBalancerRule {
@Autowired
private NacosDiscoveryProperties nacosDiscoveryProperties;
@Override
public void initWithNiwsConfig(IClientConfig iClientConfig) {
}
@Override
public Server choose(Object o) {
try {
// 拿到当前服务配置文件中的集群名称
String clusterName = nacosDiscoveryProperties.getClusterName();
BaseLoadBalancer loadBalancer = (BaseLoadBalancer)this.getLoadBalancer();
log.info("lb:{}", loadBalancer);
// 想要请求的微服务的名称
String name = loadBalancer.getName();
// 拿到服务发现的相关API
NamingService namingService = nacosDiscoveryProperties.namingServiceInstance();
// 1. 找到指定服务的所有实例 A
List<Instance> instances = namingService.selectInstances(name, true);
// 2. 过滤出相同集群下的所有实例 B
List<Instance> sanmeClusterInstances = instances.stream()
.filter(instance -> Objects.equal(instance.getClusterName(), clusterName))
.collect(Collectors.toList());
// 3. 如果B是空,就用A
List<Instance> instancesToBeChosen = Lists.newArrayList();
if(CollectionUtils.isEmpty(sanmeClusterInstances)) {
instancesToBeChosen = instances;
log.warn("发生跨集群的调用 name:{} clusterName:{} instances:{}", name, clusterName, instances);
} else {
instancesToBeChosen = sanmeClusterInstances;
}
// 4. 基于权重的负载均衡算法,返回1个实例
Instance in = ExtendBalancer.getHostByRandomWeight2(instancesToBeChosen);
log.info("选择的实例实 port:{} instance:{}", in.getPort(), in);
return new NacosServer(in);
} catch (NacosException e) {
return null;
}
}
}
class ExtendBalancer extends Balancer {
public static Instance getHostByRandomWeight2(List<Instance> hosts) {
return getHostByRandomWeight(hosts);
}
}
在配置中引入我们刚创建好的NacosSameClusterWeightedRule
。
@Configuration
public class RibbonConfiguration {
@Bean
public IRule ribbonRule() {
return new NacosSameClusterWeightedRule();
}
}
扩展Ribbon支持基于元数据的版本管理
我们需要实现的有两点:
- 优先选择同集群下,符合metadata的实例
- 如果同集群加没有符合metadata的实例,就选择所有集群下,符合metadata的实例
@Slf4j
public class NacosFinalRule extends AbstractLoadBalancerRule {
@Autowired
private NacosDiscoveryProperties nacosDiscoveryProperties;
@Override
public Server choose(Object key) {
// 负载均衡规则:优先选择同集群下,符合metadata的实例
// 如果没有,就选择所有集群下,符合metadata的实例
// 1. 查询所有实例 A
// 2. 筛选元数据匹配的实例 B
// 3. 筛选出同cluster下元数据匹配的实例 C
// 4. 如果C为空,就用B
// 5. 随机选择实例
try {
String clusterName = this.nacosDiscoveryProperties.getClusterName();
String targetVersion = this.nacosDiscoveryProperties.getMetadata().get("target-version");
DynamicServerListLoadBalancer loadBalancer = (DynamicServerListLoadBalancer) getLoadBalancer();
String name = loadBalancer.getName();
NamingService namingService = this.nacosDiscoveryProperties.namingServiceInstance();
// 所有实例
List<Instance> instances = namingService.selectInstances(name, true);
List<Instance> metadataMatchInstances = instances;
// 如果配置了版本映射,那么只调用元数据匹配的实例
if (StringUtils.isNotBlank(targetVersion)) {
metadataMatchInstances = instances.stream()
.filter(instance -> Objects.equals(targetVersion, instance.getMetadata().get("version")))
.collect(Collectors.toList());
if (CollectionUtils.isEmpty(metadataMatchInstances)) {
log.warn("未找到元数据匹配的目标实例!请检查配置。targetVersion = {}, instance = {}", targetVersion, instances);
return null;
}
}
List<Instance> clusterMetadataMatchInstances = metadataMatchInstances;
// 如果配置了集群名称,需筛选同集群下元数据匹配的实例
if (StringUtils.isNotBlank(clusterName)) {
clusterMetadataMatchInstances = metadataMatchInstances.stream()
.filter(instance -> Objects.equals(clusterName, instance.getClusterName()))
.collect(Collectors.toList());
if (CollectionUtils.isEmpty(clusterMetadataMatchInstances)) {
clusterMetadataMatchInstances = metadataMatchInstances;
log.warn("发生跨集群调用。clusterName = {}, targetVersion = {}, clusterMetadataMatchInstances = {}", clusterName, targetVersion, clusterMetadataMatchInstances);
}
}
Instance instance = ExtendBalancer.getHostByRandomWeight2(clusterMetadataMatchInstances);
return new NacosServer(instance);
} catch (Exception e) {
log.warn("发生异常", e);
return null;
}
}
@Override
public void initWithNiwsConfig(IClientConfig iClientConfig) {
}
}
负载均衡算法:
public class ExtendBalancer extends Balancer {
/**
* 根据权重,随机选择实例
*
* @param instances 实例列表
* @return 选择的实例
*/
public static Instance getHostByRandomWeight2(List<Instance> instances) {
return getHostByRandomWeight(instances);
}
}
总结
综上所述,Ribbon的负载均衡,主要通过LoadBalancerClient来实现的,而LoadBalancerClient具体交给了ILoadBalancer来处理,ILoadBalancer通过配置IRule、IPing等信息,并向EurekaClient获取注册列表的信息,并默认10秒一次向EurekaClient发送“ping”,进而检查是否更新服务列表,最后,得到注册列表后,ILoadBalancer根据IRule的策略进行负载均衡。