如何处理传统Java NIO中的慢消费者?

3

因此,我一直在加强对传统Java非阻塞API的理解。但是API的某些方面让我有点困惑,这些方面似乎迫使我手动处理背压。

例如,WritableByteChannel.write(ByteBuffer) 的文档如下:

除非另有说明,否则写入操作仅在写入所有请求的字节后才返回。根据其状态,某些类型的通道可能只写入部分字节或可能根本不写入。例如,非阻塞模式下的套接字通道无法写入超出套接字输出缓冲区可用字节数的任何其他字节。

现在,请考虑来自Ron Hitchens书籍《Java NIO》的以下示例。

在下面的代码片段中,Ron试图演示如何在非阻塞套接字应用程序中实现回声响应(为了上下文,这里有一个gist,包含完整的示例)。
//Use the same byte buffer for all channels. A single thread is
//servicing all the channels, so no danger of concurrent access.
private ByteBuffer buffer = ByteBuffer.allocateDirect(1024);

protected void readDataFromSocket(SelectionKey key) throws Exception {
    var channel = (SocketChannel) key.channel();
    buffer.clear(); //empty buffer

    int count;
    while((count = channel.read(buffer)) > 0) {
        buffer.flip(); //make buffer readable

        //Send data; don't assume it goes all at once
        while(buffer.hasRemaining()) {
            channel.write(buffer);
        }

        //WARNING: the above loop is evil. Because
        //it's writing back to the same nonblocking
        //channel it read the data from, this code
        //can potentially spin in a busy loop. In real life
        //you'd do something more useful than this.

        buffer.clear(); //Empty buffer
    }

    if(count < 0) {
        //Close channel on EOF, invalidates the key
        channel.close();
    }
}

我对while循环向输出通道流写入的操作感到困惑:

//Send data; don't assume it goes all at once
while(buffer.hasRemaining()) {
   channel.write(buffer);
}

这让我很困惑,NIO如何帮助我?根据WriteableByteChannel.write(ByteBuffer)的描述,代码可能不会阻塞,因为如果输出通道的缓冲区已满,则此写操作不会阻塞,只是不写入任何内容并返回,缓冲区保持不变。但是在这个例子中,至少没有一种简单的方法可以在等待客户端处理这些字节的同时使用当前线程进行更有用的事情。就所有的事情而言,如果我只有一个线程,那么其他请求将在选择器中堆积,而这个while循环将“等待”客户端缓冲区打开一些空间时浪费宝贵的CPU周期。在输出通道中注册准备好状态似乎没有明显的方法。或者说有吗?
所以,假设我不是在实现一个回声服务器,而是试图实现需要向客户端发送大量字节的响应(例如文件下载),并且假设客户端的带宽非常低或输出缓冲区与服务器缓冲区相比非常小,发送此文件可能需要很长时间。似乎我们需要在我们的慢速客户端正在消耗我们的文件下载字节时使用宝贵的CPU周期来服务其他客户端。
如果输入通道已准备好,但输出通道没有准备好,似乎该线程可能会浪费宝贵的CPU周期。虽然该线程没有被阻塞,但由于执行无关紧要的CPU密集型工作而变得毫无意义,就像被阻塞一样。
为了解决这个问题,Hitchens的解决方案是将此代码移动到一个新线程--这只是将问题转移到另一个地方--。然后我想知道,如果每次需要处理长时间运行的请求时都必须打开一个线程,那么在处理此类请求时,Java NIO如何比常规IO更好?
我还不清楚如何使用传统的Java NIO来处理这些情况。就像在这种情况下承诺用更少的资源做更多工作的承诺会被打破一样。如果我正在实现一个HTTP服务器,并且我无法知道为客户端提供响应需要多长时间呢?
看起来这个例子存在严重缺陷,解决方案的良好设计应考虑在输出通道上监听可读性,例如:
registerChannel(selector, channel, SelectionKey.OP_WRITE);

但是这个解决方案会是什么样子呢?我一直在尝试想出这个解决方案,但我不知道如何适当地实现它。
我不想寻找像Netty这样的其他框架,我的目的是了解核心Java API。我感激任何人能分享的见解,任何关于只使用传统的Java NIO来处理这种背压情况的正确方式的想法。
1个回答

1
NIO的非阻塞模式使得一个线程可以请求从通道读取数据,并且只会得到当前可用的数据,如果当前没有可用的数据,则什么也不会得到。线程不会一直阻塞等待数据变为可读状态,而是可以继续做其他事情。
非阻塞写入也是同样的道理。一个线程可以请求将一些数据写入通道,但不必等待它完全写入。线程可以在此期间继续做其他事情。
当线程没有被IO调用阻塞时,它们通常会在此期间执行其他通道上的IO操作。也就是说,单个线程现在可以管理多个输入和输出通道。
因此,我认为您需要依靠解决此问题的设计方案,可能**任务或策略设计模式**是很好的选择,根据您使用的框架或应用程序,您可以决定解决方案。
但在大多数情况下,您不需要自己实现它,因为它已经在Tomcat、Jetty等中实现了。
参考资料: 非阻塞IO

谢谢你的回答。我有一个后续问题。如果你的线程正在写入数据,在传统的Java NIO中,由于输出缓冲区已满,你的缓冲区可能无法完全消耗。假设你让你的线程去做其他事情,那么你该怎么处理待发送的数据以及如何知道何时输出缓冲区已准备好接收更多数据?我觉得你必须同时注册输出通道的就绪状态。只是我从来没有在传统的Java NIO示例中看到过这种模式。 - Edwin Dalorzo
我认为没有输出缓冲区,它是一个流,一旦你向通道写入数据,它就会附加到流中,问题将出现在内存消耗上! - Hatem Mohamed
你确定吗?如果服务器和客户端的网络带宽相差很大,它会如何工作?TCP连接肯定必须缓冲字节。我在问题中分享的引用明确提到了输出缓冲区。SocketChannel和ServerSocketChannel的文档明确提到发送和接收缓冲区作为可配置选项。 - Edwin Dalorzo
实际上,我是从Java文档中阅读到这个Channels.newChannel(OutputStream out)方法的。因此,它会根据不同的通道类型而有所不同。 - Hatem Mohamed
看一下这个链接,告诉我是否有什么地方理解错了。 https://docs.oracle.com/en/java/javase/12/docs/api/java.base/java/nio/channels/Channels.html - Hatem Mohamed

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