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

huangapple go评论92阅读模式
英文:

How to deal with a slow consumer in traditional Java NIO?

问题

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

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

除非另有规定,写操作将仅在写入所有请求的字节后返回。某些类型的通道,取决于其状态,可能仅写入一些字节,或者可能根本不写入。例如,非阻塞模式下的套接字通道不能写入超出套接字输出缓冲区可用字节的字节。

现在,考虑一下来自Ron Hitchens书籍《Java NIO》(https://www.oreilly.com/library/view/java-nio/0596002882/)的这个示例。

在下面的代码片段中,Ron试图演示如何在非阻塞套接字应用程序中实现回显响应(为了上下文,这是一个包含完整示例的gist)。

// 使用相同的字节缓冲区处理所有通道。一个线程服务所有通道,所以不会出现并发访问的问题。
private ByteBuffer buffer = ByteBuffer.allocateDirect(1024);

protected void readDataFromSocket(SelectionKey key) throws Exception {
    var channel = (SocketChannel) key.channel();
    buffer.clear(); // 清空缓冲区

    int count;
    while ((count = channel.read(buffer)) > 0) {
        buffer.flip(); // 使缓冲区可读

        // 发送数据;不要假设它一次发送完毕
        while (buffer.hasRemaining()) {
            channel.write(buffer);
        }

        // 警告:上面的循环是有问题的。因为
        // 它正在向从中读取数据的同一非阻塞
        // 通道写入数据,这段代码可能潜在地在忙碌循环中自旋。
        // 在实际生活中,你会做一些更有用的事情。

        buffer.clear(); // 清空缓冲区
    }

    if (count < 0) {
        // 在EOF时关闭通道,使键无效
        channel.close();
    }
}

我的困惑在于写入输出通道流的while循环:

// 发送数据;不要假设它一次发送完毕
while (buffer.hasRemaining()) {
   channel.write(buffer);
}

这段代码真的让我感到困惑,NIO如何帮助我在这里。毫无疑问,根据WritableByteChannel.write(ByteBuffer)的描述,代码可能不会阻塞,因为如果输出通道不能接受更多字节,因为其缓冲区已满,这个写操作不会阻塞,它只是写入空内容,然后返回,缓冲区保持不变。但是,至少在这个示例中,没有明显的方法可以在等待客户端处理这些字节的同时使用当前线程进行更有用的操作。就这一点而言,如果我只有一个线程,那么其他请求将在选择器中堆积,而这个while循环将浪费宝贵的CPU周期“等待”客户端缓冲区打开一些空间。在输出通道上没有明显的注册准备就绪的方法。或者有吗?

因此,假设我不是实现回显服务器,而是尝试实现需要将大量字节发送回客户端的响应(例如,文件下载),并且假设客户端带宽很低或输出缓冲区与服务器缓冲区相比非常小,发送这个文件可能需要很长时间。似乎我们需要在等待慢客户端处理文件下载字节时,使用宝贵的CPU周期来为其他客户端提供服务。

如果我们在输入通道上有准备就绪的功能,但在输出通道上没有,那么这个线程似乎会白白浪费宝贵的CPU周期。虽然它没有被阻塞,但它就像被阻塞了一样,因为线程在无法确定的时间内执行无关紧要的CPU密集型工作。

为了解决这个问题,Hitchens的解决方案是将这段代码移到一个新线程中——这只是将问题转移到另一个地方。然后我想知道,如果我们每次需要处理长时间运行的请求时都必须打开一个线程,那么Java NIO在处理这种类型的请求时与常规IO相比有何优势?

目前,我还不清楚如何使用传统的Java NIO来处理这些情况。在这种情况下,好像利用更少资源的承诺会被打破。如果我正在实现一个HTTP服务器,并且无法知道为客户端提供响应需要多长时间,那该怎么办?

看起来,这个示例似乎存在深层次的缺陷,解决方案的良好设计应该考虑在输出通道上监听准备就绪的情况,例如:

registerChannel(selector, channel, SelectionKey.OP_WRITE);

但这个解决方案会是什么样子?我一直在尝试提出解决方案,但我不知道如何适当地实现它。

我不寻找其他框架,比如Netty,我的目标是理

英文:

So, I've been brushing up my understanding of traditional Java non-blocking API. I'm a bit confused with a few aspects of the API that seem to force me to handle backpressure manually.

For example, the documentation on WritableByteChannel.write(ByteBuffer) says the following:

> Unless otherwise specified, a write operation will return only after
> writing all of the requested bytes. Some types of channels,
> depending upon their state, may write only some of the bytes or
> possibly none at all. A socket channel in non-blocking mode, for
> example, cannot write any more bytes than are free in the socket's
> output buffer.

Now, consider this example taken from Ron Hitchens book: Java NIO.

In the piece of code below, Ron is trying to demonstrate how we could implement an echo response in a non-blocking socket application (for context here's a gist with the full example).

//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)) &gt; 0) {
        buffer.flip(); //make buffer readable

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

        //WARNING: the above loop is evil. Because
        //it&#39;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&#39;d do something more useful than this.

        buffer.clear(); //Empty buffer
    }

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

My confusion is on the while loop writing into output channel stream:

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

It really confuses me how NIO is helping me here. Certainly the code may not be blocking as per the description of the WriteableByteChannel.write(ByteBuffer), because if the output channel cannot accept any more bytes because its buffer is full, this write operation does not block, it just writes nothing, returns, and the buffer remains unchanged. But --at least in this example-- there is no easy way to use the current thread in something more useful while we wait for the client to process those bytes. For all that matter, if I only had one thread, the other requests would be piling up in the selector while this while loop wastes precious cpu cycles “waiting” for the client buffer to open some space. There is no obvious way to register for readiness in the output channel. Or is there?

So, assuming that instead of an echo server I was trying to implement a response that needed to send a big number of bytes back to the client (e.g. a file download), and assuming that the client has a very low bandwidth or the output buffer is really small compared to the server buffer, the sending of this file could take a long time. It seems as if we need to use our precious cpu cycles attending other clients while our slow client is chewing our file download bytes.

If we have readiness in the input channel, but not on the output channel, it seems this thread could be using precious CPU cycles for nothing. It is not blocked, but it is as if it were since the thread is useless for undetermined periods of time doing insignificant CPU-bound work.

To deal with this, Hitchens' solution is to move this code to a new thread --which just moves the problem to another place--. Then I wonder, if we had to open a thread every time we need to process a long running request, how is Java NIO better than regular IO when it comes to processing this sort of requests?

It is not yet clear to me how I could use traditional Java NIO to deal with these scenarios. It is as if the promise of doing more with less resources would be broken in a case like this. What if I were implementing an HTTP server and I cannot know how long it would take to service a response to the client?

It appears as if this example is deeply flawed and a good design of the solution should consider listening for readiness on the output channel as well, e.g.:

registerChannel(selector, channel, SelectionKey.OP_WRITE);

But how would that solution look like? I’ve been trying to come up with that solution, but I don’t know how to achieve it appropriately.

I'm not looking for other frameworks like Netty, my intention is to understand the core Java APIs. I appreciate any insights anyone could share, any ideas on what is the proper way to deal with this back pressure scenario just using traditional Java NIO.

答案1

得分: 1

NIO的非阻塞模式使线程能够请求从通道中读取数据,只获取当前可用的数据,如果当前没有数据可用,则不获取任何数据。线程不会像在数据可供读取之前保持阻塞状态,它可以继续进行其他操作。

对于非阻塞写入也是如此。线程可以请求将一些数据写入通道,但不等待数据完全写入。然后,线程可以在此期间继续做其他事情。

当线程在非IO调用中未被阻塞时,它们通常会在此期间执行其他通道的IO操作。也就是说,现在单个线程可以管理多个输入和输出通道。

因此,我认为您需要依靠解决此问题的设计,可以使用处理此问题的设计模式,也许“任务”或“策略”设计模式是很好的选择,根据您使用的框架或应用程序,您可以决定解决方案。

但在大多数情况下,您不需要自己实现它,因为它已经在Tomcat、Jetty等中实现了。

参考链接: 非阻塞IO

英文:

NIO's non-blocking mode enables a thread to request reading data from a channel, and only get what is currently available, or nothing at all, if no data is currently available. Rather than remain blocked until data becomes available for reading, the thread can go on with something else.

The same is true for non-blocking writing. A thread can request that some data be written to a channel, but not wait for it to be fully written. The thread can then go on and do something else in the meantime.

What threads spend their idle time on when not blocked in IO calls, is usually performing IO on other channels in the meantime. That is, a single thread can now manage multiple channels of input and output.

So I think you need to rely on the design of the solution by using a design pattern for handling this issue, maybe **Task or Strategy design pattern ** are good candidates and according to the framework or the application you are using you can decide the solution.

But in most cases you don't need to implement it your self as it's already implemented in Tomcat, Jetty etc.

Reference : Non blocking IO

huangapple
  • 本文由 发表于 2020年7月27日 09:37:30
  • 转载请务必保留本文链接:https://go.coder-hub.com/63107553.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定