线上运行出现请求不了,查看机器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后链接就释放了。
实际观察和应用内的链接数一致。
遇到问题,总是会先排除自己代码问题,是不是框架问题?是不是客户端问题?
这样最终还是没有结果的,能改变的还是只有自己。
研究问题的过程中,学习了好多知识,好多工具,收获也很大。
加油!
如此修改后,会出现更快的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";
}
}