性能文章>【全网首发】优雅上下线之如何安全的关闭Tomcat持久连接>

【全网首发】优雅上下线之如何安全的关闭Tomcat持久连接原创

5171810

基本信息

为了保证应用在下线的时候尽可能少的丢失流量,我们期望达到以下两个目标:

  • 在停止应用之前,所有新的请求不再发送到该应用上(本次分析的问题
  • 在停止应用之前,应用正在处理的请求能够完整的处理完

下面是系统示意图:
image.png
其中客户端与服务端通过HTTP1.1 keep-alive方式通讯,SLB配置为TCP监听。

问题描述&分析

利用SLB健康检查机制,我们期望在停止服务端应用之前,SLB不会将流量再转发到正在准备下线的服务端,实际情况则是SLB会转发流量到正在准备下线的服务端,具体描述如下:
image.png

  1. 【系统运行态】,【客户端1】和【客户端2】通过HTTP 1.1 keep-alive方式与【ECS1】进行通讯
  2. 【系统发版态】,下线【ECS1】之前,会进行以下操作:
    1. 重命名【ECS1】的健康检查文件,使得SLB访问不到健康检查文件
    2. SLB访问不到健康检查文件,则认为【ECS1】处于不健康状态,则:
      1. 对于【客户端1】【客户端2】与【ECS1】已建立的连接,正常转发请求到【ECS1】
      2. 【客户端3】新建连接的时候,SLB不会将请求转发到【ECS1】,而是将请求转发到健康的ECS上

从以上分析看,SLB(TCP监听)不会断开已经与不健康ECS建立的连接,此时已建立请求的请求会正常转发到不健康的ECS上,那么如何安全的关闭这种持久连接呢?

解决方法

服务端使用的Servlet容器是Tomcat 7.0.59,在【Tomcat连接之KeepAlive逻辑分析】中已经对原理进行了分析,我们的方法是通过动态改变Socket上可接收请求数量来将持久连接安全的关闭。
可操作的切入点在Tomcat中org.apache.coyote.http11.AbstractHttp11Processor.process方法中的以下代码:

if (maxKeepAliveRequests == 1) {
	keepAlive = false;
} else if (maxKeepAliveRequests > 0 &&
		   socketWrapper.decrementKeepAlive() <= 0) {
	keepAlive = false;
}

通过上面代码可知,在【socketWrapper.decrementKeepAlive() <= 0】的情况下,keepAlive会被设置为false,Tomcat会在响应完客户端请求后,关闭Socket。
【socketWrapper.decrementKeepAlive()】方法逻辑如下:

public int decrementKeepAlive() { return (--keepAliveLeft);}

所以只要保证–keepAliveLeft<=0,即keepAliveLeft<=1就可以保证连接能够关闭掉。
如何动态改变keepAliveLeft的值呢,可以采用字节码增强的方式来实现。
如何验证思路是否可行呢?可以使用arthas进行快速验证,我们在arthas中新增一个keepalive的命令,这个命令完成字节码增强,主要代码实现如下:

KeepAliveCommand

@Name("keepalive")
@Summary("keepalive http connection for cxf")
@Description(Constants.EXPRESS_DESCRIPTION + "\nExamples:\n" +
        "  keepalive\n" +
        Constants.WIKI + Constants.WIKI_HOME + "keepalive")
public class KeepAliveCommand extends EnhancerCommand {
    private static String className;
    private static String methodName;
    static {
        className = "org.apache.tomcat.util.net.SocketWrapper";
        methodName = "decrementKeepAlive";
    }

    @Override
    protected Matcher getClassNameMatcher() {
        if (classNameMatcher == null) {
            classNameMatcher = SearchUtils.classNameMatcher(className, false);
        }
        return classNameMatcher;
    }

    @Override
    protected Matcher getClassNameExcludeMatcher() {
        return classNameExcludeMatcher;
    }

    @Override
    protected Matcher getMethodNameMatcher() {
        if (methodNameMatcher == null) {
            methodNameMatcher = SearchUtils.classNameMatcher(methodName, false);
        }
        return methodNameMatcher;
    }

    @Override
    protected AdviceListener getAdviceListener(CommandProcess process) {
        return new KeepAliveAdviceListener(this, process, GlobalOptions.verbose || this.verbose);
    }

    @Override
    protected void completeArgument3(Completion completion) {
        CompletionUtils.complete(completion, Arrays.asList(EXPRESS_EXAMPLES));
    }
}

KeepAliveAdviceListener

class KeepAliveAdviceListener extends AdviceListenerAdapter {
    private static final Logger logger = LoggerFactory.getLogger(KeepAliveAdviceListener.class);
    private KeepAliveCommand command;
    private CommandProcess process;

    public KeepAliveAdviceListener(KeepAliveCommand command, CommandProcess process, boolean verbose) {
        this.command = command;
        this.process = process;
        super.setVerbose(verbose);
    }

    @Override
    public void before(ClassLoader loader, Class<?> clazz, ArthasMethod method, Object target, Object[] args)
            throws Throwable {
        Class<?> cls = target.getClass();
        try {
            Method toString = cls.getDeclaredMethod("toString");
            String string = (String) toString.invoke(target);

            Field keepAliveLeft = cls.getDeclaredField("keepAliveLeft");
            keepAliveLeft.setAccessible(true);
            int left = (int) keepAliveLeft.get(target);

            Method setKeepAliveLeft = cls.getDeclaredMethod("setKeepAliveLeft",int.class);
            setKeepAliveLeft.invoke(target, 1);

            int afterLeft = (int) keepAliveLeft.get(target);
            logger.info("keepAliveLeft value before {} after {} , socket {}", left, afterLeft, string);
        }catch (Throwable t){
            logger.error("{} {}",args[0],args[1],t);
        }
    }

    @Override
    public void afterReturning(ClassLoader loader, Class<?> clazz, ArthasMethod method, Object target, Object[] args,
                               Object returnObject) throws Throwable {
    }

    @Override
    public void afterThrowing(ClassLoader loader, Class<?> clazz, ArthasMethod method, Object target, Object[] args,
                              Throwable throwable) {
        logger.info("{} {} {} {}",clazz.getName(),method.getName(),target,args.length);
    }
}

验证效果

image.png

参考资料

GitHub - alibaba/arthas: Alibaba Java Diagnostic Tool Arthas/Alibaba Java诊断利器Arthas

点赞收藏
大禹的足迹

在阿里搬了几年砖的大龄码农,头条号:大禹的足迹

请先登录,查看8条精彩评论吧
快去登录吧,你将获得
  • 浏览更多精彩评论
  • 和开发者讨论交流,共同进步

为你推荐

JDBC PreparedStatement 字段值为null导致TBase带宽飙升的案例分析

JDBC PreparedStatement 字段值为null导致TBase带宽飙升的案例分析

10
8