Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OutOfDirectMemoryError for large uploads using HttpPostMultipartRequestDecoder #10973

Closed
danielflower opened this issue Jan 28, 2021 · 7 comments · Fixed by #10989
Closed

OutOfDirectMemoryError for large uploads using HttpPostMultipartRequestDecoder #10973

danielflower opened this issue Jan 28, 2021 · 7 comments · Fixed by #10989
Milestone

Comments

@danielflower
Copy link

Expected behavior

With a HttpDataFactory that has useDisk=true, I thought files of any size could potentially be uploaded.

Actual behavior

Example error from below unit test:

io.netty.util.internal.OutOfDirectMemoryError: failed to allocate 4194304 byte(s) of direct memory (used: 62914560, max: 64487424)
	at io.netty.util.internal.PlatformDependent.incrementMemoryCounter(PlatformDependent.java:775)
	at io.netty.util.internal.PlatformDependent.reallocateDirectNoCleaner(PlatformDependent.java:748)
	at io.netty.buffer.UnpooledUnsafeNoCleanerDirectByteBuf.reallocateDirect(UnpooledUnsafeNoCleanerDirectByteBuf.java:34)
	at io.netty.buffer.UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeNoCleanerDirectByteBuf.reallocateDirect(UnpooledByteBufAllocator.java:194)
	at io.netty.buffer.UnpooledUnsafeNoCleanerDirectByteBuf.capacity(UnpooledUnsafeNoCleanerDirectByteBuf.java:52)
	at io.netty.buffer.AbstractByteBuf.ensureWritable0(AbstractByteBuf.java:307)
	at io.netty.buffer.AbstractByteBuf.ensureWritable(AbstractByteBuf.java:282)
	at io.netty.buffer.AbstractByteBuf.writeBytes(AbstractByteBuf.java:1105)
	at io.netty.buffer.AbstractByteBuf.writeBytes(AbstractByteBuf.java:1098)
	at io.netty.buffer.AbstractByteBuf.writeBytes(AbstractByteBuf.java:1089)
	at io.netty.handler.codec.http.multipart.HttpPostMultipartRequestDecoder.offer(HttpPostMultipartRequestDecoder.java:351)
	at NettyUploadTest.itCanProcessLargeFiles(NettyUploadTest.java:46)
// snip

Steps to reproduce

Set -Xmx64m and run below unit test.

Minimal yet complete reproducer code (or URL to code)

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.handler.codec.http.*;
import io.netty.handler.codec.http.multipart.DefaultHttpDataFactory;
import io.netty.handler.codec.http.multipart.FileUpload;
import io.netty.handler.codec.http.multipart.HttpDataFactory;
import io.netty.handler.codec.http.multipart.HttpPostMultipartRequestDecoder;
import org.junit.Test;

import java.nio.charset.StandardCharsets;
import java.util.Arrays;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;

public class NettyUploadTest {

    @Test
    public void itCanProcessLargeFiles() throws Exception {

        int fileSize = 100_000_000; // set Xmx to a number lower than this and it crashes
        int bytesPerChunk = 1_000_000;

        String prefix = "--861fbeab-cd20-470c-9609-d40a0f704466\n" +
            "Content-Disposition: form-data; name=\"image\"; filename=\"guangzhou.jpeg\"\n" +
            "Content-Type: image/jpeg\n" +
            "Content-Length: " + fileSize + "\n" +
            "\n";

        String suffix = "\n" +
            "--861fbeab-cd20-470c-9609-d40a0f704466--\n";

        HttpRequest request = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, "/upload");
        request.headers().set("content-type", "multipart/form-data; boundary=861fbeab-cd20-470c-9609-d40a0f704466");
        request.headers().set("content-length", prefix.length() + fileSize + suffix.length());

        HttpDataFactory factory = new DefaultHttpDataFactory(true);
        HttpPostMultipartRequestDecoder decoder = new HttpPostMultipartRequestDecoder(factory, request);
        decoder.offer(new DefaultHttpContent(Unpooled.wrappedBuffer(prefix.getBytes(StandardCharsets.UTF_8))));

        byte[] body = new byte[bytesPerChunk];
        Arrays.fill(body, (byte)1);
        for (int i = 0; i < fileSize / bytesPerChunk; i++) {
            ByteBuf content = Unpooled.wrappedBuffer(body, 0, bytesPerChunk);
            decoder.offer(new DefaultHttpContent(content)); // **OutOfMemory here**
            content.release();
        }

        decoder.offer(new DefaultHttpContent(Unpooled.wrappedBuffer(suffix.getBytes(StandardCharsets.UTF_8))));
        decoder.offer(new DefaultLastHttpContent());
        FileUpload data = (FileUpload) decoder.getBodyHttpDatas().get(0);
        assertThat((int)data.length(), is(fileSize));
        assertThat(data.get().length, is(fileSize));

        factory.cleanAllHttpData();

    }

}

Netty version

Tested on 4.1.56 and 4.1.58.

JVM version (e.g. java -version)

jdk1.8.0_162 and 12

OS version (e.g. uname -a)

Windows 10

@franz1981
Copy link
Contributor

@fredericBregier It could be related to #10623 given that right now file isn't written until a delimiter is found?

@normanmaurer
Copy link
Member

I think so too... I think we should never the commit in question for now as while it fixes some "perf issues" when running in with paranoid leak detection I don't think it has any other benefits in real world use-cases. @fredericBregier WDYT ?

@fredericBregier
Copy link
Member

Hi, the issue is partially related but yet an issue.

  • the file is totally in the buffer while reading: previously it could be in memory but by chunk
  • once the file is over, if the "disk" based HttpData is used, the buffer is written to a temprary file, therefore leaving the memory free
  • Note that another bug was fixed in the same time since before buffers were free (discardReadBytes) wrongly

I agree that this should be changed to adapt the solution with the new way to find the delimiter. Perhaps this?
Currently:

If it is written (in Disk mode or Mixed mode and if size is greater than limit), the buffer might be cleared (free), so one could try to change this such as:

Not sure it will work or easy to implement, but I think the idea is there...

@fredericBregier
Copy link
Member

Hi all, I propose a fix for this (adding a test inspired from the one given, testing both in Memory and on Disk behaviors)

normanmaurer added a commit that referenced this issue Feb 3, 2021
…oder to e5951d4

Motivation:

The changes introduced in 1c23040 did cause various issues while the fix itself is not considered critical. For now it is considered the best to just rollback and investigate more.

Modifications:

Revert changes done in 1c23040 (and later) for
the post decoders.

Result:

Fixes #10973
fredericBregier added a commit to fredericBregier/netty that referenced this issue Feb 3, 2021
Motivation:

While improvements were made on finding delimiters, when HttpDatas do not fit in memory
while using Mixed or Disk mode in HttpDataFactory, the memory was exhausted.

Modifications:

Change the way `loadDataMultipart` tries to add contents. Instead of waiting to found the delimiter,
it will add the current buffer and, if possible, will try to reuse the current allocated buffer.
As the chunk is a retained buffer from the original one, if the `refCnt()` is 1 after the
`addContent`, then it can be "virtually" released (changing reader and writer indexes).

Note: if the HttpDataFactory is in Memory only, this will not prevent OOME since the full
data will be kept in memory.

Result:

Now the memory cunsumption in Mixed mode or Disk mode is restricted to the minimum.

A test is added to check both in Memory and on Disk behaviors.
normanmaurer added a commit that referenced this issue Feb 3, 2021
…oder to e5951d4 (#10989)

Motivation:

The changes introduced in 1c23040 did cause various issues while the fix itself is not considered critical. For now it is considered the best to just rollback and investigate more.

Modifications:

- Revert changes done in 1c23040 (and later) for
the post decoders.
- Ensure we give memory back to the system as soon as possible in a safe manner

Result:

Fixes #10973
@normanmaurer normanmaurer added this to the 4.1.59.Final milestone Feb 3, 2021
normanmaurer added a commit that referenced this issue Feb 3, 2021
…oder to e5951d4 (#10989)

Motivation:

The changes introduced in 1c23040 did cause various issues while the fix itself is not considered critical. For now it is considered the best to just rollback and investigate more.

Modifications:

- Revert changes done in 1c23040 (and later) for
the post decoders.
- Ensure we give memory back to the system as soon as possible in a safe manner

Result:

Fixes #10973
@danielflower
Copy link
Author

Thanks to both of you. The new version is working well now.

The performance degradation with paranoid detection is very noticeable (e.g. a test with a lot of data moving was taking 1 second in 4.1.58 takes 30 seconds in 4.1.59). For a while I thought there was a performance issue in the new version before I remembered what Frederic said about leak detection performance.

So if anyone else is experiencing this and wondering what is going on, try disabling PARANOID for the slow tests.

@fredericBregier
Copy link
Member

@danielflower or maybe wait for #11001 which fixes also this PARANOID issue, and improves without this level also performances (about 4 times)... ;-)

@chrisvest
Copy link
Contributor

The #11001 PR is merged now.

raidyue pushed a commit to raidyue/netty that referenced this issue Jul 8, 2022
…oder to e5951d4 (netty#10989)

Motivation:

The changes introduced in 1c23040 did cause various issues while the fix itself is not considered critical. For now it is considered the best to just rollback and investigate more.

Modifications:

- Revert changes done in 1c23040 (and later) for
the post decoders.
- Ensure we give memory back to the system as soon as possible in a safe manner

Result:

Fixes netty#10973
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
5 participants