tomcat的websocket链接网络问题不释放

2021-09-05 03:31

线上运行出现请求不了,查看机器cpu不高,第一次出现,以为出现慢sql了,查看jstack没有异常,怀疑是前端slb没有转发。

重启应用后修复,过了15天,再次出现接口不通,发现应用内存占用很高。

第一次尝试修复

使用jdump,查看实例数

/usr/jdk1.8.0_211/bin/jmap -histo:live [pid]

发现有一个tomcat的wsSession有10000个实例,占用了较高内存。

这个10000的数字很特殊,重点关注此业务,websocket使用的springboot的websocket封装。

怀疑是网络异常没有关闭,找到类似问题

https://bz.apache.org/bugzilla/show_bug.cgi?id=64848

怀疑是tomcat版本问题,springboot使用的默认版本是8.5.31,修改pom.xml指定tomcat的version最新的8.5.70版本。

顺带一提,8.5.51版本修复了高危漏洞,官方建议升级。

第二次尝试修复

上线后观察wsSession实例数,仍然是增高趋势,看来甩锅框架是不行了。看看自己的业务代码吧。

学习了jdump的命令

/usr/jdk1.8.0_211/bin/jmap -dump:live,format=b,file=jdump.hprof [pid]

拿到详细内存情况,学习使用jdk自带的jvis工具分析,学习使用MAT分析。

使用放google搜索,找到一个说法,需要在websocketHandler的handleTransportError方法和afterConnectionClosed再用webSocketSession.close()关闭一次。

部署上线后仍旧没有变化。

第三次尝试修复

根据close一次的思路,应该是链接状态不正常,没有关闭。

进行系统网络信息分析,使用 lsof -a 查看应用的链接信息,发现和wsSession实例数一样的tcp链接,状态为ESTABLISHED。这个状态是链接中状态。

确定为此问题,解释了为啥10000个实例,因为系统默认最高句柄是10000个,也解释了为啥应用问题是无法访问了,是无法创建新的链接通道了。

另外,吸取经验,若是无法访问,需要lsof -l看一下是否文件句柄满了。

查看自己的websocket应用超时设置。

没有异常情况呢,但是发现少设置了一个值,默认是-1表示永不超时。加上container.setAsyncSendTimeout(60*1000L);吧,也许就可以了呢?

@Bean
public ServletServerContainerFactoryBean createWebSocketContainer() {
ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
container.setMaxTextMessageBufferSize(8192);
container.setMaxBinaryMessageBufferSize(8192);
container.setMaxSessionIdleTimeout(3*60*1000L);

container.setAsyncSendTimeout(60*1000L);
return container;
}

第四次尝试修复

没错,没有用,啥都没变,而且我还发现ESTABLISHED的链接堆积加快了。。

抓紧学习tcp的知识,问题已经变成了ESTABLISHED链接堆积的问题,这个资料很多,但是没有websocket的。

这个情况有可能是客户端没有close,要么是close了但是网络问题,没来的及发送fin导致服务端还在等待。

但是如此频繁的情况,仍旧怀疑是客户端没有调用close方法,但是联系客户端的同事。他说"就算是我的问题,难道你服务端不能将链接关闭掉?"

说的有点道理啊,好吧我们看看,tcp状态保持在了连接中,一般是系统keeplive检测倒计时关闭的。

查看系统配置vi /etc/sysctl.conf,没有设置net.ipv4.tcp_fin_timeout默认是7200,2个小时好像有点长,我改成1800,/sbin/sysctl -p生效。观察情况。

第五次尝试修复

观察结果就是没有用。查看了资料websocket好像没有用keeplive啊,ping/pong的样子。链接信息中也没有timer倒计时。

问题似乎没得玩了,难道只能定时重启应用解决了么。。

既然是websocket处理,那就还是看源码吧,配合着源码分析的文章,一知半解。

重看jdump的信息,MAT分析每一个留存很久的wsSession的状态都是output_close。而且自动分析内存泄漏的情况是

websocket 657 instances of “org.apache.coyote.http11.upgrade.UpgradeProcessorInternal”, loaded by “org.springframework.boot.loader.LaunchedURLClassLoader“

感觉是更底层的事情了?我们使用spring的框架,实际操作的是spring的webSocketSession。

发现当调用spring的webSocketSession.close() 会先调用内部包装的getNativeSession就是实际的tomcat的wssession的close。

继续查看,这里调用的doClose的方法,第三个参数是false。

这个boolean影响的就是 是否调用wsRemoteEndpoint.close()方法。

感觉有点那个味道了,试一下关闭这个试试吧。

spring的websocketSession可以拿到内部包装的tomcat的session,找到一个onclose方法,可以跳过这个判断直接关闭。

加了就是上面第二个try,nativeSession直接调用onClose方法。

打完收工

没错,这个代码上线后,不再有问题链接堆积了。

这期间还学习了ss -aoen 命令,看到ESTAB的链接会转TIME_WAIT状态,然后会等待2ML时间据说就是1min后链接就释放了。

实际观察和应用内的链接数一致。

总结一下

遇到问题,总是会先排除自己代码问题,是不是框架问题?是不是客户端问题?

这样最终还是没有结果的,能改变的还是只有自己。

研究问题的过程中,学习了好多知识,好多工具,收获也很大。

加油!


第6次尝试

如此修改后,会出现更快的sockt连接占用问题,原因不明,现象就是阿里云虚拟机中的连接数正常,但是阿里云的物理机监控连接数会到10k,最后仍旧连接数占满然后请求无法到达tomcat。

根据tcpdump抓包查看,已经ESTAB的tcp连接其实没有消息进出,但是不知道为何系统没有回收。

最后的处理方法是,中间加一层nginx代理,在代理中增加 proxy_readtimeou 300; 用nginx来断超时的连接,这样tomcat可以识别到连接的释放。根据观察连接数可以保持在正常的水平了。

附上nginx代理配置

server{
    listen 8070;
    server_name _;
    location / {
        proxy_read_timeout 300s;
        proxy_send_timeout 300s;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_pass http://127.0.0.1:8071;

        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}