【全网首发】优雅上下线之如何安全的关闭Tomcat持久连接原创
基本信息
为了保证应用在下线的时候尽可能少的丢失流量,我们期望达到以下两个目标:
- 在停止应用之前,所有新的请求不再发送到该应用上(本次分析的问题)
- 在停止应用之前,应用正在处理的请求能够完整的处理完
下面是系统示意图:
其中客户端与服务端通过HTTP1.1 keep-alive方式通讯,SLB配置为TCP监听。
问题描述&分析
利用SLB健康检查机制,我们期望在停止服务端应用之前,SLB不会将流量再转发到正在准备下线的服务端,实际情况则是SLB会转发流量到正在准备下线的服务端,具体描述如下:
- 【系统运行态】,【客户端1】和【客户端2】通过HTTP 1.1 keep-alive方式与【ECS1】进行通讯
- 【系统发版态】,下线【ECS1】之前,会进行以下操作:
- 重命名【ECS1】的健康检查文件,使得SLB访问不到健康检查文件
- SLB访问不到健康检查文件,则认为【ECS1】处于不健康状态,则:
- 对于【客户端1】【客户端2】与【ECS1】已建立的连接,正常转发请求到【ECS1】
- 【客户端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