优雅上下线之如何安全的关闭Tomcat持久连接

本文涉及的产品
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
可观测可视化 Grafana 版,10个用户账号 1个月
简介: 优雅上下线之如何安全的关闭Tomcat持久连接

基本信息

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

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

下面是系统示意图:

其中客户端与服务端通过HTTP1.1 keep-alive方式通讯,SLB配置为TCP监听。

问题描述&分析

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

  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);
    }
}

验证效果

参考资料

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

相关实践学习
部署高可用架构
本场景主要介绍如何使用云服务器ECS、负载均衡SLB、云数据库RDS和数据传输服务产品来部署多可用区高可用架构。
负载均衡入门与产品使用指南
负载均衡(Server Load Balancer)是对多台云服务器进行流量分发的负载均衡服务,可以通过流量分发扩展应用系统对外的服务能力,通过消除单点故障提升应用系统的可用性。 本课程主要介绍负载均衡的相关技术以及阿里云负载均衡产品的使用方法。
目录
相关文章
|
1月前
|
XML 应用服务中间件 Apache
Tomcat AJP连接器配置secretRequired=“true“,但是属性secret确实空或者空字符串,这样的组合是无效的。
Tomcat AJP连接器配置secretRequired=“true“,但是属性secret确实空或者空字符串,这样的组合是无效的。
|
6月前
|
Web App开发 应用服务中间件
解决在访问tomcat时出现连接失败,Firefox 无法建立到 localhost:8080 服务器的连接的问题~
解决在访问tomcat时出现连接失败,Firefox 无法建立到 localhost:8080 服务器的连接的问题~
126 0
|
6月前
|
网络协议 应用服务中间件 Apache
100分布式电商项目 - Tomcat性能优化(禁用AJP连接器)
100分布式电商项目 - Tomcat性能优化(禁用AJP连接器)
32 0
|
6月前
|
应用服务中间件
IDEA 配置部署JavaWeb项目在阿里云服务器的tomcat上,成功连接服务器,但Artifact 没有成功部署
IDEA 配置部署JavaWeb项目在阿里云服务器的tomcat上,成功连接服务器,但Artifact 没有成功部署
443 0
|
7月前
|
Arthas 负载均衡 网络协议
Tomcat连接之KeepAlive逻辑分析
Tomcat连接之KeepAlive逻辑分析
213 1
|
弹性计算 Java 应用服务中间件
关于购买阿里云学生服务器以及win7安装Tomcat连接服务器的过程总结
关于购买阿里云学生服务器以及win7安装Tomcat连接服务器的过程总结
138 0
|
弹性计算 Oracle 安全
阿里云学生服务器(Windows)的配置以及安装Tomcat连接服务器的教程
阿里云学生服务器(Windows)的配置以及安装Tomcat连接服务器的教程
434 0
阿里云学生服务器(Windows)的配置以及安装Tomcat连接服务器的教程
|
安全 网络协议 应用服务中间件
Tomcat安全优化
Tomcat安全优化 1.Tomcat启动端口优化 1)8005端口优化 8005端口是tcp的管理端口,修改默认的8005端口为不易猜测的端口,可以是大于1024的任意端口。
166 0
|
应用服务中间件
tomcat升级版本为8.5.68后.启动报错: java.lang.IllegalArgumentException: AJP连接器配置secretRequired=“true”
ttomcat升级版本为8.5.68后.启动报错: java.lang.IllegalArgumentException: AJP连接器配置secretRequired=“true” 属性secret确实为空 1.tomcat启动报错内容如下
790 0
tomcat升级版本为8.5.68后.启动报错: java.lang.IllegalArgumentException: AJP连接器配置secretRequired=“true”
|
弹性计算 Oracle 安全
服务器配置:阿里云服务器(Windows)的配置以及安装Tomcat连接服务器的教程
服务器配置:阿里云服务器(Windows)的配置以及安装Tomcat连接服务器的教程
1731 0
服务器配置:阿里云服务器(Windows)的配置以及安装Tomcat连接服务器的教程