Пользователь
0,0
рейтинг
15 августа 2012 в 18:23

Разработка → Миллион одновременных соединений на Node.js



TL;DR:


  • Node.js v0.8 позволяет обрабатывать 1 млн одновременных HTTP Comet соединений на Intel Core i7 Quad/16 Gb RAM практически без дополнительных настроек.
  • На 1 соединение тратится чуть больше 10 Kb памяти (4.1 Kb Javascript Heap + 2.2 Kb Node.js Native + 3.8 Kb Kernel)..
  • V8 Garbage Collector не рассчитан на управление > ~500Mb памяти. При превышении нужно переходить на альтернативный режим сборки мусора, иначе «отзывчивость» сервера сильно уменьшается.
  • Подобный опыт можно (и нужно!) без особых затрат повторить самому (см. под катом).


Введение


В зарубежных блогах было рассмотрено несколько тестов максимального количества одновременных соединений, от классического A Million User Comet Applicaction with Mochiweb/Erlang, до совсем недавнего Node.js w/250k concurrent connections. Чего мне хотелось бы добиться в моём тесте — так это повторяемости. Чтобы другие могли без особых проблем повторить и получить свои результаты тестирования. Весь код, который был использован в тестах, выложен на github: сервер и клиент, добро пожаловать.

Сервер


1. Железо


Безусловно, вам понадобится Dedicated Server для проведения подобных тестов. Закажите его у вашего любимого хостера или используйте уже существующий. Я использовал Hetzner EX4 (Core i7-2600 Quad, 16Gb RAM), он достаточно дешевый и мощный.

2. Операционная система


Я использую 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


3. Код для Node.js


Во всех тестах используется Node.js v0.8.3.
Используем стандартный модуль cluster для распределения нагрузки на несколько процессов (по количеству ядер). Отключаем алгоритм Нагла.

// 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


Получаем картинку похожую на эту:


Тут надо отметить следующее:
  1. Проценты загрузки процессора везде выдаются с учетом кол-ва ядер. Т.е. на 4-ядерном процессоре максимальная загрузка — 400%.
  2. В 'used' память не входит buf+cache. Т.е. должно быть used+free+(buf+cache) = всего памяти на сервере.
  3. Для каждого процесса выводится его pid, доля загрузки процессора (user+sys), RSS-память, кол-во соединений, heapUsed/heapTotal по показаниям os.memoryUsage(), три измерения «отзывчивости» процесса (ticks, см. ниже).

Что же это за «отзывчивость» (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, где раз в секунду отмечаются все агрегатные статистики системы (см. список на гитхабе).

Клиенты


Как это ни странно, обеспечить необходимую нагрузку было сложнее, чем создать сервер. Дело в том, что TCP-соединение уникально определяется четверкой [source ip, source port, dest ip, dest port], таким образом с одной машины на 1 порт сервера можно создать не более 64 тыс одновременных соединений (по количеству source ports). Можно было бы создать 16 сетевых интерфейсов с разными IP, как было описано здесь, но это сложно когда машинка стоит у хостера.

В результате, было принято решение использовать Amazon EC2 Micro Instances по 2 цента в час. Было выяснено, что такая машинка в силу ограничений по процессору и, особенно, памяти, стабильно держит около 25 тыс. соединений. Таким образом, 40 запущенных инстансов обеспечат нам 1 млн соединений и будут стоить 0.8$ в час. Вполне приемлемо.

Отдельно нужно сказать, что по умолчанию Amazon не даст вам поднять более чем ~20 инстансов в одном регионе. Можно либо оставить заявку на увеличение этого предела, либо поднимать инстансы в нескольких регионах. Я выбрал последнее.

Сначала я настроил одну машину и скопировал её 40 раз используя механизм создания Custom AMI — это легко сделать через веб-интерфейс. Однако, это оказалось слишком сложно в поддержке и неуправляемо, поэтому я перешел на другой механизм: User Data и Cloud Init.

Вкратце, это работает так: используются стандартные образы Ubuntu (в каждом регионе они разные) и при старте инстанса в качестве параметра указывается скрипт. Он исполняется на инстансе сразу после его поднятия. В этом скрипте я устанавливаю на голую систему Node.js, записываю необходимые файлы и запускаю ноду, которая слушает определенный порт. Далее, по этому порту можно узнать статус сервера, а также дать ему команды, например, сколько соединений установить с каким ip адресом. Время от команды до работающего «дрона» — около 2-х минут.

Что приятно — код клиента может редактироваться перед каждым запуском инстансов.

Попробовать сами вы можете с помощью проекта ec2-fleet и следующих команд:
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 server.js


В первом тесте мы будем запускать node.js без дополнительных флагов, в самой что ни на есть стандартной конфигурации. Начинаем первый тест (все картинки кликабельны):


На всех графиках черной пунктирной линией обозначается кол-во соединений, с максимальным значением в 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. Вернёмся к ним ниже.

Загрузка процессора на том же тесте:


Здесь всё проще — 8 ядер = 800%. Total — общая загрузка, CPU (практически совпадает с Total) — суммарная загрузка процессов node.js, User, Sys — общая загрузка в User mode и ядре соответственно. Линии сглажены скользящим средним по 10 секунд.

Вот этот график, честно говоря, меня разочаровал. Загрузка слишком большая, причём непонятно на что она тратится. Приём соединений проходил нормально, порядка 5-7 тыс. соединений в секунду, видимо, этот сценарий хорошо оптимизирован. Однако, гораздо большую нагрузку создает отсоединение, особенно большими партиями (на графике ок. 3 минут 800% загрузки когда я попытался порвать сразу 400 тыс соединений).

Посмотрим, как ведет себя Event Loop:


На этом графике показаны средние значения 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.

node --nouse-idle-notification server.js


К счастью, мы можем выключить этот сигнал добавив флаг "--nouse-idle-notification". Посмотрим что нам это даст во втором тесте:


Во-первых, можно отметить, что потребление памяти увеличилось на 1 Гб (10%) и стабилизировалось где-то через 5 минут после последнего соединения, что, в общем-то неплохо. Также, виден «пилообразный» характер графика. Почему?

Давайте посмотрим график потребления памяти одного воркера в упрощенном тесте:


Теперь понятно — примерно раз в 5 минут происходит сборка мусора в каждом воркере, формируя «пилу» в графике суммарной памяти.

Смотрим процессор и Event Loop:



Ну вот, так гораздо лучше! 1 млн. соединений нагружают от 2 до 3 ядер из 8.

Теперь посмотрим хорошо ли справляется Garbage Collector без IdleNotification в третьем тесте:


Из этого графика видно во-первых, что желтая область (Node.js native structures) всё таки не утекла, а переиспользуется. Во-вторых, сборка мусора могла бы быть и получше.

node --nouse-idle-notification --expose-gc server.js


Чтож, берём сборку мусора в свои руки. Запускаем node.js с флагом "--expose-gc" и вызываем gc(); раз в минуту в четвертом тесте:




Чтож, неплохо. Память высвобождается достаточно резво, процессор под контролем, но раз в минуту у нас есть всплески tick-avg. Думаю, это хороший компромисс.

Что дальше?



Во-первых, необходимо подтверждение результатов независимым тестированием. Я сделал всё, чтобы это было просто. Пожалуйста, если у вас простаивает хорошая машинка, попробуйте сами проделать мой путь — это очень интересно.

Во-вторых, ясно, что этот benchmark далёк от реальности. В качестве интересного реального применения хорошо было бы построить аналог jabber-сервера и потестить его на таких же объемах.
В любом случае, надеюсь, что и текущий фреймворк поможет разработчикам Node.js и V8 в дальнейшей оптимизации.

В-третьих, нужно провести ещё эксперименты для выяснения:
  • На что влияет включение/отключение алгоритма Нагла (socket.setNoDelay()).
  • Одно сообщение в 20 секунд на соединение — достаточно мало. Какое количество сообщений в секунду выдержит сервер? Возможно, в этом будет ограничение на реальных проектах.
  • Можно более точно определить выделение памяти ядром на сокеты используя /proc/sockstat.
  • Почему не освобождается желтая область (Node.js native)? Что это? Как это можно исправить?
  • Почему закрытие сокетов нагружает процессор гораздо больше, чем их открытие?
  • Попробовать поиграться ещё с настройками ядра для TCP, а также выключить модуль netfilter/conntrack/iptables, может это положительно скажется на памяти.
  • Используя метод с 40 инстансами AWS попробовать нагрузить сервера с другими технологиями — Erlang, Java NIO, Twisted и т.п. Сравнить характеристики.
  • Как быстро происходит межпроцессное взаимодействие? Можно ли передавать открытый дескриптор между процессами, группируя их по комнатам, для локализации взаимодействия между клиентами? Это сильно поможет в реальном проекте.
Alexander Shtuchkin @ashtuchkin
карма
42,5
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

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

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

  • –64
    278,9% CPU это интересно…
    • +10
      Каждые 100% — это полная загрузка одного ядра процессора.
      • –1
        Обещаю впредь читать пост до конца и никогда не писать коментов.
    • +49
      Статью не читай, комментируй!
      • –6
        Статью не читал, — не комментируй!
        • +3
          А ведь правильно же сказал Defari. Вдумайтесь :)
          • 0
            Неправильно — там запятая лишняя ;-)
            • –4
              Я про смысл.
              Человек видит то, что хочет видеть. Запятую при первом прочтении не заметил.
              • –5
                Сарказма в моём сообщении тоже :)
            • –4
              Если лишняя запятая, то один смысл, если лишний дефис — то другой.
              • –3
                Дефиса там нет. А какой смысл, если лишнее — тире?
                • –5
                  (я) статью не читал, (ты) не комментируй!
                  (ты) статью не читал — (ты) не комментируй!
    • +10
      Набросились на человека, может он просто виндузятник?
      • +19
        А вдруг у него процессор одноядерный?!
        • –13
          Или мозг…
    • +5
      Всем спасибо, я всё понял. Даю до свидания…
  • +3
    Быстрый рост node.js очень радует.
    Еще недавно подобные результаты были непосильной планкой.
    • 0
      Гм это заслуга node.js или просто более новое железо? В чем сила?
      • 0
        Конечно-же node.js. В шестой версии были утечки памяти, теперь в восьмой с этим дела намного лучше.
  • +3
    Почему закрытие сокетов нагружает процессор гораздо больше, чем их открытие?

    Отсылаются данные клиенту — TCP graceful shutdown
    • 0
      На установление соединения, как и на разрыв, уходит по 3 передачи.
      • 0
        Видимо имелось в виду, что еще завершается передача тех пакетов, которые на момент выполнения close или shutdown еще были в очереди на отправку.
        • 0
          Читайте лучше, алгоритм Нагла выключен.
          • 0
            Это не мной имелось в виду, я уточняю комментарий eyeofhell выше.
  • +6
    Как это ни странно, обеспечить необходимую нагрузку было сложнее, чем создать сервер. Дело в том, что TCP-соединение уникально определяется четверкой [source ip, source port, dest ip, dest port], таким образом с одной машины на 1 порт сервера можно создать не более 64 тыс одновременных соединений (по количеству source ports).

    А сколько можно принять в себя соединений?
    • +8
      вообще, оно ограничивается памятью системы, памятью выделенной сетевому стеку и количеством файловых дескрипторов в системе. Предел последнего определяется типом int в С:
      root@oxpa-desk:~# sysctl fs.file-max=2147483648
      fs.file-max = 2147483648
      root@oxpa-desk:~# grep -i dhc /var/log/messages*^C
      root@oxpa-desk:~# sysctl -a | grep file-max
      error: "Invalid argument" reading key "fs.binfmt_misc.register"
      error: permission denied on key 'net.ipv4.route.flush'
      error: permission denied on key 'net.ipv6.route.flush'
      fs.file-max = -18446744071562067968
      root@oxpa-desk:~# sysctl fs.file-max=2147483647
      fs.file-max = 2147483647
      root@oxpa-desk:~# sysctl -a | grep file-max
      error: "Invalid argument" reading key "fs.binfmt_misc.register"
      fs.file-max = 2147483647
      
      • +6
        дополню сам себя: ядро выделяет структуру sk_buf для кадого соединения. В эту структуру входят указатели на буферы. Но размер самой структуры зависит от архитектуры и не превышает 250 байт. так что можно считать, что для каждого соединения выделяется 2 буфера (чтение и запись) + 250 байт. Разделите объём своей памяти на эти настройки и получите максимальное количество соединений для своей машины. Однако учтите, что софту так же требуется память ;)
        • 0
          А как заранее узнать размер этих «два буфера»? Размер буфера на передачу не может быть меньше TCP-окна (которое неизвестно до согласования этого параметра в конкретном соединении), т.е. 8Кб, к примеру, но на практике размер этих буферов в Linux вроде бы десятки килобайт (?) на каждое соединение. Т.е. 250 байт вообще погоды не делают в подсчете.
          • 0
            oxpa@oxpa-desktop:~$ sysctl -a 2>/dev/null| grep tcp_[rw]mem 
            net.ipv4.tcp_wmem = 4096        16384   4194304
            net.ipv4.tcp_rmem = 4096        87380   4194304
            

            Первые значения — сколько байт будет выделено под буферы при любых условиях.
            Второе — при обычном течении «жизни».
            Третье — сколько максимум может занять буфер.
            Дополнительно можете почитать про tcp_mem, станет ясно, что значит «при любых условиях».

            250 байт на 1 миллионе соединений превращаются в 238 мегабайт памяти ;) Буферы, как минимум, — в
            7 с половиной гигабайт
            ((((4096 * 2) * 1 000 000) / 1024) / 1024) / 1024 = 7.62939453
            . Мне кажется, это сопоставимые цифры.
            Понятно, что максимальный размер буфера может быть больше. Но для записи слова ping хватит и 4к ;)
    • +2
      Можно легко сделать через IPv6 на одном сервере.
      Благо адресов IPv6 можно сделать дофига. Тот же Hetzner дает /64 подсети всем желающим.
  • +6
    Хотелось бы сравнений с фреймворками питона и расхваливаемым Erlang'ом.
    • 0
      А зачем фреймворкам питона участвовать в таком соревновании? Они, вроде бы, не для этого…
      • +14
        twisted и tornado вполне себе для этого
    • +3
      Читал недавно более чем годовалую перепалку между разработчиком на node.js и знатными программистами на erlang'е и понял, что мне тоже безумно интересно увидеть сравнение. И по нагрузке, и по функционалу.
      • 0
        Интересно. 2 миллиона на более мощном железе (24 cpu). CPU и памяти занимает больше чем в два раза. Но непонятно, это продакшн или синтетический тест.
        • +1
          Судя по всему, это продакшн. Используется FreeBSD и Эрланг.

          Я видел эту ссылку, но, к сожалению, слишком мало информации о том, что конкретно они делают а также какие конкретно оптимизации они провели.
        • +3
          Тесты тестами, а по ссылке данные с продакшна whatsapp.com (см. их предшествующую статью, указанную в начале этой, посвященную 1млн. blog.whatsapp.com/?p=170). Подобных продакшнов с нодой что-то я пока не видал =)
    • +3
      За эрлангом далеко ходить не надо — habrahabr.ru/post/111600/
      • +1
        Да, я упомянул английский оригинал этой статьи. К сожалению, для достижения миллиона соединений в 2008 году автору пришлось дописывать к эрлангу отдельный модуль на C++, иначе по его подсчетам ему было необходимо 36 Гб памяти.

        А в целом я тоже с удовольствием посмотрел бы на поведение современного эрланга под такой нагрузкой. К сожалению, я сам в нём не очень разбираюсь.
    • +4
      • +1
        >fprog
        >node.js
        > 2010
        Сильно сомневаюсь в объективности статьи. При этом автор практически не затронул проблемы ерланга (сообщество, библиотеки, среда разработки)

        • 0
          я же сказал, что статья старая
          • 0
            Это перекрывает только "> 2010".
            • 0
              а с другим я и не спорю.
              • 0
                Я просто к тому, что любая информация о ноде старше пары месяцев может быть уже не действительна.
    • 0
      хотелось бы сравнения с boost.asio.
  • НЛО прилетело и опубликовало эту надпись здесь
    • +1
      Не преувеличивайте. Отрезать из трафика даже 40 серверов с фиксированными IP — ничего сложного. реальным DDoS тут не пахнет.
      • 0
        Реальный DDoS это не только когда много IP, а ещё когда целевая атака на уязвимый DoS вектор. А забить канал можно и 40 серверами вполне себе так.
    • +1
      Потом будет эпичный Bill-check от амазона))
      • 0
        зачем же гонять инстансы целый день.
        Подавляющее большинство сервисов достаточно досить по несколько часов в день, особенно в час пик, чтобы доставить большие неприятности.
        • 0
          А траффик?
          • 0
            что траффик?
  • +5
    Очень интересно :)

    Цифры огромные, но если задуматься, то не так уж и удивительно. Это по сути и есть самый оптимальный случай для event-based фреймворков: когда обработка запроса — это всего лишь отравка простого сообщения.

    В жизни такое вряд ли встретится. А интересно, например, что будет если для отправки сообщения нужно залезть в базу, или сделать syscall, или обратиться к данным, защищенным мьютексом. И отдельным случаем рассмотреть ситуацию, когда выполняется какое-то ресурсоемкое вычисление.
    • 0
      Обработка запроса, правда, не совсем корректный термин в данном контексте. Надеюсь понятно, что я говорил про вот эту анонимную функцию:

      var pingInterval = setInterval(function() {
          res.write('ping');
      }, 20*1000);
      
    • 0
      Отправка простого сообщения — это тоже syscall, если что :) Прочитать же данные нужно.

      Ресурсоёмкие вычисления заведомо не для event loop.

      При нагрузке в 300% из 800% вполне можно позволить иметь пул на 1000 соединений в базу и делать туда read/write. Миллион на реальных условиях не выжать, но всё равно цифры будут достаточные.
      • 0
        Отправка простого сообщения — это тоже syscall, если что :)

        Не спорю, но в ноде же асинхронное I/O по умолчанию. Нет сомнений, что тут с производительностью вопрос решен, и не надо ждать, пока данные отправятся :)

        При нагрузке в 300% из 800% вполне можно позволить иметь пул на 1000 соединений в базу и делать туда read/write.

        Так вот цифры как раз интересны. Я ж не критикую :)
  • +15
    Райан (тот, который эту самую ноду написал), в твиттер линк запостил на эту статью: twitter.com/ryah/status/235765301940191232
  • 0
    >В качестве интересного реального применения хорошо было бы построить аналог jabber-сервера и потестить его на таких же объемах.

    У вконтакте xmpp-сервер написан на nodejs.
  • 0
    А мастер случайно не ограничивает всю эту грядку? Может, если вместо него воткнуть haproxy, то ещё полегчает?
    • 0
      Вроде нет. Мастер практически не грузит процессор и занимает всего 50 мб памяти. Как я понимаю, он просто передаёт открытые дескрипторы и через себя никаких данных не пропускает.

      Другой вопрос — можно ли открытые дескрипторы между воркерами передавать, это бы сильно помогло в реальных приложениях в сокращении межпроцессного взаимодействия.
      • +2
        открытые дескрипторы между воркерами передавать
        Если он на самом деле акцептит подключения в мастере и передает воркерам каждый сокет, то я сильно удивлён…
        Предполагаю все же, что воркерам передается слушающий сокет и каджый форк акцептит подключения самостоятельно
        • 0
          Конечно же, вы правы.
      • 0
        В линуксе открытые дескрипторы между процессами передавать можно.
  • 0
    Код клиентов на разных языках/framework'ах для теста схожей функциональности можно найти по ссылкам из этой статьи: blog.virtan.com/2012/07/million-rps-battle.html
    • 0
      Класс! Стоит статьи на хабре!
  • 0
    Простите, а чем вы так красиво мониторили нагрузку?
    • 0
      Меня тут побьют, но это CSV -> Excel 2013 preview. Возможно, это можно как-то автоматизировать.
  • +2
    А что будет если появятся медленные соединения?
    • 0
      Ж!
      • 0
        С чего? Главное чтобы количество медленных соединений не превышало лимита одновременных соединений (заданного явно или наличием свободной памяти/запасом по CPU — не суть). Каждому соединению сразу выделяется буфер и серверу всё равно быстрое оно или медленное, лишь бы памяти на буфера хватило. Проблемы возникают когда не соединения медленные, а медленная обработка запросов, когда сервер может держать миллион соединений, но выполнять только 10 000 запросов в секунду. Тогда при одновременном миллионе соединений одно из них закроется, то есть получит ответ (если брать открыли-послали запрос-получили ответ-закрыли, а-ля http без keep-alive ) только через 100 секунд, а если будет миллион один запрос одновременно, то один из них просто отвалится.
    • 0
      Медленные соединения вообще-то требуют меньше ресурсов, чем быстрые.
      Главная проблема медленных соединений — их количество, но тут и так миллион соединений, куда уж больше?
      • 0
        >Медленные соединения вообще-то требуют меньше ресурсов, чем быстрые.
        поясни
        • 0
          Медленное соединение требует ресурсы только на поддержку самого соединения. Быстрое — еще и на обработку поступающих запросов.

          Держать миллион соединений гораздо проще, чем отвечать одновременно на миллион запросов.
          • 0
            Медленным-то что, отвечать не требуется?
            • 0
              Требуется. Но только гораздо реже.

              Если до сих пор не понятно, приведу формулы.
              Пусть по некоторому соединению поступают запросы с периодом T.

              Тогда стоимость обработки одного запроса составляет a + bT, где a — ресурсы, требуемые на саму обработку запроса, а b — ресурсы, требуемые на поддержание соединения.

              Стоимость же самого соединения в единицу времени составляет a/T + b.

              Вообще говоря, a и b — не совсем константы, но общей тенденции это не меняет.
              А она такова: чем медленнее соединение (чем больше T), тем больше ресурсов уходит на каждый запрос, но в единицу времени тратится меньше ресурсов.
              • 0
                T в обоих случаях одинаковое. Количество принятых запросов никак на зависит от того «медленные» они или «быстрые».
                «Медленные» — это, к примеру, запрос к базе данных. Web-сервер будет в цикле отдавать эти запросы драйверу БД. Драйвер базы будет обрабатывать запросы по мере их поступления. Сервер-то ладно, примет 1000000 запросов, так как он — супер-пупер Node.js, а вот драйвер БД не думаю, что ответит так быстро. Поэтому front-end сервер будет ждать, поэтому клиенты будут ждать. Поэтому, я считаю, пост из двух символов чуть выше, был справедлив.
                • 0
                  Не забывайте: T — это период поступления запросов по одному соединению! А если клиент «медленный», то и запросы он будет отдавать реже (ведь ему нужно дождаться ответа от сервера по прошлому запросу, прежде чем выдавать новый).
                  • 0
                    В этом случае я не спорю, запросы он будет отдавать реже, но реже он будет отдавать всем клиентам, так как event loop — это единственный поток.
                    Каждый клиент в данном случае зависит от запросов других клиентов. Вот придет очень медленный клиент — все будут ждать ответы на свои запросы очень долго, хотя и все соединены с сервером.
                    В данном случае автор поста держал 1000000 одновременных соединений, весь смысл в которых — передача одинаковых строк. Если будут медленные клиенты — да, потребление памяти увеличится незначительно, но время отклика сервера очень сильно возрастет.
                    • 0
                      Не верно.
                      Если придет медленный клиент — его запрос не попадет в event loop до тех пор, пока не будет прочитан полностью.

                      Весь смысл асинхронного программирования в том и заключается, что никто ничего никогда не ждет. Нет работы — ставь коллбэки и уступай очередь.
                      • +1
                        Могу ошибаться, но e2e4 имеел ввиду другое (просто неудачно использовал термин «медленный клиент»). Проблемой для event-loop будет если control flow задержиться на обработке какого-то запроса. Не важно по какой причине: затянувшийся computation, блокирующий запрос к базе, или еще что-то. Для одного клиента это может быть незаметно, ну на 1 сек сервер задумался — с кем не бывает. Но в данном случае получается, что 1 млн клиентов ждали эту одну секунду — а это совокупно более 11 суток потраченного времени ваших клиентов. И это даст очень резкий пик на графике среднего времени ответа (потому что эта 1 сек пойдет в плюс всем).

                        Синтетичность теста в том, что он делает достоверно простую операцию независимо ни от каких факторов. В продакшин системах, выполняющих какую-то полезную логику, все намного сложнее.
                        • 0
                          Сам web-сервер не задумается, задумается то, что работает в блокирующем режиме, а это в свою очередь скажется на всей системе, то есть как бы «весь сервер» задумается
                      • 0
                        Да, в event loop web-сервера не попадает, но драйверу БД, к примеру, придется обработать все запросы всех этих клиентов. Если он также будет асинхронный, то у меня вопросов нет, хотя любой ввод/вывод — это блокирующая операция.

                        > никто ничего никогда не ждет. Нет работы — ставь коллбэки и уступай очередь.

                        Если эти коллбэки и коллбэки этих коллбэков также будут асинхронными, то да. Но такого не бывает. По крайней мере не слышал.

                        P.S. Асинхронная модель — она как паразит — должна заразить весь продукт. Иначе — задержки отклика.
                        • 0
                          Именно в этом сила экосистемы Node.js — там всё асинхронное по умолчанию. И ввод-вывод тоже асинхронный, как файловый, так и сетевой (по крайней мере до уровня операционной системы, а дальше нам не прыгнуть).

                          С базами данных — все драйвера БД в Node.js асинхронные, насколько я знаю. Т.е. сама база может быть и синхронная, но взаимодействие с ней — нет. Event Loop никогда не ждёт ответа, операционная система сама оповещает когда нужно, что ответ пришёл.

                          Вот с долгими вычислениями — может быть проблема, но есть предположение, что в большинстве веб-серверов они не возникают, а если и возникают, то можно опять же асинхронно передать задачу другому воркеру или серверу.
                          • 0
                            > Event Loop никогда не ждёт ответа
                            Зато все клиенты ждут, пока БД выполнит запросы.
                            Будет время — попробую сделать тест (как вы это любите), но не с простым «ping — pong», а с обработкой запросов.
                            Модель может быть на 100% асинхронной неблокирующей, но от нее будет толку 0: никто работать не хочет, а только перекладывают задачи с одного на другого.
                            • +2
                              Просто тут нет ситуации, когда БД выполняет 1 секунду запрос от 1 клиента, а остальные 999999 клиентов эту секунду ждут из-за того, что Event Loop заблокирован. В реальности сразу после посылки запроса БД (например, записи в TCP сокет БД) управление возвращается Event Loop-у. Далее идет обработка событий от других клиентов в штатном режиме, а через секунду от операционной системы приходит событие, что в TCP сокете БД появились данные, они обрабатываются и возвращаются клиенту. То, что сама БД синхронная или асинхронная — совершенно не важно. Важно, что работа с ней асинхронная, через асинхронный механизм TCP сокетов.
                              Совершенно другой вопрос, если, например, веб-сервер может обрабатывать 10 тыс запросов в секунду, каждый запрос генерирует 1 запрос в БД, а БД может обрабатывать только 1 тыс в секунду. Тогда очевидно, что все очереди переполнятся и скорость упадёт до скорости БД. Но, я так понимаю, вы не этот случай имели в виду.
    • 0
      Кирилл,
      судя по описанию, много открытых соединений появилось именно из-за того что node.js не успевал их обрабатывать, хотя обработка одного соединения довольно-таки быстрая.
      т.е. интересен график скорости реакции (зависимость скорости обработки соединения от кол-во соединений).
      • 0
        akalend, вы ошиблись.

        Если внимательно посмотреть на код клиента, то будет видно, что клиенты поддерживают заданную нагрузку на сервер, независимо от того, как быстро сервер работает.
      • 0
        Akalend, здесь задача ставилась другая. Сервер принимает соединения (ок 10000 в сек если интересно), но не закрывает их. Это дает возможность инициировать события с сервера. Посмотрите в интернетах термин HTTP Comet.
    • 0
      Вроде ничего страшного не предвидится. В текущем тесте передается один пакет в 20 секун, мне кажется это уже достаточно медленно. Возможно, если вы опишете подробнее что вы имеете в виду, тогда можно будет сделать тест.
      • 0
        Под медленными клиентами, я понимал в первую очередь людей у которых плохой или ненадежный интернет.

        Как работает ваш асинхронный сервер — вы сказали send и отправили буфер (фактически скопировали память с userspace в память которая уйдет уже в сокет). Интересный факт в том, что если вы оправляете большой ответ, скажем два пакета, пока не придет два подтверждения от клиента. Т.е. если у вас много медленных клиентов, то ядро своими буферами с ответами съест какую-то память.

        Далее, вы в примере используете ip conntrack, судя по настройкам в тесте, что увеличит время в пессимистичном раскладе которое будет использоваться больший объем памяти.

        Т.е. в этих тестах вы совершенно не следили за памятью ядра — а она тоже будет расходоваться.
        • 0
          Действительно, пока не придёт подтверждения, вся отправленная информация хранится в буфере ядра.
          На графиках в статье суммарный объём памяти, используемой ядром, обозначается серой полосой (разница между Total Netto и RSS Sum), это около 4 Гб в пике, по 4 кб на соединение. Думаю, этого должно хватить для обслуживания совсем медленных клиентов если их будет не большинство.

          К тому же, т.к. в тесте клиентские машины были равномерно в 3-х датацентрах AWS, два из них в США, а сервер — в Германии, а, думаю, пинг у них был не менее 200-300 мс, то это можно считать вполне реалистичной пользовательской нагрузкой (если, конечно, не целиться на мобильные клиенты — там, конечно, всё по другому).
          • 0
            Тогда тут мой пардон, я не смог интерпретировать Total Netto как ядерную память.

            Да, 4kb современный линукс выделяет на одно соединение, это правда.

            Если данное решение ориентировано на сервис, у которого 80% пользователей будут иметь стабильное (тут скорее вопрос не в latency, а именно в bandwish) соединение с сервером — то вопросов нет.

            Если там будут мобильные клиенты, то я бы закладывался что total netto будет выше.

            Так же я бы закладывался что total netto будет выше, в случае если вы будете посылать более большие ответы.
            • 0
              В свою очередь извиняюсь, что не сделал это очевидным) С остальным полностью согласен!
            • 0
              Честного говоря не понимаю какая разница медленное соединение или быстрое, если их одновременное количество одинаково, а сообщения влезают в минимальный буфер и всё же успевают отдаваться не переполняя и не вызывая роста буфера. Грубо говоря, если сервер генерирует по килобайту в секунду для каждого клиента, а скорость сервер-клиент не ниже 9600 бит в секунду.

              Другое дело, если мы говорим не о количестве одновременных соединений, а о постоянном количестве запросов за период, при этом некоторые клиенты за этот период не будут успевать выбирать ответ (latency или bandwish — не суть, если речь о протоколе с подтверждением доставки) — тогда, да, рано или поздно количество соединений достигнет такого числа, что у сервера будет исчерпан лимит на количество соединений и/или размер буферов. Грубо говоря он будет держать миллион медленных соединений, а для миллион первого уже не сможет выделить буфер, даже если оно будет быстрым и общая скорость запросов невелика относительно возможностей сервера.
              • +1
                Т.к. у нас TCP нам нужно держать буфер пока не придет ACK.

                4кб расходы системы на буфер (на самом деле их несколько, просто тут не секундная летенси и получается как будто 4кб, в реальности будет где-то 12-16).

                Если у нас идет поток 10,000 запросов в секунду, то мы будем потреблять как минимум буфера на эти соеденения + дельту на передачу данных. Если буфер передается за 1 секунду до клиента, то нам нужно ~100 мегабайт памяти. Если начинаются потери и идут всякие ретрансмиты, то у нас требования по памяти вырастают.

                Я не говорю что невозможно сделать 1М соединений с сервером. У меня пока рекорд около 4,5М на amazon :), тут просто надо понимать, что есть ограничения другого уровня и их тоже надо учитывать.
            • 0
              да, с мобильными клиентами у нас сервер вылетает по памяти на лонгпуле…
              думаю как решить эту проблему
              • 0
                Там счастья нет. Надо смотреть что у вас за траффик, какой процент людей и чем вы готовы жертвовать ради достижения какого-то счастья.
  • 0
    Можно было бы создать 16 сетевых интерфейсов с разными IP

    А что мешало на один интерфейс повесить 16 IP адресов?
    • +1
      Очевидно, отсутствие этих самых дополнительных адресов.
      • 0
        Я, конечно, отродясь не пользовался услугами подобного рода хостеров, но если они могут выдать две машины в одном броадкастовом сегменте и без фильтрации на порту, то можно назначить интерфейсам абсолютно произвольную адресацию, хоть из сети 1.1.1.0/24. Оно не будет маршрутизироваться вне сегмента, но в пределах его — без проблем.
        • 0
          Проблемы начнутся, если подобное кто-нибудь заметит…
          • 0
            А это запрещено? Никто ведь не собирается маршрутизировать пакеты с этими адресами наружу. А что творится в VLANе клиента — дело клиента. Хостеру это никак не повредит.
            • 0
              Если клиенту предоставлен VLAN — то действительно, никаких проблем нет.

              А вот если клиенту выдано всего лишь «две машины в одном броадкастовом сегменте и без фильтрации на порту», то подобное поведение может быть расценено как попытка нарушения работы сети.
              • 0
                А вот если клиенту выдано всего лишь «две машины в одном броадкастовом сегменте и без фильтрации на порту», то подобное поведение может быть расценено как попытка нарушения работы сети.

                В любом случае, Хецнер выдает /64 сеть IPv6. Это чертовски много адресов. И можно назначить на один интерфейс много-много v6 адресов, по ним и создавать нагрузку.
                • 0
                  Да, IPv6 действительно решает проблему.
    • 0
      Проще и более масштабируемо было отдать 80 центов амазону) Не нужно доп машины и переписки с хостером. Плюс, географически распределенные клиенты создают более реалистичные условия.
      • 0
        Тут мне в голову пришла безумная идея: генерить коннекты к серверу на самом сервере. То есть, скажем, 16 воркеров, каждый цепляется со своего адреса на лупбек. Памяти это выжрет немерено…
        • 0
          Тогда вам не удастся получить достоверную инфу.
          Нагрузка созадваемая клиентскими процессами будет мешаться серверной
          Нужно жостко следить за ядрами и памятью конкретных процессов
  • +1
    > Можно ли передавать открытый дескриптор между процессами, группируя их по комнатам,
    у меня получилось передать ссылку на открытый дескриптор через shmem (У Стивенсона описано про передачу ресурсов через системную очередь),
    но проблема в том, что принимающий процесс не должен открывать/закрывать другие файлы, иначе ссылка в процессе может сбиться.
    • +1
      Все проще, и делается это через unix-сокеты в дейтаграммном режиме. К сожалению, нужную константу я почему-то не смог найти…
      • 0
        cmsg_level = SOL_SOCKET
        cmsg_type = SCM_RIGHTS
        • 0
          передаем открытый дескриптор через unix сокет?
          можно передать только номер дескриптора
          • 0
            1. да
            2. SCM_RIGHTS — передаем права доступа. Далее идет в структуре идет номер дескриптора.
  • 0
    По алгоритму нагла можно почитать тут — en.wikipedia.org/wiki/Nagle's_algorithm
    Если коротко, то это дополнительная буферизация, которая дает выигрыш, если приложение часто отправляет маленькие куски данных по сети. Это будет работать в том случае, если вы отправляете постоянно данные в сокет размером заметно меньшим половине MTU (в этом случае несколько пакетов можно отправить один TCP-пакетом и сэкономить на передаче заголовков TCP-пакетов). В случае HTTP-сервера этот алгоритм не имеет смысла использовать.
  • 0
    Выглядит круто. Но пугает одно Но: допустим у меня есть нагрузка в 2кк клиентов и она распределена между 2мя серверами. Что будет если один из них внезапно умрёт? Ведь 1кк клиентов придут на 2й сервер, причём сразу — 1кк одновременных попыток соединений. На установку соединения ведь необходимо время, а значит второй сервер будет не доступен до тех пор, пока все соединения не будут установлены/отброшены. Что будет в продакшене? 1кк соединений конечно хорошо, но может лучше 10 по 100к и равномерное размазывание нагрузки с упавшего сервера?
    • 0
      Наверное подобные решения для того у кого либо пик 1кк, либо 10 по 1кк :)
    • 0
      Это же тест без нагрузки. Планируется, что в реальном приложении на пэйлоад уйдет в 10-1000 раз больше ресурсов, чем на поддержание соединений, тогда пики на подключении будут непринципиальны.
  • 0
    А для приема 1kk соединений вам не требовалось писать Хецнеру? Я знаю, что на VPS их файервол ограничивает количество одновременных соединений до ~100/сек. На dedicated они это не делают?
    • +1
      Нет, не требовалось. Хотя, после теста они прислали мне письмо, что зарегистрировали атаку с моего сервера, т.к. количество пакетов превысило 30 тыс в секунду (мой сервер отсылал 50 тыс), но ничего страшного — я написал, что это тест и всё.
  • 0
    Я правильно понял что вы на 1IP и 1 порту(8888) смогли обслуживать одновременно 1кк постоянных коннектов? Ведь кол-во одновременных TCP соеденений для 1IP:port ограничено net.ipv4.ip_local_port_range(максимально 65535).
    • +1
      Прямо в статье же написано
      Дело в том, что TCP-соединение уникально определяется четверкой [source ip, source port, dest ip, dest port]

      Таких четверок очевидно больше чем 65535.

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