在Java系统中,实际中你可能会遇到
1.数据库连接如果忘记关闭或者其他原因没有关闭,就会出现系统运行一段时间就会卡死、后台数据库不停的报错、停止服务等等。这时候如果系统比较庞大,你使用的连接池又没有连接泄露监控功能,想排查出连接没有关闭的代码位置很难,复现也不一定容易。
2.RPC时候使用http或者其他基于tcp协议的接口调用,一些原因导致的连接没有关闭想排查也不容易。
针对上面的问题我开发了个小工具,用于监控java系统中的socket泄露。工具比较简陋,但是可以监控定位到出问题的代码位置。
3.RPC的被调用端很多close_wait
等等
原理:
拦截connect 方法。通过观察java.net.Socket源码得知,所有的socket连接都需执行
public void connect(SocketAddress endpoint, int timeout) throws IOException
那么我们就拦截它。拦截后获取当前调用栈,然后把调用栈,调用时间保存起来。现在所有的tcp访问全部记录在案。接下来,就是把关闭的连接从记录里面清除掉,拦截java.net.Socket的close方法,把当前连接记录在案的数据清除掉。这里需要注意,在connect时候可能会抛出异常,在异常时候需要把记录清除掉。
public synchronized void close() throws IOException
这两步做完,我们有了每个连接打开的时间,以及这个连接被打开的调用栈,然后根据打开时间和业务逻辑就能定位出来打开连接没有关闭的代码是哪里了。
实现
为了减少被监控应用的入侵,采用javaagent技术。关于javaagent技术请自行百度。
我所使用的jdk版本:1.8.0_172。
pom.xml
4.0.0
monitor-socket
monitor-socket
1.0-SNAPSHOT
org.jboss
javassist
3.7.1.GA
maven-assembly-plugin
package
single
jar-with-dependencies
true
true
true
pkg.MonitorAgent
org.apache.maven.plugins
maven-compiler-plugin
8
代码
package pkg;
import javassist.*;
import java.io.IOException;
import java.lang.instrument.Instrumentation;
public class MonitorAgent {
public static void premain(String agentArgs, Instrumentation inst) {
inst.addTransformer((loader, className, classBeingRedefined, protectionDomain, classfileBuffer) -> {
if(className.equals("java/net/Socket")){
ClassPool aDefault = ClassPool.getDefault();
CtClass ctClass = null;
try {
ctClass = aDefault.get("java.net.Socket");
CtClass param1 = aDefault.get("java.net.SocketAddress");
CtClass concurrentHashMapClass=aDefault.get("java.util.concurrent.ConcurrentHashMap");
CtClass mapClass=aDefault.get("java.util.Map");
CtField monitor = new CtField(mapClass, "monitorMap", ctClass);
monitor.setModifiers(Modifier.STATIC|Modifier.PUBLIC);
CtField.Initializer initializer = CtField.Initializer.byNew(concurrentHashMapClass);
ctClass.addField(monitor,initializer);
CtMethod connect = ctClass.getDeclaredMethod("connect", new CtClass[]{param1, CtClass.intType});
connect.insertBefore("java.text.SimpleDateFormat simpleDateFormat = new java.text.SimpleDateFormat(\"yyyy-MM-dd HH:mm:ss\");\n" +
"java.util.Date date=new java.util.Date();"+
" java.lang.String createTime=simpleDateFormat.format(date);" +
"java.lang.StackTraceElement[] stackTrace = java.lang.Thread.currentThread().getStackTrace();\n" +
" java.lang.StringBuilder stringBuilder=new java.lang.StringBuilder();\n" +
" for (int i = 0; i < stackTrace.length; i++) {\n" +
" java.lang.StackTraceElement stackTraceElement = stackTrace[i];\n" +
" stringBuilder.append(stackTraceElement.toString());\n" +
" stringBuilder.append(\"\\n\");\n" +
" " +
" }\n" +
" stringBuilder.append(createTime);"+
" monitorMap.put(this,stringBuilder.toString());");
//exception remove record
CtClass ioExceptionCtClass = aDefault.get("java.io.IOException");
connect.addCatch("monitorMap.remove(this);throw $e;",ioExceptionCtClass);
//hook close
CtMethod close = ctClass.getDeclaredMethod("close");
close.insertBefore("monitorMap.remove(this);");
System.out.println("hook socket success");
return ctClass.toBytecode();
} catch (NotFoundException | CannotCompileException | IOException e) {
e.printStackTrace();
}
}
return classfileBuffer;
});
}
}
访问记录的方法
如果你是web项目,新键一个Controller ,使用反射获取保存记录的map
Field monitorMap = Socket.class.getDeclaredField("monitorMap");
Map map= (Map) monitorMap.get(null);
把map中value ,就是代码的调用栈和socket打开的时间,把这个value输出到页面就行能显示当前打开的socket连接了。 每次刷新都会重新读取map中打开的socket,根据map value中的最后一行打开时间减掉当前时间就是连接被打开的时间。如果发现一个socket连接的调用栈一直在,那么基本可以判断这个连接是泄露的。如果使用了连接池技术, 因为连接池close调用不会真正的执行socket close方法,而只是放回到连接池中,短时间内看到某个连接一直在,并不能说明这个连接就是泄露的,但是随着时间变长,发生连接泄露后,连接池中的连接也会被消耗殆尽,根据记录调用栈里面的连接打开时间对泄露连接的代码怀疑。
编译方法
使用上面的pom.xml 执行mvn package ,会生成一个jar文件,使用这个jar文件完成监控。比如你的系统使用tomcat ,你编译的jar文件名是monitor.jar,路径在 /var/jar/monitor.jar 那么在tomcat的bin/catalina.sh开始增加CATALINA_OPTS=-javaagent:/var/jar/monitor.jar 。 这样配置完成。 启动之后访问你的controller,就可以看到当前系统活动的socket连接
能保证什么?
每个被打开的socket连接都map value中都会有记录,并且会记录调用栈。每个连接被关闭记录都会消除掉
不能保证什么
不能保证map value中时间很久的就是泄露的连接,需要根据自身业务进行人为判断
注意:监控活动的socket连接会造成额外的性能消耗,再定位到问题代码后,建议取消掉监控
如果文中有错误欢迎指出,感激不尽!