Пользователь
0,0
рейтинг
30 декабря 2015 в 17:08

Разработка → Потоки vs процессы на примере нативного Node.js аддона для нагрузочного тестирования

Чуть меньше года назад я написал заметку о попытке создать инструмент нагрузочного тестирования на Node.js используя встроенные возможности (модули cluster и net). В комментариях справедливо указали на необходимость анализа RPS и сравнении с другими бенчмарками. В результате сравнения я пришел к естественному выводу, что многопроцессовый сервис никогда не сравнится по производительности с многопоточным из-за очень дорогих издержек на обмен данными (позже мы убедимся в этом на примере)

Процессы или потоки?


В первого взгляда существенной разницы нет, и то и то обеспечивает параллельность запросов, разница только в shared memory для потоков и сам процесс создания процессов чуть более дорогой. Но ведь мы можем создать все процессы заранее и просто передавать им задачи. Но теперь нам нужен канал общения. Давайте посмотрим какие имеются способы межпроцессорного взаимодействия:

Сигналы

Быстрый и универсальный (поддерживается почти всеми ОС) метод общения системы и процессов. Проблема в исключительной негибкости, да и не для того они создавались, чтобы слать по ним JSON.

Сокеты

Именно так реализован cluster в Node. Метод process.send вызывает отправку данных другому процессу по TCP. Что значит сокеты? Это значит новый дескриптор на каждый вызов, куча I/O и CPU вхолостую.

Есть еще несколько способов, но все они либо не кроссплатформенны либо также зависят от I/O.
А теперь посмотрим на состояние системы, когда мы создаем 100 процессов:



И 100 потоков:



Очевидно, что теперь CPU занят лишь тем, чтобы поддерживать связь между этими примитивными процессами (каждый должен выполнять по одному запросу, иначе V8 поставит их в синхронный event loop). CPU utilization падает и каждый запрос выполняется дольше (не дай бог еше память кончится и пойдет файл подкачки)

Но ведь нода однопоточна, что делать?




Делать нативный аддон на С++, используя multithreading. Nan, Node-gyp, POSIX threads и в итоге аддон стал похож на ab — на вход поступает concurrency, на выходе — результаты тестирования. Только в отличии от ab мы можем пользоваться всеми возможностями js для анализа результатов:

[ { time: 80,
      body: '<!doctype html><html itemscope=...',
      headers: 'HTTP/1.1 200 OK\r\nDate: Mon, 28 Dec 2015 10:37:35 GMT\r\nConnection: close\r\n\r\n' },
....
]

Поддерживаются дополнительные хедеры, POST payload и поскольку это POSIX, то к сожалению только Linux/Mac.
При желании можно считывать только хедеры, обычно этого достаточно, тогда можно сэкономить
еще немного времени на процессинге.

В итоге по производительности nnb сравнялся с ab, выдавая на разных машинах и сетях до 3000 RPS.

Зачем это нужно?


Есть JMeter, есть Tsung, есть масса других платных и бесплатных бенчмарков, но причина по которой многие из них не приживаются в арсенале разработчиков — перегруженный функционал и в итоге все равно недостаточная гибкость. На основне nnb же можно создать свой инструмент для специфических целей или просто скрипт из 10 строчек, который делает только что вам нужно на одном из самых популярных языков.
К примеру stress, который можно запустить с дефолтовым конфигом и смотреть в лайве что происходит с RPS гугла при росте нагрузки (спойлер: ничего) прямо в браузере и на любой unix машине.



Здесь по оси абсцисс кол-во отправленных запросов, по оси ординат — время отклика сервера в миллисекундах. На втором графике запросы в секунду. В конце видно замедление, это вероятно меня начал резать хостер.

К сожалению, с доступными мне машинами достичь больше 5000 RPS пока не удалось. Обычно все упирается в ограничения сети. При этом CPU и память почти не нагружены. Stress кстати поддерживает и Node.js cluster и мультипоточность через nnb. Можно играться и с тем и тем, предварительно выставив ulimit -u (максимальное количество процессов запущенное пользователем) и ulimit -n (максимальное количество дескрипторов).

Надеюсь, статья была полезной. По-прежнему рад сотрудничеству со всеми, кому интересна эта тема и, конечно, с наступающим!
Роман @yarax
карма
9,0
рейтинг 0,0
Пользователь
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (12)

  • +4
    Есть еще несколько способов, но все они либо не кроссплатформенны либо также зависят от I/O.

    Shared memory вполне себе кросс-платформенный и дает нулевые издержки по ресурсам. Давно планирую статью на хабр о переходе с Redis на Shared memory с STL-контейнерами, с графиками и впечатлениями…
  • 0
    что-то оно не работает =( с локальным сервером пишет 30 секунд на реквест, удаленным — 60
  • 0
    Довольно поздно прочитал Вашу статью.
    Хотелось бы уточнить, пробовали ли что-нибудь наподобие https://github.com/plaidev/minimum-rpc?
    Нашел модуль с очень компактный код. Там поднимается WebSocket-сервер на основе socket.io, ну, и, соответственно, используется WebSocket-клиент.
    Хотелось бы, попросить Вас провести бенчмарк того же minimum-rpc с условиями бенчмарков, которые вы описывали в статье.
    • 0
      Вы имеете в виду сравнить скорость работы HTTP и WebSockets? Сейчас можно тестировать только HTTP эндпоинты, но сделать TCP WebSocket коннект несложно, просто непонятно зачем.
      • 0
        Дело в том, что сам по себе socket.io обещал давать неплохие результаты при использовании тысяч различных постоянных соединений.
        В основном, речь идет о клиентах как браузеров. Но, как я понимаю, ваша проблема проявлялась в виде блокировок event-loop в процессе сервера.
        Ну, и не даром в minimum-rpc используют socket.io вместо, например, стандартных http или process.send().

        Интересно было бы посмотреть на сравнительные результаты.

        Возможно, также проблема решения со 100 процессами и process.send() могла заключаться в том, что каждый процесс выполнял слишком мало задач и сразу завершался. Из статьи неизвестно, как организован пулл процессов. Вы запускали 100 процессов и потом раздавали им задачи, или Вы организовывали пулл, который запускает 100 одновременных процессов, и каждый процесс выполнял одну или несколько задач и завершался, а на месте завершенного процесса пуллом запускался новый?

        По моему скромному мнению, второй способ предпочтительнее, более защищен от утечек, защищен от различных необработанных исключений. Но тут надо понимать, что каждый новый процесс кушает при запуске довольно много CPU-time, и нужно соблюдать пропорцию, хотя бы:
        Runtime_CPU_time * Proc_count < Execute_CPU_time, где:
        • Runtime_CPU_time — это количество затраченного CPU-time для запуска процесса;
        • Execute_CPU_time — это сумма затраченного CPU-time для выполнения всех задач порученных одному процессу;
        • Proc_count — количество одновременно запущенных процессов.

        Если проблема 100 процессов заключалась в I/O, то тут необходимы испытания на разных версиях ядер ОС. Пробовали ли Вы запускать тесты на FreeBSD? Сомневаюсь, что проблема производительности process.send() упирается в I/O. Всегда есть вариант разгрузить количество (частоту) использования process.send() и объем информации, передаваемой через process.send(), в пользу самостоятельной загрузки данных процессом и последующим самостоятельным сохранением результатов.

        Сколько раз сам использовал process.send(), с Вашими трудностями я не сталкивался. Правда, в последний раз это было давно, так я использовал для запуска тех скриптов NodeJS версий 0.6.x (если мне не изменяет память).

        Как-то, в свое время, мне необходимо было написать парсер VK для расчетов различных статистических данных. На самом деле, действие происходило чуть позднее новости о том, что Яндекс стал индексировать VK. Необходимо было использовать максимум доступных ресурсов для обработки максимального объема информации за ограниченное время. На тот момент, располагали тремя арендованными в ЦОД серверами с различными характеристиками.

        Я использовал комбинированный подход, то есть, master-процесс запускал несколько worker-процессов, каждый из которых выполнял задачи в несколько потоков. Причем, для использования максимума свободных ресурсов, master-процесс в процессе выполнения руководил количеством одновременно запущенных процессов и количеством потоков в каждом worker-процессе. Master-процесс различал несколько состояний ОС: если весь chain процессов превышал общий Memory_limit, master-процесс уменьшал количество процессов и увеличивал количество потоков; если worker-процессы превышали CPU_limit, он увеличивал количество процессов и уменьшал количество потоков в каждом процессе. Также master-процесс останавливал перегруженные и idle процессы.

        Для реализации использовал простые инструменты:
        • для запуска worker-процессов использовал child_process.spawn() (позднее появился Cluster);
        • для передачи данных между процессами использовал process.send();
        • для выполнения задач в несколько потоков использовал async.queue() модуля async.

        Возможно, последние версии NodeJS перегружены функционалом. В те времена V8 умел использовать только 32-битную архитектуру ОС. Опять же, если мне не изменяет память, свеже-запущенный процесс node занимал 1.8Мб оперативной памяти, а процесс с запущенным парсером html-страниц занимал памяти около 15Мб и точно не более 32Мб (был индивидуальный heap-лимит, превышая который, процесс считался зависшим и перезапускался).
        • 0
          Спасибо за подробный комментарий. У вас фундаментальное заблуждение. У ноды только один рабочий поток (async.queue использует обычные js колбеки, которые не что иное как очередь функций и event loop) поэтому как бы не был организован сервер, если он не спаунит процессы, то запросы будут идти через event loop.
          Поэтому разработчики socket.io не могут ничего утверждать о производительности, они лишь сделали обертку для WebSockets на клиенте и TCP обертку на сервере.
          По поводу process.send как раз и идет речь в статье, нода использует TCP для коммуникации, а значит I/O а значит падение производительности.
          Но если держать процессы запущенными и коммуникация между ними не принципиальна, то производительность процессов и потоков должна быть по идее сравнимой, если у ОС есть достаточно ресурсов на все открытые процессы.
          • 0
            У ноды только один рабочий поток

            Об этом я в курсе. В моем примере с VK, парсинг страницы, разумеется происходит гораздо быстрее, чем ее закачка. Во время закачки CPU простаивает. При этом процесс остается запущенным, данные для работы уже загружены в память. Было бы глупо скачивать VK в 1 поток/процесс. В разное время ситуация со скоростью доступа к VK меняется. Необходимо контролировать несколько параметров, вызывать события контроля чисел потоков/процессов при пробивании min-max-лимитов.

            Я не утверждаю, что socket.io будет магически-быстрее или менее затратным по ресурсам. Можно же просто следить за пулами процессов, чтобы они не перегружали друг друга и при этом работали на максимуме своих возможностей?

            Пишу комментарии для того, чтобы выяснить сколько % профита принесло Вам ваше решение.
            • 0
              Уже который комментарий пишу, и все забываю сказать. Через socket.io есть возможность передавать данные в формате BSON, который в отличии от синхронного JSON, имеет асинхронный по-блоковый алгоритм генерации и парсинга. Другими словами, умеет ReadableStream и WritableStream, прямо как SAX у XML.
              • 0
                По поводу тестирования socket.io есть интересная статья bocoup.com/weblog/node-stress-test-analysis
                • 0
                  В целом, вы правы, но мне все же интересно вычислить реальные цифры. Статье больше трех лет, по меркам nodejs — это целое поколение в эволюции скриптов node :-) Думаю, когда встанет в очередной раз этот вопрос, запилю бэнчмарки самостоятельно, может даже, напишу об этом статью на Хабр.
              • 0
                Но опять же, socket.io значит нода, нода значит один поток и много процессов, кол-во процессов ограничено ресурсами, поэтому в статье юзается одновременный запуск сразу с кучи EC2 инстансов
            • 0
              В моем случае профит составил десятки раз. На одной и той же машине решение с процессами достигало максимум 300 RPS, с потоками — 2000-5000. Я опять же говорю, что все зависит от задачи и необходимости коммуникации между инстансами, но так или иначе странно ожидать от потоков меньшей производительности.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.