Full GC 后为什么 socket 连接变慢?

17

我们有一个客户端服务器应用程序,1个服务器和大约10个客户端。它们通过使用自定义查询的tcp套接字进行通信。

系统运行了很多月,但在某一时间点在每日预定的耗时约50秒的服务器FULL GC之后,我们发现客户端发送查询并从服务器接收响应之间的时间很长,超过了10-20秒。经过约3小时,系统恢复正常,一切都重新运行良好。

在调查问题时,我们发现:

  1. 客户端和服务器上都没有垃圾收集问题
  2. 服务器上的查询处理时间很短。
  3. 服务器负载很高。
  4. 网络带宽未饱和。
  5. 在FULL GC期间连接未重置(直到那时每日的FULL GC都是正常事件)
  6. 机器和操作系统最近从Centos 6(内核2.6.32)更改为Centos 7(内核3.10.0),但新配置已经经过了广泛的测试。此外,Oracle JDK版本从1.7.65更改为1.7.75。

我们在服务器上进行了线程转储:

java.lang.Thread.State: RUNNABLE
    at java.io.FilterInputStream.read(FilterInputStream.java:83)
    at util.network.BytesBasedSocketConnection$ReadConnectionRunnable.run(BytesBasedSocketConnection.java:293)
    at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:471)
    at java.util.concurrent.FutureTask.run(FutureTask.java:262)
    at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$201(ScheduledThreadPoolExecutor.java:178)
    at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:292)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
    at java.lang.Thread.run(Thread.java:745)

FilterInputStream.read() 的含义如下:

    public int read() throws IOException {
    return in.read();
}

我们代码中的in是一个BufferedInputStream

问题是:为什么大多数连接在完整GC暂停后变慢?为什么堆栈跟踪以FilterInputStream.read()结束?难道不应该在BufferedInputStream或套接字输入流中结束吗?这个read导致服务器负载高吗?

我们用于读取的代码:

int constructLength = _socketDIS.readInt();
ByteArrayOutputStream constructBOAS = new ByteArrayOutputStream(constructLength);
for (int i = 0; i != constructLength; i++)
      constructBOAS.write(_socketDIS.read());
constructBOAS.close();
byte[] bytes = constructBOAS.toByteArray();

其中:

_socketDIS = new DataInputStream(new BufferedInputStream(_socket.getInputStream()));

以下是正常工作客户端连接的堆栈跟踪:

java.lang.Thread.State: RUNNABLE
    at java.net.SocketInputStream.socketRead0(Native Method)
    at java.net.SocketInputStream.read(SocketInputStream.java:152)
    at java.net.SocketInputStream.read(SocketInputStream.java:122)
    at java.io.BufferedInputStream.fill(BufferedInputStream.java:235)
    at java.io.BufferedInputStream.read(BufferedInputStream.java:254)
    - locked <0x00007f522cbebca8> (a java.io.BufferedInputStream)
    at java.io.DataInputStream.readInt(DataInputStream.java:387)
    at util.network.BytesBasedSocketConnection$ReadConnectionRunnable.run(BytesBasedSocketConnection.java:287)
    at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:471)
    at java.util.concurrent.FutureTask.run(FutureTask.java:262)
    at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$201(ScheduledThreadPoolExecutor.java:178)
    at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:292)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
    at java.lang.Thread.run(Thread.java:745)

更新:

关于EJP的答案:

  1. 没有涉及EOS,连接是正常的,但速度非常慢。

  2. 即使有EOS,我也看不出代码怎么会在EOS处停滞,因为for循环受到constructLength值的限制。但是,建议的改进仍然有效。

  3. 问题的堆栈跟踪以对DataInputStream(_socketDIS.read())上的读取结束,该方法继承自FilterInputStream.read(),请参见上面的代码。 DataInputStream,而不是BufferedInputStream,缺少read()方法。在FilterInputStream.read()中,调用了一个BufferedInputStream上的in.read(),它具有其自己定义的read()方法。但是堆栈跟踪在中间停止,没有达到BufferedInputStream.read()。为什么?

4个回答

7
一次只读取一个字节是对CPU的浪费。请将其删除:
int constructLength = _socketDIS.readInt();
ByteArrayOutputStream constructBOAS = new ByteArrayOutputStream(constructLength);
for (int i = 0; i != constructLength; i++)
      constructBOAS.write(_socketDIS.read());
constructBOAS.close();
byte[] bytes = constructBOAS.toByteArray();

并使用以下内容:

int constructLength = _socketDIS.readInt();
byte[] bytes = new byte[constructLength];
_socketDIS.readFully(bytes);

NB _socketDIS 明显不是 BufferedInputStream,而是未经缓冲的 DataInputStream

编辑

为什么堆栈跟踪以 FilterInputStream.read() 结尾?

仔细看。 BufferedInputStream 没有实现所有三个 read() 重载。其中一个(我忘记是哪一个)在基类 FilterInputStream 中实现,另外两个 read() 重载会调用该方法。

它不应该在 BufferedInputStream 中结束吗?

不是,见上文所述。

还是在 socket input stream 中结束?

如果是阻塞式,则是在那里结束,但它不是阻塞式,可能是因为您的代码问题,在流末尾旋转。

这个读取会导致服务器负载过高吗?

会的。


好的,但大多数情况下这段代码都能正常工作。而且确保_socketDIS已经被缓存了。我很快会上传另一个使用相同代码并且正常工作的连接的堆栈跟踪。 - dcernahoschi
无论如何,这是更好的代码。它正确处理了边角情况,例如过早的EOS,而你的代码只是自旋循环。 - user207421
谢谢,我同意这样做更好,但这不是问题所在,连接始终保持活动状态,并且在某个时刻恢复,速度恢复正常。 - dcernahoschi
1
当然这是问题所在。你自己的堆栈跟踪就证明了这一点。你正在 FilterInputStream.read() 中忙碌着。你每次只复制一个字节,而你本可以通过单次读取完成所有操作。 - user207421
这并不是当且仅当此代码一直出现问题。它是吗? - user207421
显示剩余3条评论

3
堆栈跟踪显示您正在使用一个ScheduledThreadPoolExecutor。我建议您研究一下该调度程序,很可能延迟仅是因为读取遵循某种时间表 - 这对我来说似乎很荒谬。

啊,确实有一个错误使用了 ScheduledThreadPoolExecutor,但任务是持续运行的,它只是从套接字读取查询并将其放入队列中。 - dcernahoschi

1

这更像是一条扩展性评论,但是它太长了不能作为评论,所以我会用答案来提供它。

正如你所指出的那样,线程转储显示FilterInputStream.read()中的一个线程是不寻常的。虽然它可能是偶然发生的,当覆盖的FilterInputStream.read()被解析为BufferedInputStream.read()时,偶然发生的可能性似乎相当小。

考虑到这是在进行完整的垃圾回收后发生的,我认为更有可能是由于在完整的垃圾回收期间移动或卸载了BufferedInputStream类,导致解析FilterInputStream调用需要更长时间。例如,如果巧合地在完整的垃圾回收发生时没有使用BufferedInputStream对象,则可以卸载BufferedInputStream类,当需要其read()方法时需要进行类加载。这可以解释你看到的延迟,至少一次。

在某些情况下,即使没有进行完整的垃圾回收,某些类也可能在其最后一个实例被垃圾回收时被卸载。如果每次使用后都要卸载BufferedInputStream类并在下一次使用时重新加载,那么这就可以解释你所说的症状,但我通常不会期望发生这种情况。

也有可能是由于内存页面抖动导致了缓冲输入流类的丢失,特别是在使用虚拟内存时,可能会在完整垃圾回收期间重新定位该类。如果您有关于此期间内存使用情况的记录,建议查看一下。


谢谢。BufferedInputStream 实例不应该被垃圾回收,因为套接字连接仍然保持活动状态。否,内存保持较低水平,也没有出现垃圾回收。 - dcernahoschi

-1

我猜你必须尝试刷新它,以便在流中读写而不出现错误或减慢连接速度。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接