- PVSM.RU - https://www.pvsm.ru -

В зарубежных блогах было рассмотрено несколько тестов максимального количества одновременных соединений, от классического A Million User Comet Applicaction with Mochiweb/Erlang [1], до совсем недавнего Node.js w/250k concurrent connections [2]. Чего мне хотелось бы добиться в моём тесте — так это повторяемости. Чтобы другие могли без особых проблем повторить и получить свои результаты тестирования. Весь код, который был использован в тестах, выложен на github: сервер [3] и клиент [4], добро пожаловать.
Безусловно, вам понадобится Dedicated Server для проведения подобных тестов. Закажите его у вашего любимого хостера или используйте уже существующий. Я использовал Hetzner EX4 [5] (Core i7-2600 Quad, 16Gb RAM), он достаточно дешевый и мощный.
Я использую Ubuntu 12.04 LTS. Возможно, мой тест можно будет использовать и в других Linux-ах, с небольшими изменениями. Скорее всего такое не прокатит на других OS. В сети достаточно много рассказано про настройку Linux под большое кол-во соединений. Что радует, практически ничего из этого не понадобилось:
#/etc/security/limits.conf
# Увеличиваем лимит дескрипторов файлов (на каждое соединение нужно по одному).
* - nofile 1048576
#/etc/sysctl.conf
# Если используете netfilter/iptables, увеличить лимит нужно и здесь:
net.ipv4.netfilter.ip_conntrack_max = 1048576
Во всех тестах используется Node.js v0.8.3.
Используем стандартный модуль cluster [6] для распределения нагрузки на несколько процессов (по количеству ядер). Отключаем алгоритм Нагла [7].
// Server.js (упрощённый)
var cluster = require('cluster');
var config = {
numWorkers: require('os').cpus().length,
};
cluster.setupMaster({
exec: "worker.js"
});
// Fork workers as needed.
for (var i = 0; i < config.numWorkers; i++)
cluster.fork()
// Worker.js (упрощённый)
var server = require('http').createServer();
var config = {...};
server.on('connection', function(socket) {
socket.setNoDelay(); // Отключаем алгоритм Нагла.
});
var connections = 0;
server.on('request', function(req, res) {
connections++;
// Каждое соединение получает'пинг' каждые 20 сек
// = 50к сообщений в секунду на 1 млн соединений
var pingInterval = setInterval(function() {
res.write('ping');
}, 20*1000);
res.writeHead(200);
res.write("Welcome!");
res.on('close', function() {
connections--;
clearInterval(pingInterval);
pingInterval = undefined;
});
});
server.listen(config.port);
Для удобства наблюдения за происходящим я добавил отслеживание и отображение множества системных параметров в терминале, а также их логгирование. Запустить это чудо интерактивности у себя на сервере можно следующим образом:
git clone git://github.com/ashtuchkin/node-millenium.git
cd node-millenium
# По умолчанию слушаем порт 8888.
node server.js
Получаем картинку похожую на эту:

Тут надо отметить следующее:
Что же это за «отзывчивость» (responsiveness) процесса? Условно, это сколько нужно будет ждать событию перед тем, как его обработает Event Loop Node.js. Этот параметр принципиален в приложениях, рассчитывающих на хоть-какой-нибудь real-time, но его достаточно сложно подсчитать.
Измерять его будем следующим образом:
// Просим Node.js вызывать нас ровно раз в 10 мс и запоминаем время вызовов.
var ticks = [];
setInterval(function() {
ticks.push(Date.now());
}, 10);
// Раз в секунду обрабатываем.
setInterval(function() {
// 1. Вычисляем промежутки между тиками: ticks[i] = ticks[i+1]-ticks[i]
// 2. Затем в полученном массиве ticks вычисляем среднее (tick-avg), 90-й процентиль и максимум (tick-max).
// 3. Выводим в приведенном порядке на экран и в лог.
ticks.length=0;
}, 1000);
Хочу отдельно отметить, что интервал замера 10 мс был выбран как баланс между погрешностью и доп. нагрузкой системы. Если выводится 10/10/10ms — это не значит, что на обработку каждого события Node.js тратит по 10 мс. Это значит, что при сетке измерения 10 мс Event Loop каждый раз свободен и готов обрабатывать сразу любое поступившее событие, что означает, что среднее время обработки событий гораздо ниже. Если же вступает в действие Garbage Collector, или длинные операции, то мы это мгновенно увидим и зафиксируем.
Лог ведется в формате CSV, где раз в секунду отмечаются все агрегатные статистики системы (см. список на гитхабе [8]).
Как это ни странно, обеспечить необходимую нагрузку было сложнее, чем создать сервер. Дело в том, что TCP-соединение уникально определяется четверкой [source ip, source port, dest ip, dest port], таким образом с одной машины на 1 порт сервера можно создать не более 64 тыс одновременных соединений (по количеству source ports). Можно было бы создать 16 сетевых интерфейсов с разными IP, как было описано здесь [9], но это сложно когда машинка стоит у хостера.
В результате, было принято решение использовать Amazon EC2 Micro Instances по 2 цента в час. Было выяснено, что такая машинка в силу ограничений по процессору и, особенно, памяти, стабильно держит около 25 тыс. соединений. Таким образом, 40 запущенных инстансов обеспечат нам 1 млн соединений и будут стоить 0.8$ в час. Вполне приемлемо.
Отдельно нужно сказать, что по умолчанию Amazon не даст вам поднять более чем ~20 инстансов в одном регионе. Можно либо оставить заявку на увеличение этого предела, либо поднимать инстансы в нескольких регионах. Я выбрал последнее.
Сначала я настроил одну машину и скопировал её 40 раз используя механизм создания Custom AMI — это легко сделать через веб-интерфейс. Однако, это оказалось слишком сложно в поддержке и неуправляемо, поэтому я перешел на другой механизм: User Data [10] и Cloud Init [11].
Вкратце, это работает так: используются стандартные образы Ubuntu (в каждом регионе они разные) и при старте инстанса в качестве параметра указывается скрипт. Он исполняется на инстансе сразу после его поднятия. В этом скрипте я устанавливаю на голую систему Node.js, записываю необходимые файлы и запускаю ноду, которая слушает определенный порт. Далее, по этому порту можно узнать статус сервера, а также дать ему команды, например, сколько соединений установить с каким ip адресом. Время от команды до работающего «дрона» — около 2-х минут.
Что приятно — код клиента [12] может редактироваться перед каждым запуском инстансов.
Попробовать сами вы можете с помощью проекта ec2-fleet [13] и следующих команд:
git clone git://github.com/ashtuchkin/ec2-fleet.git
cd ec2-fleet
# Инсталлируем необходимые внешние модули
npm install
# Вставьте свои параметры accessKeyId, accessKeySecret от вашего аккаунта в Amazon.
# https://portal.aws.amazon.com/gp/aws/securityCredentials
# Также, выберите регионы в которые будут запускаться инстансы (не менее трёх)
# Важно! Во всех этих регионах вам нужно будет подредактировать Security Group 'default' и открыть
# TCP порт 8889 для source 0.0.0.0/0 - через него мы будем управлять нашими инстансами.
nano aws-config.json
# Стартуем, например, 10 инстансов равномерно по регионам.
# Все инстансы помечаются специальным тегом. Далее, мы работаем только с ними.
./aws.js start 10
# Смотрим статус в отдельном терминале (похоже на top). Ждем пока все не стартуют.
./aws.js status
# Ставим цель (предполагается, что это именно тот сервер, который мы тестируем)
./aws.js set host <ip>
# Каждый дрон устанавливает по 1000 соединений
./aws.js set n 1000
# Максимальное рекомендуемое значение - 25000 соединений
./aws.js set n 25000
# Рестарт ноды на всех серверах. Рекомендуется делать между тестами.
./aws.js set restart 1
# После тестов удаляем все наши инстансы. Другие - не трогаем.
./aws.js stop all
Ну чтож, начнём тестирование. Здесь и в следующих тестах будем делать 1 млн соединений, 50 тыс сообщений в секунду на всех. Node.js версии v0.8.3. Обработка будет вестись в 8 процессах («воркерах») по кол-ву ядер сервера.
В первом тесте мы будем запускать node.js без дополнительных флагов, в самой что ни на есть стандартной конфигурации. Начинаем первый тест (все картинки кликабельны):
[14]
На всех графиках черной пунктирной линией обозначается кол-во соединений, с максимальным значением в 1 млн. По горизонтали — секунды от начала теста, вертикальные линии отмечают минуты. Графики памяти показывают: Total — общее кол-во занятой памяти (напомню, тестирование проводилось на сервере с общим объемом памяти 16 Gb), Total netto — увеличение Total по сравнению с первой секундой (был введён т.к. на этой машине крутится еще несколько моих проектов, суммарно они занимают ~1.3 Gb), RSS mem, JS Heap Total, JS Heap Used — суммарное значение RSS (ссылка), JS Heap Total, JS Heap Used всех процессов node.js.
Визуально величина серой области обозначает объём памяти, выделенных ядром, желтая область — нативных структур node.js, зелёная — JS Heap.
Как видно, Total netto в пике составляет 10 Gb и стабильно держится. После эксперимента все параметры возвращаются практически в исходные значения кроме нативных структур node.js. Вернёмся к ним ниже.
Загрузка процессора на том же тесте:
[15]
Здесь всё проще — 8 ядер = 800%. Total — общая загрузка, CPU (практически совпадает с Total) — суммарная загрузка процессов node.js, User, Sys — общая загрузка в User mode и ядре соответственно. Линии сглажены скользящим средним по 10 секунд.
Вот этот график, честно говоря, меня разочаровал. Загрузка слишком большая, причём непонятно на что она тратится. Приём соединений проходил нормально, порядка 5-7 тыс. соединений в секунду, видимо, этот сценарий хорошо оптимизирован. Однако, гораздо большую нагрузку создает отсоединение, особенно большими партиями (на графике ок. 3 минут 800% загрузки когда я попытался порвать сразу 400 тыс соединений).
Посмотрим, как ведет себя Event Loop:
[16]
На этом графике показаны средние значения tick-avg по 8 воркерам (к сожалению, tick-max не удалось восстановить из-за бага). Шкала логарифмическая, чтобы отразить большие колебания. Желтой линией отображается скользящее среднее по 20 секундам.
Как видно, в среднем при 1 млн соединений к Event Loop получается получить доступ всего 10 раз в секунду (желтая линия ~100 мс). Это просто никуда не годится. При отключении 400 тыс соединений среднее время обработки события возрастает до 400 мс.
После использования гугла по назначению и проведения нескольких опытов меньшего масштаба, было выяснено, что основную часть нагрузки вызывает Garbage Collector, который пытается довольно часто собирать лишнюю память, используя «тяжёлый» алгоритм Mark&Sweep (у V8 их два — есть ещё «легкий» Scavenge). Ответственность за такое поведение лежит где-то на границе между Node.js и V8, и связана с механизмом Idle Notification. Вкратце — это сигнал для V8, что работа сейчас не выполняется и есть время подчистить мусор, которым Node.js злоупотребляет, особенно если JS HeapTotal > 128Mb.
К счастью, мы можем выключить этот сигнал добавив флаг "--nouse-idle-notification". Посмотрим что нам это даст во втором тесте:
[17]
Во-первых, можно отметить, что потребление памяти увеличилось на 1 Гб (10%) и стабилизировалось где-то через 5 минут после последнего соединения, что, в общем-то неплохо. Также, виден «пилообразный» характер графика. Почему?
Давайте посмотрим график потребления памяти одного воркера в упрощенном тесте:
[18]
Теперь понятно — примерно раз в 5 минут происходит сборка мусора в каждом воркере, формируя «пилу» в графике суммарной памяти.
Смотрим процессор и Event Loop:
[19]
[20]
Ну вот, так гораздо лучше! 1 млн. соединений нагружают от 2 до 3 ядер из 8.
Теперь посмотрим хорошо ли справляется Garbage Collector без IdleNotification в третьем тесте:
[21]
Из этого графика видно во-первых, что желтая область (Node.js native structures) всё таки не утекла, а переиспользуется. Во-вторых, сборка мусора могла бы быть и получше.
Чтож, берём сборку мусора в свои руки. Запускаем node.js с флагом "--expose-gc" и вызываем gc(); раз в минуту в четвертом тесте:
[22]
[23]
[24]
Чтож, неплохо. Память высвобождается достаточно резво, процессор под контролем, но раз в минуту у нас есть всплески tick-avg. Думаю, это хороший компромисс.
Во-первых, необходимо подтверждение результатов независимым тестированием. Я сделал всё, чтобы это было просто. Пожалуйста, если у вас простаивает хорошая машинка, попробуйте сами проделать мой путь — это очень интересно.
Во-вторых, ясно, что этот benchmark далёк от реальности. В качестве интересного реального применения хорошо было бы построить аналог jabber-сервера и потестить его на таких же объемах.
В любом случае, надеюсь, что и текущий фреймворк поможет разработчикам Node.js и V8 в дальнейшей оптимизации.
В-третьих, нужно провести ещё эксперименты для выяснения:
Ну и в-четвертых, shameless plug — если вам интересно работать на Node.js в реальных проектах — напишите мне на ashtuchkin@gmail.com [25], я как раз сейчас ищу людей в команду.
Автор: ashtuchkin
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/node-js/13213
Ссылки в тексте:
[1] A Million User Comet Applicaction with Mochiweb/Erlang: http://www.metabrew.com/article/a-million-user-comet-application-with-mochiweb-part-1
[2] Node.js w/250k concurrent connections: http://blog.caustik.com/2012/04/10/node-js-w250k-concurrent-connections/
[3] сервер: https://github.com/ashtuchkin/node-millenium
[4] клиент: https://github.com/ashtuchkin/ec2-fleet
[5] Hetzner EX4: http://www.hetzner.de/hosting/produkte_rootserver/ex4
[6] cluster: http://nodejs.org/api/cluster.html
[7] алгоритм Нагла: http://en.wikipedia.org/wiki/Nagle%27s_algorithm
[8] гитхабе: https://github.com/ashtuchkin/node-millenium/blob/master/server.js#L293
[9] здесь: http://www.metabrew.com/article/a-million-user-comet-application-with-mochiweb-part-3
[10] User Data: http://alestic.com/2009/06/ec2-user-data-scripts
[11] Cloud Init: https://help.ubuntu.com/community/CloudInit
[12] код клиента: https://github.com/ashtuchkin/ec2-fleet/blob/master/client.js
[13] ec2-fleet: https://github.com/ashtuchkin/ec2-fleet/
[14] Image: http://s3-eu-west-1.amazonaws.com/habr1/log1mem.png
[15] Image: http://s3-eu-west-1.amazonaws.com/habr1/log1cpu.png
[16] Image: http://s3-eu-west-1.amazonaws.com/habr1/log1ticks.png
[17] Image: http://s3-eu-west-1.amazonaws.com/habr1/log2mem.png
[18] Image: http://s3-eu-west-1.amazonaws.com/habr1/logsinglemem.png
[19] Image: http://s3-eu-west-1.amazonaws.com/habr1/log2cpu.png
[20] Image: http://s3-eu-west-1.amazonaws.com/habr1/log2ticks.png
[21] Image: http://s3-eu-west-1.amazonaws.com/habr1/log3mem.png
[22] Image: http://s3-eu-west-1.amazonaws.com/habr1/log4mem.png
[23] Image: http://s3-eu-west-1.amazonaws.com/habr1/log4cpu.png
[24] Image: http://s3-eu-west-1.amazonaws.com/habr1/log4ticks.png
[25] ashtuchkin@gmail.com: mailto:ashtuchkin@gmail.com
Нажмите здесь для печати.