Что приводит к снижению производительности?

Я использую структуру Disruptor для выполнения быстрой коррекции ошибок Рида-Соломона по некоторым данным. Это моя настройка:

          RS Decoder 1
        /             \
Producer-     ...     - Consumer
        \             /
          RS Decoder 8 
  • Производитель считывает блоки из 2064 байт с диска в буфер байта.
  • Пользователи RS-декодера 8 параллельно выполняют коррекцию ошибок Рида-Соломона.
  • Пользователь пишет файлы на диск.

В терминах DSL дефрагментатора настройка выглядит следующим образом:

        RsFrameEventHandler[] rsWorkers = new RsFrameEventHandler[numRsWorkers];
        for (int i = 0; i < numRsWorkers; i++) {
            rsWorkers[i] = new RsFrameEventHandler(numRsWorkers, i);
        }
        disruptor.handleEventsWith(rsWorkers)
                .then(writerHandler);

Когда у меня нет потребителя вывода диска (нет .then(writerHandler)), измеренная пропускная способность составляет 80 М/с, как только я добавляю потребителя, даже если он пишет в /dev/null или doesn ' t даже писать, но объявляется как зависимый потребитель, производительность падает до 50-65 М/с.

Я профилировал его с помощью Oracle Mission Control, и это показывает график использования ЦП:

Без дополнительного пользователя: Without an additional consumer

С дополнительным потребителем: With additional consumer

Что это за серая часть графика и откуда она? Я полагаю, что это связано с синхронизацией потоков, но я не могу найти никакой другой статистики в Mission Control, которая указывала бы на такую ​​задержку или противоречие.

Ответ 1

Ваша гипотеза верна, это проблема синхронизации потоков.

Из Документация API для EventHandlerGroup<T>.then (Emphasis mine)

Настройте пакетные обработчики для использования событий из кольцевого буфера. Эти обработчики будут обрабатывать события только после того, как каждый EventProcessor в этой группе обработал событие.

Этот метод обычно используется как часть цепочки. Например, если обработчик A должен обрабатывать события перед обработчиком B:

Это должно обязательно уменьшать пропускную способность. Подумайте об этом как воронка:

Event Funnel

Потребитель должен ждать для завершения каждого EventProcessor, прежде чем он сможет продолжить горлышко бутылки.

Ответ 2

Здесь я вижу две возможности, основываясь на том, что вы показали. На вас может повлиять один или оба, я бы рекомендовал тестировать оба. 1) Узкое место для обработки ввода-вывода. 2) Конфликт по написанию нескольких потоков в буфер.

Обработка ввода-вывода

Из приведенных данных вы заявили, что как только вы включите компонент ввода-вывода, ваша пропускная способность уменьшается, а время ядра увеличивается. Это может быть довольно легко, когда время ожидания ввода-вывода в то время как ваш потребительский поток пишет. Контекстный коммутатор для выполнения вызова write() значительно дороже, чем ничего не делать. Ваш Decoder теперь ограничен максимальной скоростью потребителя. Чтобы проверить эту гипотезу, вы можете удалить вызов write(). Другими словами, откройте выходной файл, подготовьте строку для вывода и просто не вызовите вызов записи.

Предложения

  • Попробуйте удалить вызов write() в Потребителе, посмотрите, уменьшает ли он время ядра.
  • Вы записываете в один плоский файл последовательно - если нет, попробуйте это
  • Используете ли вы интеллектуальную группировку (то есть: буферизацию до endOfBatch), а затем пишите в одну партию), чтобы обеспечить максимально возможное объединение IO?

Конфликт нескольких авторов

Основываясь на вашем описании, я подозреваю, что ваш Decoder читает из разрушителя, а затем записывается обратно в тот же самый буфер. Это вызовет проблемы с несколькими авторами, а также конкуренция с процессорами, записываемыми в память. Одна вещь, которую я хотел бы предложить, состоит в том, чтобы иметь два кольца прерывания:

  • Producer записывается в # 1
  • Decoder читает из # 1, выполняет RS-декодирование и записывает результат на # 2
  • Consumer читается С# 2 и записывается на диск

Предполагая, что ваши RB достаточно велики, это должно привести к хорошей чистой ходьбе через память.

Ключ здесь состоит в том, что потоки Decoder (которые могут работать на другом ядре) записываются в ту же память, которая принадлежала только Producer. С помощью всего лишь 2 ядер, вы, вероятно, увидите улучшенную пропускную способность, если только скорость диска не является узким местом.

У меня есть статья в блоге, в которой более подробно описывается, как достичь этого, включая пример кода. http://fasterjava.blogspot.com.au/2013/04/disruptor-example-udp-echo-service-with.html

Другие мысли

  • Было бы также полезно узнать, что вы используете WaitStrategy, сколько физических процессоров находится в машине и т.д.
  • Вы должны иметь возможность значительно снизить загрузку процессора, перейдя к другому WaitStrategy, учитывая, что ваша самая большая задержка будет записываться в IO.
  • Предполагая, что вы используете достаточно новое оборудование, вы должны иметь возможность насыщать устройства ввода-вывода только с этой настройкой.
  • Вам также необходимо убедиться, что файлы находятся на разных физических устройствах для достижения разумной производительности.