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

17

Я расследую проблему с производительностью 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.

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

    
задан Sven 27.01.2012 в 10:34
источник
  • +1. Я также наблюдал это. Но на самом деле не получилось хорошего решения. –  Thomas Jungblut 28.01.2012 в 13:49

3 ответа

13

Я думаю, что нашел ответ сам, просматривая исходный код 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) не используется. Тем не менее, я теперь писал части данных по 4 КБ, которые помещаются в буфер размером 24 КБ даже с зарезервированными 12 байтами, а затем очищаются как блок размером 4 КБ с помощью вызова outStream.flush . Однако, когда вызов flush удаляется, он позволяет буферу заполниться, опять же, с 12 байтами, разливающимися в следующий фрагмент, потому что 24 кратно 4.

В заключение

Похоже, что для получения хорошей производительности с Jetty вы должны либо:

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

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

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

    
ответ дан Sven 31.01.2012 в 05:36
  • Я сомневаюсь, что вы найдете кого-нибудь в команде причала, который особенно реагирует на сообщение об ошибке на Jetty 6. Но если такая же проблема существует в Jetty 7 или 8, то отчет об ошибке будет высоко оценен. –  Tim 03.02.2012 в 10:34
1

Да, 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);

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

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

    
ответ дан manocha_ak 30.01.2012 в 09:15
  • Спасибо за ваш ответ. resp.setContentLength без resp.setBufferSize все еще медленно. Однако с resp.setContentLength оба размера буфера, которые я пробовал (64 КБ и 4 КБ), теперь бывают быстрыми. Также см. Обновление к вопросу. –  Sven 31.01.2012 в 04:39
  • На самом деле, похоже, размер буфера по умолчанию является быстрым, если и только если размер буфера, переданного в outStream.write, меньше 24 КБ (размер буфера ответа по умолчанию). –  Sven 31.01.2012 в 05:18
  • забыл, что я прокомментировал флеш выше в случае отсутствия chunking (настройка длины содержимого) –  manocha_ak 31.01.2012 в 06:42
  • как я могу заставить Jetty отказаться от использования Transfer-Encoding: по умолчанию по умолчанию. Некоторые из них являются древними и требуют ответа заголовка Content-Length. –  4ntoine 10.07.2015 в 14:11
0

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

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

Мои 2 цента.

    
ответ дан Bob Kuhar 29.01.2012 в 21:45
  • При второй мысли ... 128 м кажется по умолчанию. Поэтому мое предложение не поможет. Я останусь в забвении. О человечество. –  Bob Kuhar 29.01.2012 в 21:46
  • Эти ребята говорят, что размер управляющего буфера по своему усмотрению может и не быть такой замечательной идеей: stackoverflow.com/questions/4638974/... –  Bob Kuhar 30.01.2012 в 07:14
  • Однако использование размера буфера по умолчанию в HttpServletResponse фактически дало мне худшую производительность. –  Sven 30.01.2012 в 09:04