基本信息
为了保证应用在下线的时候尽可能少的丢失流量,我们期望达到以下两个目标:
- 在停止应用之前,所有新的请求不再发送到该应用上(本次分析的问题)
- 在停止应用之前,应用正在处理的请求能够完整的处理完
下面是系统示意图:
其中客户端与服务端通过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