JSch SFTP 连接卡住

JSch SFTP connections getting stuck

我们有一个 java 7 代码库,我们在其中使用 Apache commons vfs2 v2.2,它使用 JSch-0.1.54 作为 sftp 提供程序。

现在,用例是通过 sftp 将文件传输到远程主机。但是,时不时地,文件上传过程会卡住。在获取应用程序的线程转储后,我们发现了两个线程(t1,它将数据发送到远程 sftp 和 t2,它接收数据来自 sftp)永远处于等待状态。下面是线程转储快照。

JSch 会话线程:

"Connect thread remote.ftp.com session" daemon prio=10 tid=0x00007f99cc243000 nid=0x144 in Object.wait() [0x00007f9985606000]
   java.lang.Thread.State: TIMED_WAITING (on object monitor)
    at java.lang.Object.wait(Native Method)
    at java.io.PipedInputStream.awaitSpace(PipedInputStream.java:273)
    at java.io.PipedInputStream.receive(PipedInputStream.java:231)
    - locked <0x000000043eda02d8> (a com.jcraft.jsch.Channel$MyPipedInputStream)
    at java.io.PipedOutputStream.write(PipedOutputStream.java:149)
    at com.jcraft.jsch.IO.put(IO.java:64)
    at com.jcraft.jsch.Channel.write(Channel.java:438)
    at com.jcraft.jsch.Session.run(Session.java:1459)
    at java.lang.Thread.run(Thread.java:748)

用于上传文件数据的应用程序线程。

"akka.actor.default-dispatcher-19" prio=10 tid=0x00007f99d4012000 nid=0xea in Object.wait() [0x00007f9988785000]
   java.lang.Thread.State: TIMED_WAITING (on object monitor)
    at java.lang.Object.wait(Native Method)
    at com.jcraft.jsch.Session.write(Session.java:1269)
    - locked <0x0000000440a61b48> (a com.jcraft.jsch.ChannelSftp)
    at com.jcraft.jsch.ChannelSftp.sendWRITE(ChannelSftp.java:2646)
    at com.jcraft.jsch.ChannelSftp.access0(ChannelSftp.java:36)
    at com.jcraft.jsch.ChannelSftp.write(ChannelSftp.java:806)
    at java.io.BufferedOutputStream.write(BufferedOutputStream.java:122)
    - locked <0x0000000440aab240> (a org.apache.commons.vfs2.provider.sftp.SftpFileObject$SftpOutputStream)
    at org.apache.commons.vfs2.util.MonitorOutputStream.write(MonitorOutputStream.java:104)
    - locked <0x0000000440aab240> (a org.apache.commons.vfs2.provider.sftp.SftpFileObject$SftpOutputStream)
    at java.io.BufferedOutputStream.flushBuffer(BufferedOutputStream.java:82)
    at java.io.BufferedOutputStream.write(BufferedOutputStream.java:126)
    - locked <0x0000000440aac278> (a org.apache.commons.vfs2.provider.DefaultFileContent$FileContentOutputStream)
    at org.apache.commons.vfs2.util.MonitorOutputStream.write(MonitorOutputStream.java:104)
    - locked <0x0000000440aac278> (a org.apache.commons.vfs2.provider.DefaultFileContent$FileContentOutputStream)
    at org.apache.commons.vfs2.provider.DefaultFileContent.write(DefaultFileContent.java:741)
    at org.apache.commons.vfs2.provider.DefaultFileContent.write(DefaultFileContent.java:720)
    at org.apache.commons.vfs2.provider.DefaultFileContent.write(DefaultFileContent.java:691)
    at org.apache.commons.vfs2.provider.DefaultFileContent.write(DefaultFileContent.java:707)
    at org.apache.commons.vfs2.FileUtil.copyContent(FileUtil.java:78)
    at org.apache.commons.vfs2.provider.AbstractFileObject.copyFrom(AbstractFileObject.java:289)

在查看了 Jsch 库的 codebase 之后,这就是我的感受。

  1. 应用程序线程正在以 4KB 的块上传文件数据。
  2. 每次块写入后,应用程序都会读取输入套接字以获得任何确认,直到输入套接字缓冲区为空。
  3. 在块写入期间,它会检查 ssh window 大小。如果它小于有效负载大小,我们会等到远程服务器调整它的大小。(这是我的应用程序线程永远等待的地方) ssh 会话线程侦听此调整大小的消息并在应用程序线程继续写入之后在通道对象处进行更新。
  4. 在一个单独的线程中,会话正在侦听来自远程服务器的传入数据。根据收到的消息,它会采取相关操作,例如调整通道大小 window,将确认消息传递给通道 (读取应用程序) 以供使用。
  5. 现在,当消息到达以供通道使用时,它被写入链接到 PipedInputStream 的 PipedOutStream。该输入流由应用程序线程读取以获取确认消息。万一应用程序线程未能读取任何消息,PipedOutputstream 的缓冲区将变满并因此进入等待状态,直到应用程序读取一些数据。 (这是会话线程永远等待的地方)

现在,两个线程都相互依赖。因此,这是一种僵局。

此外,我已经检查了这个应用程序 linux 所在的机器 运行,套接字的 RecQ 一直在建立。这意味着,套接字仍然存在,并且远程服务器不时地继续发送 32KB 数据包。

sudo netstat -anpt | grep 19321
tcp6       0      0 10.14.233.97:59594      64.233.167.130:19321    TIME_WAIT   -
tcp6   58256      0 10.14.233.97:58214      64.233.167.130:19321    ESTABLISHED 460144/java
tcp6  499888      0 10.14.233.97:58422      64.233.167.130:19321    ESTABLISHED 460144/java
tcp6       0      0 10.14.233.97:59622      64.233.167.130:19321    ESTABLISHED 460144/java
tcp6       0      0 10.14.233.97:59608      64.233.167.130:19321    TIME_WAIT   -
tcp6   74672      0 10.14.233.97:56656      64.233.167.130:19321    ESTABLISHED 460144/java
tcp6   92688      0 10.14.233.97:56842      64.233.167.130:19321    ESTABLISHED 460144/java

现在,我有 2 个问题。

  1. 为什么会这样?这种情况很少发生,但一旦发生,就会经常发生。
  2. 如何解决这个问题?

P.S. 我知道 Apache commons vfs 库的多线程问题,因此,所有 ssh 会话都是 运行 在单独的线程中.因此,它看起来不像是图书馆的问题。

Why is this happening? This happens rarely but when happens, it occurs frequently.

我觉得可能有两个原因。

  1. 网络很慢。
  2. 客户端计算机没有足够的资源,因此无法优先处理 session 线程。

对于应用程序线程写入远程 sftp 服务器的每个数据包(8KB + header 数据),它都会收到约 28 字节的确认数据。现在,此数据由连接到 PipedInputStreamPipedOutputStream 中的 session 线程写入,并由应用程序线程使用。此外,该流的缓冲区大小为 32KB。

现在,根据逻辑,应用程序线程不断将数据包写入套接字,直到 PipedInputStream 中存在最少 1KB 的数据供其使用。这大约相当于约 37 次确认。但是由于上述两个原因中的任何一个,这些 ack 数据包可能无法使用,因此应用程序线程将继续将数据包写入输出套接字,直到远程 window 大小,即 rwsize远程服务器达到其限制。

什么是rwsize

这里,rwsize是远程服务器向客户端发送通道打开确认消息时传递的参数。这是流量控制参数。根据 SSH 协议,这是对通信通道的硬性限制。此外,客户端和服务器都保留此参数的计数。对于客户端机器完成的每个数据传输字节,它会不断减小此参数的值,直到它变为~0。一旦它变为 ~0,它就会等待来自远程服务器的 window 调整大小消息,这意味着服务器已经消耗了一些未完成的数据量并且准备好进一步消耗。

现在,在我的场景中,此参数的值为 32MB。因此,我的应用程序线程能够毫无问题地写入 32MB 数据。现在,一旦达到此限制,它就会进入永远等待状态,等待远程 window 调整大小消息。现在 session 线程负责接收 window 调整大小消息和确认消息。并且这两种类型的消息都是在 FCFS 的基础上收到的。

因此,当应用程序线程进入等待状态时,session 线程可能首先开始接收 ack 消息。由于每个 ack 消息都是 ~28 字节,缓冲区是 32KB。它只能在没有任何阻塞的情况下摄取约 1170 条确认消息。但是,rwsize 是 32MB,1 个数据包是 ~8KB,很可能有 4144 ack 消息待处理。因此,如果远程服务器在生成至少 ~1170 条确认消息后生成 window 调整大小消息,则 session 线程将在能够接收 window 之前永远阻塞在 PipedOutputStream调整消息大小。这就是问题所在。

How to resolve this issue?

这个问题之前也有人遇到过。这是他们的错误报告和修复的 link。修复是增加 PipedInputStream 缓冲区大小。但是,我认为此修复程序很脆弱,除非您将缓冲区大小增加到足够大,以便它可以容纳所有可能的消息,直到 window 调整大小的消息到达。

对于我的情况,我通过确保应用程序线程在达到 rwsize 限制后进入永久等待状态之前消耗所有确认消息来修复它。代码细节可以参考this commit。下面是调用 sendWRITE 方法之前添加的代码的摘录。

if(rwsize<21+handle.length+_len+4) {
  flush();
}