博主整理的SpringCloud系列目录:>>戳这里<<


一、基本介绍

最近根据公司业务需求,需要将传统Web系统接入SpringCloud微服务中,通过微服务网关(zuul)进行统一分发。

由于这个Web项目,一直都是通过Nginx的IP_Hash策略进行请求分发的,所以若要接入微服务架构,通过Zuul网关进行统一分发,那么必须要在分发策略上使用IP_Hash的策略。

我在网上看了各种各样博客,都没有解决核心问题 ---- 如何获取用户IP,或者说如何获取Request请求。

网上都是通过如下两种方式获取Request,其实都是从ThreadLocal中获取

// 这种是Spring提供的方式
//import org.springframework.web.context.request.RequestContextHolder;
//import org.springframework.web.context.request.ServletRequestAttributes;
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();

// 这种是Zuul提供的方式
//import com.netflix.zuul.context.RequestContext;
HttpServletRequest request = RequestContext.getCurrentContext().getRequest();

但如果直接像上面这样使用,获取到的结果是null

下面我会说说如何解决,以及为什么获取到的结果为null。

二、代码实现

先直接上解决方案

1. IpHashRule.java

实现IRule接口,自定义IP_Hash策略

package com.swotxu.eurekazuul.server.irule;

import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.AbstractLoadBalancerRule;
import com.netflix.loadbalancer.ILoadBalancer;
import com.netflix.loadbalancer.Server;
import com.netflix.zuul.context.RequestContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.List;
import java.util.Random;

/**
 * @Date: 2020/8/22 18:55
 * @Author: swotXu
 */
@Slf4j
public class IpHashRule extends AbstractLoadBalancerRule {

    public IpHashRule() {}
    
    public IpHashRule(ILoadBalancer lb) {
        this.setLoadBalancer(lb);
    }

    @Override
    public void initWithNiwsConfig(IClientConfig iClientConfig) {}

    @Override
    public Server choose(Object o) {
        return this.choose(this.getLoadBalancer(), o);
    }

    public Server choose(ILoadBalancer lb, Object o) {
        if (lb == null) {
            log.warn("No load balancer!");
            return null;
        }
        Server server = null;
        int count = 0;
        while (server == null && count++ < 10) {
            List<Server> reachableServers = lb.getReachableServers();
            List<Server> allServers = lb.getAllServers();
            int upCount = reachableServers.size();
            int serverCount = allServers.size();

            if (upCount != 0 && serverCount != 0) {
                server = allServers.get(this.ipHashIndex(serverCount));
                log.info(">>> IP_Hash 策略预选[{}]!", server);
                if (server == null)
                    Thread.yield();
                // 这里除了判断服务是否存活以及是否可用外,我还额外判断了当前服务是否存在于可用服务列表中
                else if (server.isAlive() && server.isReadyToServe() && reachableServers.contains(server)) {
                    log.info("IP_Hash 策略选择[{}]服务!<<<", server);
                    return server;
                } else
                    server = null;

                continue;
            }
            log.warn("No up servers available from load balancer: {}", lb);
            return null;
        }
        if (count >= 10) {
            log.warn("No available alive servers after 10 tries from load balancer: {}", lb);
        }
        return server;
    }

    private int ipHashIndex(int serverCount){
        String userIp = getRemoteAddr();
        int hashCode = Math.abs(userIp.hashCode());
        return hashCode % serverCount;
    }

    private String getRemoteAddr() {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        // 两种获取Request的方式,这里容易出现 null的情况,第二步是关键
        // HttpServletRequest request = RequestContext.getCurrentContext().getRequest();
        String remoteAddr = request.getRemoteAddr();
        if (request.getHeader("X-FORWARDED-FOR") != null) {
            remoteAddr = request.getHeader("X-FORWARDED-FOR");
        }
        log.debug("RemoteAddr: {}", remoteAddr);
        return remoteAddr;
    }
}
2. RequestAttributeHystrixConcurrencyStrategy.java

自定义并发策略(划重点:这一步才真正解决了Rule策略类中获取Request对象为null的问题)

package com.swotxu.eurekazuul.server.strategy;

import com.netflix.hystrix.strategy.HystrixPlugins;
import com.netflix.hystrix.strategy.concurrency.HystrixConcurrencyStrategy;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.netflix.hystrix.security.HystrixSecurityAutoConfiguration;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;

import java.util.concurrent.Callable;

@Slf4j
@Component // 注册到Spring容器
public class RequestAttributeHystrixConcurrencyStrategy extends HystrixConcurrencyStrategy {

    /**
     * 注册并发策略
     * {@link HystrixSecurityAutoConfiguration#init()}
     */
    public RequestAttributeHystrixConcurrencyStrategy() {
        // 通过HystrixPlugins注册当前扩展的HystrixConcurrencyStrategy实现
        HystrixPlugins.reset();
        HystrixPlugins.getInstance().registerConcurrencyStrategy(this);
    }

    /*此方法应写入配置类中 - 上面的构造方法作用与此方法相同
    @PostConstruct
    public void init() {
        HystrixPlugins.getInstance().registerConcurrencyStrategy(new RequestAttributeHystrixConcurrencyStrategy());
    }*/

    @Override
    public <T> Callable<T> wrapCallable(Callable<T> callable) {
    	// 切换线程后,将父线程中上下文信息,记录到子线程任务
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        return new WrappedCallable<>(callable, requestAttributes);
    }

    static class WrappedCallable<T> implements Callable<T> {

        private final Callable<T> target;
        private final RequestAttributes requestAttributes;

        public WrappedCallable(Callable<T> target, RequestAttributes requestAttributes) {
            this.target = target;
            this.requestAttributes = requestAttributes;
        }

        @Override
        public T call() throws Exception {
            try {
                // 切换线程后,将父线程中上下文信息,记录到子线程任务
                RequestContextHolder.setRequestAttributes(requestAttributes);
                return target.call();
            } finally {
            	// 任务执行完成后,清空线程本地缓存
                RequestContextHolder.resetRequestAttributes();
            }
        }
    }
}
3. 策略使用

只需要将如上两个类加入到你的网关项目中,你便拥有IP_Hash策略了,如何使用呢?

  • 编码使用

    // 注册到容器中
    @Bean
    public IpHashRule ipHashRule(){
    	return new IpHashRule();
    }
    
  • 配置使用

    # 你的服务名
    test-provider:
      ribbon:
        NFLoadBalancerRuleClassName: com.swotxu.eurekazuul.server.irule.IpHashRule
    

三、源码分析问题原因

要知道原因,先想想另外一个问题。我们为什么够通过 RequestContextHolderRequestContext 获取到 Request 用户请求呢?

我们进去看看org.springframework.web.context.request.RequestContextHolder

public abstract class RequestContextHolder {
    
    private static final ThreadLocal<RequestAttributes> requestAttributesHolder = new NamedThreadLocal("Request attributes");
    private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder = new NamedInheritableThreadLocal("Request context");

	// 省略其他代码....
	
	@Nullable
    public static RequestAttributes getRequestAttributes() {
        RequestAttributes attributes = (RequestAttributes)requestAttributesHolder.get();
        if (attributes == null) {
            attributes = (RequestAttributes)inheritableRequestAttributesHolder.get();
        }

        return attributes;
    }
}

再看看com.netflix.zuul.context.RequestContext

public class RequestContext extends ConcurrentHashMap<String, Object> {
    
    protected static Class<? extends RequestContext> contextClass = RequestContext.class;
    protected static final ThreadLocal<? extends RequestContext> threadLocal = new ThreadLocal<RequestContext>() {
        protected RequestContext initialValue() {
            try {
                return (RequestContext)RequestContext.contextClass.newInstance();
            } catch (Throwable var2) {
                throw new RuntimeException(var2);
            }
        }
    };

	public static RequestContext getCurrentContext() {
        if (testContext != null) {
            return testContext;
        } else {
            RequestContext context = (RequestContext)threadLocal.get();
            return context;
        }
    }
}

可以发现,其内部都是通过 ThreadLocal,将Reques请求对象记录到线程本地缓存中。

那么,为什么在Rule中获取到的Request对象为null呢?

首先,检查我们项目环境中是否使用了Hystrix,Zuul自带Hystrix的。
Hystrix有2个隔离策略:THREAD以及SEMAPHORE,当隔离策略为THREAD时,由于线程切换,会导致无法获取到原线程中的缓存数据。

方案一:切换隔离级别:

zuul:
  ribbon-isolation-strategy: Semaphore
hystrix:
  command:
    default:
      execution:
        isolation:
          strategy: Semaphore

但是呢,官方并不建议我们随便使用信号量级别,原文如下

Generally the only time you should use semaphore isolation for HystrixCommands is when the call is so high volume (hundreds per second, per instance) that the overhead of separate threads is too high; this typically only applies to non-network calls.

那么,在Thread级别下,有什么处理方式呢?

通过查找资料,我们知道了有这么一个抽象类HystrixConcurrencyStrategy,Hystrix并发策略插件钩子。我们看看这个类的定义

/**
 * Abstract class for defining different behavior or implementations for concurrency related aspects of the system with default implementations.
 * <p>
 * For example, every {@link Callable} executed by {@link HystrixCommand} will call {@link #wrapCallable(Callable)} to give a chance for custom implementations to decorate the {@link Callable} with
 * additional behavior.
 * <p>
 * When you implement a concrete {@link HystrixConcurrencyStrategy}, you should make the strategy idempotent w.r.t ThreadLocals.
 * Since the usage of threads by Hystrix is internal, Hystrix does not attempt to apply the strategy in an idempotent way.
 * Instead, you should write your strategy to work idempotently.  See https://github.com/Netflix/Hystrix/issues/351 for a more detailed discussion.
 * <p>
 * See {@link HystrixPlugins} or the Hystrix GitHub Wiki for information on configuring plugins: <a
 * href="https://github.com/Netflix/Hystrix/wiki/Plugins">https://github.com/Netflix/Hystrix/wiki/Plugins</a>.
 */
public abstract class HystrixConcurrencyStrategy {

意思是说,被@HystrixCommand注解的方法,其执行源Callable可以通过wrapCallable方法进行定制化装饰,加入附加的行为,继续来看看wrapCallable方法的定义

/**
 * Provides an opportunity to wrap/decorate a {@code Callable<T>} before execution.
 * <p>
 * This can be used to inject additional behavior such as copying of thread state (such as {@link ThreadLocal}).
 * <p>
 * <b>Default Implementation</b>
 * <p>
 * Pass-thru that does no wrapping.
 * 
 * @param callable
 *            {@code Callable<T>} to be executed via a {@link ThreadPoolExecutor}
 * @return {@code Callable<T>} either as a pass-thru or wrapping the one given
 */
public <T> Callable<T> wrapCallable(Callable<T> callable) {
    return callable;
}

该方法提供了在方法被执行前进行装饰的机会,可以用来复制线程状态等附加行为。

我们看下其默认实现有哪些?
在这里插入图片描述

首先来看看HystrixConcurrencyStrategyDefault

public class HystrixConcurrencyStrategyDefault extends HystrixConcurrencyStrategy {
    private static HystrixConcurrencyStrategyDefault INSTANCE = new HystrixConcurrencyStrategyDefault();

    public static HystrixConcurrencyStrategy getInstance() {
        return INSTANCE;
    }

    private HystrixConcurrencyStrategyDefault() {
    }
}

很精简的一段代码,并没有任何方法重写,其作为了一个标准提供默认实现。
继续来看看SecurityContextConcurrencyStrategy实现,直接找到wrapCallable方法

public <T> Callable<T> wrapCallable(Callable<T> callable) {
    return this.existingConcurrencyStrategy != null ?
     			this.existingConcurrencyStrategy.wrapCallable(new DelegatingSecurityContextCallable(callable)) 
     			: super.wrapCallable(new DelegatingSecurityContextCallable(callable));
}

其对Callabe进行了二次包装,继续跟进来看看DelegatingSecurityContextCallable的定义
在这里插入图片描述
其主要实现均在call方法中,红色框中标出了重点,在调用call方法前,我们可以将当前上下文信息放入SecurityContextHolder中,在执行完成后清空SecurityContextHolder对应的设置。

再来看看SecurityContextConcurrencyStrategy是如何被应用的,在HystrixSecurityAutoConfiguration中有如下代码段
在这里插入图片描述
在启动注册配置过程中会通过HystrixPlugins注册当前扩展的HystrixConcurrencyStrategy实现。

小节: 自定义扩展类实现Callable接口,并传入当前Callable变量delegate,在delegate执行call方法前后进行线程上下文的操作即可实现线程状态在父线程与子线程间的传播。

通过源码的部分解读,我们基本了解springcloud是如何实现扩展的,又是如何被应用,使得线程上下文在父线程和子线程间传播。

所以,我们只需照葫芦画瓢,实现自己的并发策略,用于传播线程上下文,并发布出去即可。

四、多种解决方案

方案一

关闭feign的熔断功能

feign:
  hystrix:
    enabled: false # 开启Feign的熔断功能
方案二

调整隔离策略

zuul:
  ribbon-isolation-strategy: Semaphore
hystrix:
  command:
    default:
      execution:
        isolation:
          strategy: Semaphore
方案三 - 推荐使用

即实现自己的线程并发策略,见目录二中的 RequestAttributeHystrixConcurrencyStrategy即可。


最后,如果这篇文章帮助到你,别忘了点赞关注收藏~

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐