Подтвердить что ты не робот

Медленная передача в Jetty с кодированием с коротким переносом при определенном размере буфера

Я изучаю проблему производительности с Jetty 6.1.26. Кажется, что Jetty использует Transfer-Encoding: chunked, и в зависимости от используемого размера буфера это может быть очень медленным при локальном переносе.

Я создал небольшое тестовое приложение Jetty с одним сервлетом, который демонстрирует проблему.

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.mortbay.jetty.Server;
import org.mortbay.jetty.nio.SelectChannelConnector;
import org.mortbay.jetty.servlet.Context;

public class TestServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        final int bufferSize = 65536;
        resp.setBufferSize(bufferSize);
        OutputStream outStream = resp.getOutputStream();

        FileInputStream stream = null;
        try {
            stream = new FileInputStream(new File("test.data"));
            int bytesRead;
            byte[] buffer = new byte[bufferSize];
            while( (bytesRead = stream.read(buffer, 0, bufferSize)) > 0 ) {
                outStream.write(buffer, 0, bytesRead);
                outStream.flush();
            }
        } finally   {
            if( stream != null )
                stream.close();
            outStream.close();
        }
    }

    public static void main(String[] args) throws Exception {
        Server server = new Server();
        SelectChannelConnector ret = new SelectChannelConnector();
        ret.setLowResourceMaxIdleTime(10000);
        ret.setAcceptQueueSize(128);
        ret.setResolveNames(false);
        ret.setUseDirectBuffers(false);
        ret.setHost("0.0.0.0");
        ret.setPort(8080);
        server.addConnector(ret);
        Context context = new Context();
        context.setDisplayName("WebAppsContext");
        context.setContextPath("/");
        server.addHandler(context);
        context.addServlet(TestServlet.class, "/test");
        server.start();
    }

}

В моем эксперименте я использую тестовый файл 128 Мбайт, который сервлет возвращает клиенту, который подключается с помощью localhost. Загрузка этих данных с помощью простого тестового клиента, написанного на Java (с использованием URLConnection), занимает 3,8 секунды, что очень медленно (да, это 33 МБ/с, что не звучит медленно, за исключением того, что это чисто локально, а входной файл был кеширован, он должен быть намного быстрее).

Теперь, когда это становится странным. Если я загружу данные с помощью wget, который является клиентом HTTP/1.0 и, следовательно, не поддерживает кодирование с передачей пакетов, это займет всего 0,1 секунды. Это намного лучше.

Теперь, когда я меняю bufferSize на 4096, клиент Java занимает 0,3 секунды.

Если я полностью удаляю вызов resp.setBufferSize (который, как представляется, использует размер блока 24 КБ), клиент Java теперь занимает 7,1 секунды, а wget - в равной степени медленный!

Обратите внимание, что я ни в коем случае не специалист по Jetty. Я столкнулся с этой проблемой при диагностике проблемы производительности в Hadoop 0.20.203.0 с уменьшением перетасовки задач, которая передает файлы с помощью Jetty таким же образом, как и уменьшенный примерный код, с размером буфера 64 КБ.

Проблема воспроизводится как на наших серверах Linux (Debian), так и на моей машине с Windows и с Java 1.6 и 1.7, поэтому она зависит только от Jetty.

Кто-нибудь знает, что может быть причиной этого, и если что-то я могу с этим поделать?

4b9b3361

Ответ 1

Я считаю, что сам нашел ответ, просмотрев исходный код Jetty. Это на самом деле сложное взаимодействие размера буфера ответа, размер буфера, переданного в outStream.write, и вызывается ли outStream.flush (в некоторых ситуациях). Проблема связана с тем, как Jetty использует свой внутренний буфер ответа и как данные, которые вы пишете на выходе, копируются в этот буфер, а также когда и как этот буфер сбрасывается.

Если размер буфера, используемого с outStream.write, равен буферу ответа (я думаю, что работает несколько), или меньше, и outStream.flush, тогда производительность будет прекрасной. Каждый вызов write затем сбрасывается прямо на выход, что отлично. Однако, когда буфер записи больше и не является кратным буферу ответа, это, как представляется, вызывает некоторую странность в том, как обрабатываются флеши, что приводит к дополнительным флешам, что приводит к плохой производительности.

В случае кодирования с чередующейся передачей в кабеле имеется дополнительный излом. Для всех, кроме первого блока, Jetty резервирует 12 байтов буфера ответа, чтобы содержать размер блока. Это означает, что в моем исходном примере с буфером для записи и ответа на 64 Кбайта фактический объем данных, которые вписываются в буфер ответа, составлял всего 65524 байта, так что части буфера записи разливались в несколько флешей. Если посмотреть на захваченную сетевую трассировку этого сценария, я вижу, что первый фрагмент составляет 64 КБ, но все последующие фрагменты составляют 65524 байта. В этом случае outStream.flush не имеет значения.

При использовании буфера 4 КБ я видел быстрые скорости только при вызове outStream.flush. Оказывается, что resp.setBufferSize будет только увеличивать размер буфера, а поскольку размер по умолчанию составляет 24 КБ, resp.setBufferSize(4096) - это не-op. Тем не менее, я теперь писал 4 Кбайта данных, которые вписываются в буфер 24 КБ, даже с зарезервированными 12 байтами, и затем сбрасываются как кусок 4 КБ по вызову outStream.flush. Однако, когда вызов flush будет удален, он будет заполнен буфером, снова с 12 байтами, просыпающимися в следующий фрагмент, потому что 24 кратно 4.

В заключение

Кажется, что для обеспечения хорошей производительности с Jetty вам необходимо:

  • При вызове setContentLength (без кодировки передачи по каналам) и используйте буфер для write того же размера, что и размер буфера ответа.
  • При использовании закодированного кодирования передачи используйте буфер записи, размер которого не меньше, чем на 12 байтов, чем размер буфера ответа, и вызовите flush после каждой записи.

Обратите внимание, что производительность "медленного" сценария по-прежнему такова, что вы, скорее всего, увидите только разницу на локальном хосте или очень быстрое (1 Гбит/с или больше) сетевое подключение.

Я думаю, что я должен опубликовать отчеты о проблемах с Hadoop и/или Jetty для этого.

Ответ 2

Да, Jetty по умолчанию будет Transfer-Encoding: Chunked, если размер ответа не может быть определен.

Если вы знаете размер ответа, то каким он будет. Вы должны вызвать resp.setContentLength(135*1000*1000*1000); в этом случае вместо

resp.setBufferSize();

Фактически установка resp.setBufferSize несущественна.

Прежде чем открыть OutputStream, который находится перед этой строкой: OutputStream outStream = resp.getOutputStream(); вам нужно позвонить resp.setContentLength(135*1000*1000*1000);

(строка выше)

Дайте ему вращение. посмотрите, работает ли это. Это мои догадки из теории.

Ответ 3

Это чисто спекуляция, но я предполагаю, что это какая-то проблема сборщика мусора. Увеличивает производительность Java-клиента, когда вы запускаете JVM с большим количеством кучи, например...   java -Xmx 128m

Я не помню JVM-переключатель, чтобы включить GC-протоколирование, но посмотрите на это, и посмотрите, будет ли GC ударом, как только вы попадаете в ваш doGet.

Мои 2 цента.