JSch SFTP 连接卡住

时间:2021-07-16 09:15:40

标签: java multithreading ssh jsch apache-commons-vfs

我们有一个 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.access$100(ChannelSftp.java:36)
    at com.jcraft.jsch.ChannelSftp$1.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 窗口大小。如果它小于有效负载大小,我们会等到远程服务器调整它的大小。(这是我的应用程序线程永远等待的地方) ssh 会话线程侦听此调整大小消息,同样是在通道对象上更新,之后应用程序线程继续写入。
  4. 在一个单独的线程中,会话正在侦听来自远程服务器的传入数据。根据接收到的消息,它会执行相关操作,例如调整通道窗口的大小,将确认消息传递给通道(读取应用程序) 以供使用。
  5. 现在,当消息到达以供通道使用时,它会被写入与 PipedInputStream 链接的 PipedOutStream。此输入流由应用程序线程读取以获取确认消息。如果应用程序线程无法读取任何消息,则 PipedOutputstream 的缓冲区已满,因此它会进入等待状态,直到应用程序读取一些数据。 (这是会话线程永远等待的地方)

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

此外,我检查了运行此应用程序的 linux 机器,套接字的 RecQ 一直在建立。这意味着,socket 还活着,远程服务器时不时地发送 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

现在,我有两个问题。

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

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

1 个答案:

答案 0 :(得分:0)

<块引用>

为什么会这样?这种情况很少发生,但一旦发生,就会发生 经常。

我觉得可能有两个原因。

  1. 网络速度很慢。
  2. 客户端机器没有足够的资源,因此无法优先处理会话线程。

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

现在,按照逻辑,应用程序线程不断将数据包写入套接字,直到 PipedInputStream 中至少有 1KB 数据可供其使用。这大约转化为 ~37 个确认。但是由于以上 2 个原因中的任何一个,这些 ack 数据包可能无法使用,因此应用程序线程将继续将数据包写入输出套接字,直到远程服务器的远程窗口大小即 rwsize 达到其限制。

什么是rwsize

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

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

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

<块引用>

如何解决这个问题?

之前也有人遇到过这个问题。这是他们的错误报告和修复的 link。修复方法是增加 PipedInputStream 缓冲区大小。然而,我认为这个修复是脆弱的,除非你增加足够大的缓冲区大小,以便它可以容纳所有可能的消息,直到窗口大小调整消息到达。

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

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