通过 Spring Websocket STOMP 打开连接会导致我们的服务器死机

Posted

技术标签:

【中文标题】通过 Spring Websocket STOMP 打开连接会导致我们的服务器死机【英文标题】:Open connections via Spring Websocket STOMP cause our server to die 【发布时间】:2019-02-23 15:41:27 【问题描述】:

所以我们在后端使用 Spring websocket STOMP + RabbitMQ,我们在打开文件描述符时遇到了问题。一段时间后,我们达到了服务器的限制,服务器不接受任何连接,包括 websocket 和 API 端点。

2018-09-14 18:04:13.605  INFO 1288 --- [MessageBroker-1] 
o.s.w.s.c.WebSocketMessageBrokerStats    : WebSocketSession[2 current WS(2)- 
HttpStream(0)-HttpPoll(0), 1159 total, 0 closed abnormally (0 connect 
failure, 0 send limit, 63 transport error)], stompSubProtocol[processed 
CONNECT(1014)-CONNECTED(1004)-DISCONNECT(0)], stompBrokerRelay[9 sessions, 
127.0.0.1:61613 (available), processed CONNECT(1015)-CONNECTED(1005)- 
DISCONNECT(1011)], inboundChannel[pool size = 2, active threads = 2, queued 
tasks = 2, completed tasks = 12287], outboundChannelpool size = 0, active 
threads = 0, queued tasks = 0, completed tasks = 4225], sockJsScheduler[pool 
size = 1, active threads = 1, queued tasks = 3, completed tasks = 683]

我们得到以下例外:

2018-09-14 18:04:13.761 ERROR 1288 --- [http-nio-127.0.0.1-8443-Acceptor-0] 
org.apache.tomcat.util.net.NioEndpoint   : Socket accept failed

java.io.IOException: Too many open files
    at sun.nio.ch.ServerSocketChannelImpl.accept0(Native Method)
    at sun.nio.ch.ServerSocketChannelImpl.accept(ServerSocketChannelImpl.java:422)
    at sun.nio.ch.ServerSocketChannelImpl.accept(ServerSocketChannelImpl.java:250)
    at org.apache.tomcat.util.net.NioEndpoint$Acceptor.run(NioEndpoint.java:455)
    at java.lang.Thread.run(Thread.java:748)

linux 的默认文件描述符限制是 1024,即使我们将其增加到 65000 之类的东西,无论如何它都会在某个时候达到限制。

我们希望从后端解决这个问题,最好是通过 Spring 解决,而不需要任何解决方法。有什么想法吗?

更新

RabbitMQ 和应用程序位于不同的服务器上。实际上,RabbitMQ 在 Compose 上工作。我们可以通过不从客户端发送 DISCONNECT 消息来重现此问题。

更新 2

今天我意识到,无论发生什么,所有的文件描述符和 java 线程总是留在那里。我已经实现了一个解决方法,包括从 Spring 发送 DISCONNECT 消息并关闭 WebSocketSession 对象并且没有任何更改。我通过检查以下链接实现了这些:

Disconnect client session from Spring websocket stomp server https://github.com/isaranchuk/spring-websocket-disconnect https://github.com/rstoyanchev/spring-websocket-portfolio

附带说明,服务器端发送如下消息: simpMessagingTemplate.convertAndSend("/queue/" + sessionId, payload)。这样,我们保证每个客户端通过相关的sessionId.得到对应的消息

这是某种错误吗?为什么不关闭文件描述符?以前没有人遇到过这个问题吗?

更新 3

每次关闭套接字时,我都会看到以下异常。无论是通过来自客户端的 DISCONNECT 消息还是来自服务器的 webSocketSession.close() 代码来关闭它都无关紧要。

[reactor-tcp-io-66] o.s.m.s.s.StompBrokerRelayMessageHandler : TCP connection failure in session 45r7i9u3: Transport failure: epoll_ctl(..) failed: No such file or directory
io.netty.channel.unix.Errors$NativeIoException: epoll_ctl(..) failed: No such file or directory
at io.netty.channel.unix.Errors.newIOException(Errors.java:122)
at io.netty.channel.epoll.Native.epollCtlMod(Native.java:134)
at io.netty.channel.epoll.EpollEventLoop.modify(EpollEventLoop.java:186)
at io.netty.channel.epoll.AbstractEpollChannel.modifyEvents(AbstractEpollChannel.java:272)
at io.netty.channel.epoll.AbstractEpollChannel.clearFlag(AbstractEpollChannel.java:125)
at io.netty.channel.epoll.AbstractEpollChannel$AbstractEpollUnsafe.clearEpollRdHup(AbstractEpollChannel.java:450)
at io.netty.channel.epoll.AbstractEpollChannel$AbstractEpollUnsafe.epollRdHupReady(AbstractEpollChannel.java:442)
at io.netty.channel.epoll.EpollEventLoop.processReady(EpollEventLoop.java:417)
at io.netty.channel.epoll.EpollEventLoop.run(EpollEventLoop.java:310)
at io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:884)
at java.lang.Thread.run(Thread.java:748)

所以我将日志级别更改为TRACE,我看到 websockets 确实被关闭了,但是这些异常立即被抛出。所以在这一点上,我真的很怀疑这个例外。挂起的 java 线程的数量总是与 websocket 的数量齐头并进,即创建 400 个 websocket 总是在主进程中最终导致 ~400 个挂起的线程。并且内存资源永远不会被释放。

用谷歌搜索这个异常只会得到以下 4 个结果:(其余的是其他异常)

https://github.com/netty/netty/issues/2414 https://github.com/reactor/reactor-ipc/issues/16 https://github.com/cloudfoundry/cf-java-client/issues/495 https://github.com/cloudfoundry/cf-java-client/issues/480

netty 库更新到最新版本(4.1.29.Final)也不起作用,所以我相应地更改了问题的标签。我也在考虑针对netty 创建一个问题。我已经尝试了很多东西,并在应用程序级别上进行了多次实验,但似乎没有任何效果。在这一点上,我对任何想法都持开放态度。

【问题讨论】:

您使用的是什么 spring/rabbitMQ 版本?也许这是图书馆中的一些隐藏错误。 Rabbit 是否与受影响的应用程序在同一台服务器上运行?您可以尝试使用 JVisualVM 或其他类似工具进行分析,尤其是在 IOException 发生后在 heapdump 中搜索持有打开套接字等的对象 嗨@KamilPiwowarski。 RabbitMq 版本是 3.7.7,Spring Boot 版本是 1.5.9。当客户端不发送 DISCONNECT 消息时会发生这种情况。所以我认为这是一个安全漏洞。当我尝试编写 JS 代码并且不发送断开连接时,大约 1000 条消息后,服务器出现故障。我不确定其他人如何解决这个问题,因为它看起来像一个常见问题。 Rabbit 不在同一台服务器上,实际上它在 Compose 上。我也会尝试使用 JVisualVM 来查看它。 @leventunver 您的 RabbitMQ 客户端库版本是什么,您是否使用任何包装库来管理连接,例如spring-amqp. 您找到解决此问题的方法了吗?我也面临同样的问题?提前感谢您的帮助 @SulimanAlzamel 是的,但我做了一个完全独立的问题。这是解决方案:***.com/a/48660544/6999882 【参考方案1】:

如果您总是使用try-with-resource 或在 finally 块中关闭您打开的文件,您可能真的超出了您的文件描述符限制,您需要另一个主机来接受您的请求。为此,您需要扩展您的应用程序并对其进行负载平衡。我建议你在集群中部署rabbitmq来解决这个问题。

有RabbitMQ disregards your file descriptor limits的情况。

【讨论】:

嗨。感谢您的回答。我添加了有关上下文的更多详细信息。所以我们已经有了负载均衡器,并且 RabbitMQ 可以在 Compose 和与应用程序本身不同的机器上工作。我们如何重现该问题是通过忽略 websockets 上的 DISCONNECT 消息。如果您不这样做,那么 websocket 将永远保持打开状态,这很糟糕。我们正在寻找一种从后端解决它的方法。 @leventunver 对此我深表歉意。就一个问题。您是否从服务器和客户端发送心跳?为什么您的 stompBrokerRelay 在您显示的日志中连接到 127.0.0.1?这仅用于演示目的吗? 服务器位于 nginx 反向代理后面。这就是为什么它们被视为 localhost ip。我没有为心跳明确实现任何东西,但是当客户端 js 在浏览器上打开时,我看到了 HEARTBEAT 消息。客户端关闭后心跳消失。服务器端没有心跳。那会解决吗? 附加信息:队列被创建为rabbitmq上的自动删除。即使队列在一段时间后关闭并且浏览器连接关闭,我仍然可以看到 Java 线程挂在那里。抱歉,如果我对这个话题说的话是假的,因为我是这方面的新手。【参考方案2】:

RabbitMQ Java client library 有时会在管理打开的文件描述符时出现问题。它很少是坏的,但有一些问题,例如ChannelManager line 218.

您想尝试几个不同的 Java 客户端库版本,因为这是客户端问题。在一个版本中,由于连接创建错误,我产生了数千个 Java 线程(不确定哪个版本受到影响,我通过使用 FlightRecorder 并转到锁定部分发现了这一点,所有线程都在等待获取 RabbitMQ 连接(?)类锁)。

【讨论】:

以上是关于通过 Spring Websocket STOMP 打开连接会导致我们的服务器死机的主要内容,如果未能解决你的问题,请参考以下文章

通过 Spring Websocket STOMP 打开连接会导致我们的服务器死机

如何使用 SockJs 通过 STOMP Java 客户端验证 Spring 非 Web Websocket?

如何使用 Spring WebSocket 向 STOMP 客户端发送错误消息?

Spring 4 websocket + stomp + rabbitmq 和集群

Spring 4 - websocket 消息传递 stomp 处理程序

如何使用 spring+stomp+sockjs 获得 websocket 响应