close wait解决方法 (closewait怎么复盘)

现象

在日常系统巡查系统监控时发现消息通知系统CLOSE_WAIT监控不正常,如下图:

导致closewait原因,closewait状态的原因与解决方法

图1

排查过程

登录机器通过命令查看CLOSE_WAIT状态链接信息,发现是由本地发起的连接,调用外部接口,端口是80,一般情况下应该是HTTP调用

netstat -nt | grep CL

导致closewait原因,closewait状态的原因与解决方法

图2

再确认下CLOSE WAIT在TCP状态转移中所处的阶段,IBM Documentation

导致closewait原因,closewait状态的原因与解决方法

从图中看,CLOSE_WAIT是被动关闭方的状态,被动关闭方调用close后向主动关闭方发送FIN后进入LAST_ACK状态。

至此,怀疑是应用内有http请求没有关闭造成了泄漏。瞅准时机登录机器线上dump

sudo -u tomcat jmap -dump:format=b,file=/tmp/heap.hprof 3033

发现大量unreachable_objects里面相关的对象KeepAliveStream,达到10712个

导致closewait原因,closewait状态的原因与解决方法

分析KeepAliveStream和其引用它的对象,分析下来是由大量的HtmlEmail引用持有:

导致closewait原因,closewait状态的原因与解决方法

抽样分析看有432个MimeBodyPart对象,其类为URLDataSource的属性url_conn持有:

导致closewait原因,closewait状态的原因与解决方法

进一步查看URL对象信息都是固定url下的邮件发送的附件信息

导致closewait原因,closewait状态的原因与解决方法

问题分析

到这里,已经从dump中分析出是邮件发送时造成了链接泄漏的场景。分析Apache Common Email源代码了解发送邮件的过程,为方便理解简单划分为2步骤

1.attach附件过程会创建URLDataSource(url),首先会校验文件是否存在,打开并关闭连接。

package org.apache.commons.mail;

public class MultiPartEmail extends Email {
/*...省略...*/
    public MultiPartEmail attach(URL url, String name, String description, String disposition) throws EmailException {
        try {
            InputStream is = url.openStream();
            is.close();
        } catch (IOException var6) {
            throw new EmailException("Invalid URL set:" + url, var6);
        }

        return this.attach((DataSource)(new URLDataSource(url)), name, description, disposition);
    }
/*...省略...*/
}

2.发送邮件 先获取附件的ContentType,然后获取文件流。其中获取ContentType调用的方法是DataHandler.getContentType,执行后openConnection会赋值给URLDataSource.url_conn属性。获取文件流后的InputStream之后finally里面直接关闭。

package javax.activation;
public class DataHandler implements Transferable {
/**...省略...**/
 public String getContentType() {
        return this.dataSource != null ? this.dataSource.getContentType() : this.objectMimeType;
    }

public void writeTo(OutputStream os) throws IOException {
        if (this.dataSource != null) {
            InputStream is = null;
            byte[] data = new byte[8192];
            is = this.dataSource.getInputStream();

            int bytes_read;
            try {
                while((bytes_read = is.read(data)) > 0) {
                    os.write(data, 0, bytes_read);
                }
            } finally {
                is.close();
                is = null;
            }
        } else {
            DataContentHandler dch = this.getDataContentHandler();
            dch.writeTo(this.object, this.objectMimeType, os);
        }

    }
/**...省略...**/
}

由此可看,url_conn没有被关闭滞留成为不可达对象了。ng侧超时会主动关闭连接而客户端侧没有发起close方法导致了CLOSE_WAIT状态的持续增长

另外还遗留两个问题:

1.为啥CLOSE_WAIT呈锯齿状有陡然下降的趋势?

对象都是unreachable_objects,会不会是GC导致的呢?查看PS_MarkSweep监控看完全对的上趋势

导致closewait原因,closewait状态的原因与解决方法

那么此时信息已经闭环了。FGC时会调用finallize方法里面会有close等资源释放,close方法调用后回归到图3的状态转移,CLOSE_WAIT量会掉下来。

2.为啥之前没有CLOSE_WAIT的持续增长问题?

分析发现此次case出现是由于一个邮件有400+附件需要发送,发送过程是串行的导致超大对象存活时间超长晋升到了老年代。之前邮件附件数量少,在YGC侧时就已经回收了。

问题解决

翻看Apache common email组建没找到如何提ISSUE(哪位大佬知道可指导一下嘛?)

代码已经提交在github上 GitHub : https://github.com/saxon-j/commons-email