NACOS自定义负载

164 阅读2分钟

背景介绍

A服务要调用B服务,之后的一段时间内,要对B服务的这个线程进行操作,如果A,B服务都是集群部署的情况下,我们怎么实现。以下是我一个不太成熟的方案。 (可以理解为 A服务需要多次调用到B服务的某一台实例,仅限个别的接口需要)

架构介绍

NACOS配置、注册中心,feign调用

方案1

初步想法是把相关的几个个接口通过Hash负载到同一台服务上,话不多说,直接上代码

通过NACOS配置接口:例如:/aaa/a1,/aaa/a2,/aaa/a3 要负载到同一台服务,/bbb/b1:/bbb/b2:/bbb/b3 要负载到同一台服务

feign.loadPolicy.hash.urls=/aaa/a1:/aaa/a2:/aaa/a3;/bbb/b1:/bbb/b2:/bbb/b3

在服务启动后,把这些配置存储到hashUrlsMap缓存中

key1 = /aaa/a1  value = /aaa/a1:/aaa/a2:/aaa/a3
key1 = /aaa/a2  value = /aaa/a1:/aaa/a2:/aaa/a3
key1 = /aaa/a3  value = /aaa/a1:/aaa/a2:/aaa/a3

当请求被FeignClientInterceptor拦截时,获取相同的value值,取hash,通过hash请求相同的服务


import feign.RequestInterceptor;
import feign.RequestTemplate;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;

@Slf4j
@Component
public class FeignClientInterceptor implements RequestInterceptor {

    public static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();

    public static Map<String, String> hashUrlsMap = new HashMap<>();
    @Value("${feign.loadPolicy.hash.urls:}")
    private String hashUrls;

    @PostConstruct
    public void initHashUrlsMap() {
        if (StringUtils.isEmpty(hashUrls)) {
            log.info("==========未配置 feign.loadPolicy.hash.urls===========");
            return;
        }
        String[] pairs = hashUrls.split(",");
        for (String pair : pairs) {
            if(!pair.contains(":")){
                continue;
            }
            String[] keyValue = pair.split(":");
            for (int i = 0; i < keyValue.length; i++) {
                hashUrlsMap.put(keyValue[i], pair);
            }
        }
        log.info("==========初始化initHashUrlsMap===========");
        for (String key : hashUrlsMap.keySet()) {
            String value = hashUrlsMap.get(key);
            log.info("============Key: " + key + ", Value: " + value);
        }
    }

    @Override
    public void apply(RequestTemplate requestTemplate) {
        requestTemplate.header("webToken", getWebToken());
        String url = (requestTemplate.feignTarget().url() + requestTemplate.url()).replace("http://", "");
        String thisUrl = url.substring(url.indexOf("/"));
        if (!CollectionUtils.isEmpty(hashUrlsMap) && StringUtils.isNotEmpty(hashUrlsMap.get(thisUrl))) {
            String item = hashUrlsMap.get(thisUrl);
            threadLocal.set(Math.abs(item.hashCode()));
        }
    }

    public static String getWebToken() {
        HttpServletRequest request = getRequest();
        if(null != request){
            return getRequest().getHeader("webToken");
        }
        return null;
    }

    public static HttpServletRequest getRequest() {
        if(RequestContextHolder.getRequestAttributes() != null) {
            return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        } else {
            return null;
        }
    }
}

自定义MyHashRule 继承 ZoneAvoidanceRule,如果请求不需要hash负载则调用默认的负载规则

import com.netflix.loadbalancer.ILoadBalancer;
import com.netflix.loadbalancer.Server;
import com.netflix.loadbalancer.ZoneAvoidanceRule;
import lombok.extern.slf4j.Slf4j;

import java.util.List;

@Slf4j
public class MyHashRule extends ZoneAvoidanceRule {

    @Override
    public Server choose(Object key) {
        if (FeignClientInterceptor.threadLocal.get() != null) {
            try {
                ILoadBalancer lb = getLoadBalancer();
                List<Server> servers = getPredicate().getEligibleServers(lb.getAllServers(), key);
                if (servers.size() == 0) {
                    return null;
                }
                return servers.get(FeignClientInterceptor.threadLocal.get() % servers.size());
            } finally {
                FeignClientInterceptor.threadLocal.remove();
            }
        }
        return super.choose(key);
    }
}

就这样,第一版就顺利完成了,直到有一天.....

突然有人提问,服务扩容,缩容了怎么办

...

...

...

给了我当头一棒,这一棒,敲到了我本就格格不入的发际线,干!!!

我需要给这些服务排序,我怎么排序,怎么搞,只能夜以继日 粪发涂墙。。。

然后方案2就诞生了

方案2

思路还是那个思路,我在注册的时候,主动给注册中心加上注册时间,然后用注册时间排序不就行了

上代码:添加注册逻辑

import com.alibaba.cloud.nacos.NacosDiscoveryProperties;
import com.alibaba.cloud.nacos.registry.NacosServiceRegistry;
import org.springframework.cloud.client.serviceregistry.Registration;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;

import java.util.Map;

@Primary
@Component
public class RegistrationTimeServiceRegistry extends NacosServiceRegistry {

    public RegistrationTimeServiceRegistry(NacosDiscoveryProperties nacosDiscoveryProperties) {
        super(nacosDiscoveryProperties);
    }

    @Override
    public void register(Registration registration) {
        Map<String, String> metadata = registration.getMetadata();
        metadata.put("registration-time", String.valueOf(System.currentTimeMillis()));
        super.register(registration);
    }

}

其实在这里 hashUrlsMap 的作用就是过滤一下需要被我负载的url了,没什么大用了,大家自行优化

import feign.RequestInterceptor;
import feign.RequestTemplate;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;

@Slf4j
@Component
public class FeignClientInterceptor implements RequestInterceptor {

    public static ThreadLocal<String> threadLocal = new ThreadLocal<>();

    public static Map<String, String> hashUrlsMap = new HashMap<>();
    @Value("${feign.loadPolicy.hash.urls:}") 
    private String hashUrls;

    @PostConstruct
    public void initHashUrlsMap() {
        if (StringUtils.isEmpty(hashUrls)) {
            log.info("==========未配置 feign.loadPolicy.hash.urls===========");
            return;
        }
        String[] pairs = hashUrls.split(",");
        for (String pair : pairs) {
            if(!pair.contains(":")){
                continue;
            }
            String[] keyValue = pair.split(":");
            for (int i = 0; i < keyValue.length; i++) {
                hashUrlsMap.put(keyValue[i], pair);
            }
        }
        log.info("==========初始化initHashUrlsMap===========");
        for (String key : hashUrlsMap.keySet()) {
            String value = hashUrlsMap.get(key);
            log.info("============Key: " + key + ", Value: " + value);
        }
    }

    @Override
    public void apply(RequestTemplate requestTemplate) {
        requestTemplate.header("webToken", getWebToken());
        String url = (requestTemplate.feignTarget().url() + requestTemplate.url()).replace("http://", "");
        String thisUrl = url.substring(url.indexOf("/"));
        if (!CollectionUtils.isEmpty(hashUrlsMap) && StringUtils.isNotEmpty(hashUrlsMap.get(thisUrl))) {
            threadLocal.set(requestTemplate.feignTarget().name());
        }
    }

    public static String getWebToken() {
        HttpServletRequest request = getRequest();
        if(null != request){
            return getRequest().getHeader("webToken");
        }
        return null;
    }

    public static HttpServletRequest getRequest() {
        if(RequestContextHolder.getRequestAttributes() != null) {
            return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        } else {
            return null;
        }
    }
}

通过MyHashRule 获取注册时间最早的服务进行负载,如果它已经挂了,线程已不在、无需处理了

import com.netflix.loadbalancer.Server;
import com.netflix.loadbalancer.ZoneAvoidanceRule;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;

import java.util.Comparator;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;

@Slf4j
public class MyHashRule extends ZoneAvoidanceRule {

    @Autowired
    private DiscoveryClient discoveryClient;

    private static ConcurrentHashMap<String, Server> map = new ConcurrentHashMap<>();

    @Override
    public Server choose(Object key) {
        String apiName = FeignClientInterceptor.threadLocal.get();
        if (apiName != null) {
            try {
                return chooseServer(apiName);
            } finally {
                FeignClientInterceptor.threadLocal.remove();
            }
        }
        return super.choose(key);
    }

    private Server chooseServer(String serviceName) {
        List<ServiceInstance> instances = discoveryClient.getInstances(serviceName);
        if (instances.isEmpty()) {
            return null;
        }
        ServiceInstance earliestInstance = instances.stream()
                .filter(instance -> instance.getMetadata().containsKey("registration-time") &&
                        !instance.getMetadata().get("registration-time").isEmpty())
                .min(Comparator.comparingLong(instance ->
                        Long.parseLong(instance.getMetadata().get("registration-time"))))
                .orElse(null);
        if(earliestInstance == null){
            return null;
        }
        return new Server(earliestInstance.getHost(), earliestInstance.getPort());
    }
}

这只是我一个不成熟的想法,各位辣鸡有什么意见尽管提 不说了 去植发了