Openfeign + LoadBalancer + Nacos 实现就近调用(同机房优先)负载均衡策略

提示:本文侧重使用,原理解析部分较少


前言

目前大部分系统在生产环境部署使用双中心(同城双活)策略来提高系统的可用性,从而也产生了本文面临的问题:两个中心的集群的服务,需要优先调用本中心内的子服务,如果本中心子服务不可用,则调用另一个中心的子服务;
系统使用Openfeign + LoadBalancer + Nacos,让我们看一下基于此技术栈如何使用基于loadbalancer或基于nacos,实现以上需求。

版本信息:
spring-boot:2.6.13
spring-cloud:2021.0.5
spring-cloud-alibaba:2021.0.5.0
nacos-client:2.2.0


提示:以下是本篇文章正文内容,下面案例可供参考

一、基于loadbalancer

1.原理浅析

基于loadbalancer的ZonePreferenceServiceInstanceListSupplier类可以实现,基本原理如下:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.springframework.cloud.loadbalancer.core;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.loadbalancer.config.LoadBalancerZoneConfig;
import reactor.core.publisher.Flux;

public class ZonePreferenceServiceInstanceListSupplier extends DelegatingServiceInstanceListSupplier {
    private final String ZONE = "zone";
    private final LoadBalancerZoneConfig zoneConfig;
    private String zone;

    public ZonePreferenceServiceInstanceListSupplier(ServiceInstanceListSupplier delegate, LoadBalancerZoneConfig zoneConfig) {
        super(delegate);
        this.zoneConfig = zoneConfig;
    }

    public Flux<List<ServiceInstance>> get() {
        return ((Flux)this.getDelegate().get()).map(this::filteredByZone);
    }

    private List<ServiceInstance> filteredByZone(List<ServiceInstance> serviceInstances) {
        if (this.zone == null) {
            this.zone = this.zoneConfig.getZone();
        }

        if (this.zone != null) {
            List<ServiceInstance> filteredInstances = new ArrayList();
            Iterator var3 = serviceInstances.iterator();

            while(var3.hasNext()) {
                ServiceInstance serviceInstance = (ServiceInstance)var3.next();
                String instanceZone = this.getZone(serviceInstance);
                if (this.zone.equalsIgnoreCase(instanceZone)) {
                    filteredInstances.add(serviceInstance);
                }
            }

            if (filteredInstances.size() > 0) {
                return filteredInstances;
            }
        }

        return serviceInstances;
    }

    private String getZone(ServiceInstance serviceInstance) {
        Map<String, String> metadata = serviceInstance.getMetadata();
        return metadata != null ? (String)metadata.get("zone") : null;
    }
}

ZonePreferenceServiceInstanceListSupplier类中会获取注册列表中所有服务实例,获取每个服务的元数据中的“zone”配置项,和当前服务的“zone”配置项进行“equalsIgnoreCase”对比,如果一致则加入过滤列表,最终如果过滤列表不为空,则返回过滤列表,否则返回原始的注册列表。

2.具体使用

基于以上原理,我们可以使用yaml配置来指定我们的区域偏好策略,以及我们所属的区域

1)服务提供方

# 方式一(基于loadbalancer):
spring:
  application:
    name: app2
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
        namespace: demo
        # 设置元数据
        metadata:
          # 设置区域名称-机房1
          zone: jifang1

2)服务消费方

# 方式一(基于loadbalancer):
spring:
  application:
    name: app1
  cloud:
    loadbalancer:
      # 指定负载均衡模式为区域偏好
      configurations: zone-preference
      # 指定区域偏好
      zone: jifang1
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
        namespace: demo
        # 设置元数据
        metadata:
          # 设置区域名称-机房1
          zone: jifang1

二、基于nacos

1.原理浅析

/*
 * Copyright 2013-2023 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.alibaba.cloud.nacos.loadbalancer;

import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import javax.annotation.PostConstruct;

import com.alibaba.cloud.commons.lang.StringUtils;
import com.alibaba.cloud.nacos.NacosDiscoveryProperties;
import com.alibaba.cloud.nacos.balancer.NacosBalancer;
import com.alibaba.cloud.nacos.util.InetIPv6Utils;
import com.alibaba.nacos.client.naming.utils.CollectionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Mono;

import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.DefaultResponse;
import org.springframework.cloud.client.loadbalancer.EmptyResponse;
import org.springframework.cloud.client.loadbalancer.Request;
import org.springframework.cloud.client.loadbalancer.Response;
import org.springframework.cloud.loadbalancer.core.NoopServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.core.ReactorServiceInstanceLoadBalancer;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;

/**
 * see original.
 * {@link org.springframework.cloud.loadbalancer.core.RoundRobinLoadBalancer}
 *
 * @author XuDaojie
 * @since 2021.1
 */
public class NacosLoadBalancer implements ReactorServiceInstanceLoadBalancer {

	private static final Logger log = LoggerFactory.getLogger(NacosLoadBalancer.class);

	private final String serviceId;

	private ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;

	private final NacosDiscoveryProperties nacosDiscoveryProperties;

	private static final String IPV4_REGEX = "((2(5[0-5]|[0-4]\\d))|[0-1]?\\d{1,2})(.((2(5[0-5]|[0-4]\\d))|[0-1]?\\d{1,2})){3}";

	private static final String IPV6_KEY = "IPv6";
	/**
	 * Storage local valid IPv6 address, it's a flag whether local machine support IPv6 address stack.
	 */
	public static String ipv6;

	@Autowired
	private InetIPv6Utils inetIPv6Utils;


	@PostConstruct
	public void init() {
		String ip = nacosDiscoveryProperties.getIp();
		if (StringUtils.isNotEmpty(ip)) {
			ipv6 = Pattern.matches(IPV4_REGEX, ip) ? nacosDiscoveryProperties.getMetadata().get(IPV6_KEY) : ip;
		}
		else {
			ipv6 = inetIPv6Utils.findIPv6Address();
		}
	}

	private List<ServiceInstance> filterInstanceByIpType(List<ServiceInstance> instances) {
		if (StringUtils.isNotEmpty(ipv6)) {
			List<ServiceInstance> ipv6InstanceList = new ArrayList<>();
			for (ServiceInstance instance : instances) {
				if (Pattern.matches(IPV4_REGEX, instance.getHost())) {
					if (StringUtils.isNotEmpty(instance.getMetadata().get(IPV6_KEY))) {
						ipv6InstanceList.add(instance);
					}
				}
				else {
					ipv6InstanceList.add(instance);
				}
			}
			// Provider has no IPv6, should use IPv4.
			if (ipv6InstanceList.size() == 0) {
				return instances.stream()
						.filter(instance -> Pattern.matches(IPV4_REGEX, instance.getHost()))
						.collect(Collectors.toList());
			}
			else {
				return ipv6InstanceList;
			}
		}
		return instances.stream()
				.filter(instance -> Pattern.matches(IPV4_REGEX, instance.getHost()))
				.collect(Collectors.toList());
	}

	public NacosLoadBalancer(
			ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider,
			String serviceId, NacosDiscoveryProperties nacosDiscoveryProperties) {
		this.serviceId = serviceId;
		this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider;
		this.nacosDiscoveryProperties = nacosDiscoveryProperties;
	}

	@Override
	public Mono<Response<ServiceInstance>> choose(Request request) {
		ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider
				.getIfAvailable(NoopServiceInstanceListSupplier::new);
		return supplier.get().next().map(this::getInstanceResponse);
	}

	private Response<ServiceInstance> getInstanceResponse(
			List<ServiceInstance> serviceInstances) {
		if (serviceInstances.isEmpty()) {
			log.warn("No servers available for service: " + this.serviceId);
			return new EmptyResponse();
		}

		try {
			String clusterName = this.nacosDiscoveryProperties.getClusterName();

			List<ServiceInstance> instancesToChoose = serviceInstances;
			if (StringUtils.isNotBlank(clusterName)) {
				List<ServiceInstance> sameClusterInstances = serviceInstances.stream()
						.filter(serviceInstance -> {
							String cluster = serviceInstance.getMetadata()
									.get("nacos.cluster");
							return StringUtils.equals(cluster, clusterName);
						}).collect(Collectors.toList());
				if (!CollectionUtils.isEmpty(sameClusterInstances)) {
					instancesToChoose = sameClusterInstances;
				}
			}
			else {
				log.warn(
						"A cross-cluster call occurs,name = {}, clusterName = {}, instance = {}",
						serviceId, clusterName, serviceInstances);
			}
			instancesToChoose = this.filterInstanceByIpType(instancesToChoose);

			ServiceInstance instance = NacosBalancer
					.getHostByRandomWeight3(instancesToChoose);

			return new DefaultResponse(instance);
		}
		catch (Exception e) {
			log.warn("NacosLoadBalancer error", e);
			return null;
		}
	}

}

NacosLoadBalancer类中会获取注册列表中所有服务实例,获取每个服务的元数据中的“nacos.cluster”配置项,和当前服务的“spring.cloud.nacos.discovery.cluster-name”配置项进行“equals”对比,如果一致则加入同集群列表,最终如果同集群列表不为空,则返回同集群列表,否则返回原始的注册列表。

2.具体使用

基于以上原理,我们可以使用yaml配置来指定我们的集群名称,进而实现同集群优先调用

1)服务提供方

# 方式二(基于nacos):
spring:
  application:
    name: app2
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
        namespace: demo
        # 指定集群名-机房1
        cluster-name: jifang1

2)服务消费方

# 方式二(基于nacos):
spring:
  application:
    name: app1
  cloud:
    loadbalancer:
      # 指定负载均衡模式为nacos
      nacos:
        enabled: true
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
        namespace: demo
        # 指定集群名-机房1
        cluster-name: jifang1

总结

以上就是同机房优先调用的实现方式。

  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
如果要在Gateway中使用LoadBalancer实现按权重负载均衡,并且权重值从Nacos读取,可以按照以下步骤进行: 1. 首先,需要在Nacos中为每个服务实例设置权重值。可以在Nacos的服务注册页面中,为每个实例设置metadata,例如:`weight=5`表示该实例的权重为5。 2. 在Gateway中,需要使用`@LoadBalanced`注解标注`RestTemplate`实例,这样就可以使用LoadBalancer进行负载均衡。同时,需要使用`@Configuration`注解标注一个配置类,并在该类中创建一个`@Bean`方法,返回一个`LoadBalancerClientFilter`实例,用于拦截请求并进行负载均衡。 3. 在`LoadBalancerClientFilter`中,可以通过`LoadBalancerClient`来获取到服务实例列表,并通过`LoadBalancer`来选择一个实例。在选择实例时,可以根据实例的权重值进行加权随机选择,从而实现按权重负载均衡。 下面是一个示例代码,用于演示如何实现按权重负载均衡: ```java @Configuration public class GatewayConfig { @Autowired private LoadBalancerClientFactory loadBalancerClientFactory; @LoadBalanced @Bean public RestTemplate restTemplate() { return new RestTemplate(); } @Bean public LoadBalancerClientFilter loadBalancerClientFilter() { return new LoadBalancerClientFilter(loadBalancerClientFactory) { @Override protected ServiceInstance choose(ServerWebExchange exchange, LoadBalancerClient client) { String serviceId = exchange.getRequest().getHeaders().getFirst(HttpHeaders.HOST); List<ServiceInstance> instances = client.choose(serviceId); if (instances == null || instances.isEmpty()) { return null; } if (instances.size() == 1) { return instances.get(0); } // 计算实例权重总和 int totalWeight = instances.stream().mapToInt(instance -> { String weightStr = instance.getMetadata().get("weight"); int weight = StringUtils.isEmpty(weightStr) ? 1 : Integer.parseInt(weightStr); return weight; }).sum(); // 使用加权随机算法选择实例 int randomWeight = new Random().nextInt(totalWeight); for (ServiceInstance instance : instances) { String weightStr = instance.getMetadata().get("weight"); int weight = StringUtils.isEmpty(weightStr) ? 1 : Integer.parseInt(weightStr); if (randomWeight < weight) { return instance; } randomWeight -= weight; } return instances.get(0); } }; } } ``` 在上述代码中,我们通过`LoadBalancerClient`获取到服务实例列表,然后计算实例的权重总和,并使用加权随机算法选择一个实例。注意,在计算权重时,需要先获取到实例的metadata,然后从中获取到权重值。如果metadata中没有设置权重值,则默认权重为1。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值