О насБлогКонтакты
Backend5 декабря 2011 г. 5 мин 10

Почему мы выбрали Node.js в 2011 году

AunimedaAunimeda
📋 Содержание

Почему мы выбрали Node.js в 2011 году

В ноябре 2011 у нас была конкретная проблема: BI-дашборд, опрашивающий сервер каждые 5 секунд от 150+ одновременных пользователей. PHP-бэкенд генерировал 1800+ запросов к базе данных в минуту, большинство возвращало идентичные данные. Нагрузка на сервер была 70% в рабочее время. Решение выглядело как кэширование — но клиенту нужны были свежие данные в течение 10 секунд после закрытия сделки.

Мы могли добавить Memcached. Мы могли настроить MySQL. Мы выбрали Node.js.

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


Техническая проблема

Наш дашборд был циклом опроса:

// Клиентская сторона (jQuery) — что у нас было
setInterval(function() {
  $.get('/api/dashboard-data', function(data) {
    updateCharts(data);
  });
}, 5000);

150 пользователей × опрос каждые 5 секунд = 30 запросов/сек к PHP-бэкенду. Каждый запрос: PHP-FPM порождал воркера, тот подключался к MySQL, выполнял 4-6 запросов, возвращал JSON.

Сами запросы были быстрыми — менее 20мс каждый. Накладные расходы — управление процессами PHP-FPM + накладные расходы соединения MySQL × 30 запросов/сек.

Данные на дашборде менялись когда закрывалась сделка — примерно 20-40 раз в час в рабочее время. Значит, 98% из тех 1800 запросов в минуту возвращали те же данные, что были возвращены 5 секунд назад.

Фундаментальное несоответствие: pull-модель для данных, которые менялись всего 30-40 раз в час.


Почему Node.js подходил для этой задачи

1. WebSockets делали архитектуру правильной.

Вместо того чтобы 150 клиентов спрашивали «есть новые данные?» каждые 5 секунд, сервер сам сообщал клиентам когда появлялись новые данные. Push вместо pull. WebSockets давали постоянное соединение; Socket.io 0.9 обрабатывал совместимость с браузерами и переподключение.

Новая архитектура:

  • Одно WebSocket-соединение на пользователя: 150 соединений всего
  • Сервер отправляет обновление всем клиентам при изменении данных: 30-40 раз/час
  • Запросы MySQL: 30-40 в час вместо 1800 в минуту

Улучшение соотношения: в 2700 раз меньше запросов к базе данных при реальной нагрузке.

2. Node.js эффективно обрабатывает постоянные соединения.

150 открытых WebSocket-соединений с одним процессом. В Apache/PHP 150 одновременных соединений означали до 150 PHP-воркеров, каждый занимал 2-8МБ стека. В Node.js 150 соединений обрабатывает один event loop — менее мегабайта накладных расходов на соединение.


Внутренние возражения

Мы были PHP-командой. Никто не выпускал Node.js в production. Возражения были реальными:

Возражение 1: «Node.js не готов к production»

Версия 0.6 была первым релизом с заявленной гарантией стабильности. Аргумент против: даже в 0.6 были отчёты об утечках памяти, модель процесса (перезапуск при падении) ещё развивалась.

Наш ответ: запустим Node.js за nginx, с PM2 как менеджером процессов. Если Node упадёт, PM2 перезапустит его через 1-2 секунды. Дашборд, ненадолго ставший пустым — приемлемо для BI-инструмента. Неприемлемо для оформления заказа.

Мы намеренно выбирали первый деплой с низкими ставками: дашборд, не транзакционный эндпоинт. Если Node.js нас подведёт, пользователи увидят пустой график, а не потеряют покупку.

Возражение 2: «Никто в команде не знает Node.js»

Правда. Мы выделили одного разработчика учиться во время разработки. Кривая обучения была ниже ожидаемой: JavaScript мы уже знали; новой ментальной моделью был event loop и асинхронный I/O. Две недели вечеров и выходных.

Настоящий риск был не в изучении Node.js — а в отладке незнакомой среды выполнения в production. Мы смягчили это обильным логированием:

var logger = new winston.Logger({
  transports: [
    new winston.transports.Console(),
    new winston.transports.File({ filename: '/var/log/dashboard.log' })
  ]
});

io.on('connection', function(socket) {
  logger.info('Клиент подключён', { id: socket.id, timestamp: new Date().toISOString() });
});

function broadcastUpdate(section, data) {
  logger.info('Broadcast обновления', {
    section: section,
    clients: io.sockets.clients(section).length
  });
  io.to('dashboard:' + section).emit('update', data);
}

Возражение 3: «PHP работает нормально, зачем рисковать?»

Это было самым тяжёлым возражением, потому что оно не было неправильным. PHP бы справился. Memcached + PHP + опрос раз в 10 секунд снизил бы нагрузку на 50%. Это был проект на 2 дня против 3-недельного build на Node.js.

Наш ответ был ориентирован на будущее: у нас было ещё два клиентских проекта с реал-тайм функциями. Если мы проверим Node.js здесь, у тех проектов будет проверенная среда выполнения. 3 недели — это инвестиция в возможности, а не просто решение одной проблемы.


Реализация: что мы построили

var pool = mysql.createPool({ host: 'db.internal', connectionLimit: 5 });

// Опрос MySQL на изменения — только бродкастим когда данные реально изменились
var lastDataHash = {};

setInterval(function() {
  ['sales', 'agents', 'recent'].forEach(function(section) {
    getDashboardData(section, function(err, data) {
      if (err) return;
      var hash = JSON.stringify(data);
      if (hash !== lastDataHash[section]) {
        lastDataHash[section] = hash;
        socketServer.to('dashboard:' + section).emit('update', data);
      }
    });
  });
}, 3000);

Мы всё ещё опрашивали MySQL каждые 3 секунды — в 2011 не было уведомлений об изменениях MySQL. Но вместо 150 клиентов, опрашивающих PHP каждые 5 секунд (750 PHP-запросов/минуту), у нас был 1 Node.js-процесс, опрашивающий MySQL каждые 3 секунды (9 запросов/минуту) и рассылающий всем подключённым клиентам.


Результаты через 60 дней

Метрика До (PHP polling) После (Node.js WebSockets)
Запросов MySQL/час при 150 пользователях ~108 000 ~540
CPU сервера (рабочее время) 68-74% 8-12%
Задержка обновления дашборда 0–5 секунд <0.5 секунды
Память (серверный процесс) 350-480МБ (PHP-FPM воркеры) 42МБ (Node.js)
Инцидентов за 60 дней 1 (OOM через 18 дней)

Один инцидент: утечка памяти в объекте lastDataHash. Мы добавляли в него новые сравнения без очистки старых. Исправлено за 20 минут, 2-секундный downtime. PM2 автоперезапустился раньше, чем нас успели оповестить.


Что бы мы сделали иначе

Мы бы всё равно выбрали Node.js. Добавили бы:

  • --max-old-space-size=256 для ограничения heap с самого начала
  • Профилирование heap с первого дня
  • Автоперезапуск PM2 при превышении памяти: max_memory_restart: '200M'
  • Redis для состояния вместо in-process хеша

Решение было правильным. Реализация имела утечку памяти, проявившуюся через 18 дней. Оба утверждения верны.

Более широкий урок: новая технология в production всегда рискованнее, чем ты оцениваешь. Смягчение — не избегать её, а выбирать первые деплои с низкими ставками, чрезмерно инструментировать и иметь путь отката. У нас было всё три. Инцидент был управляемым.

Читайте также

Реальное время на Node.js: WebSockets и MongoDB вместо бесконечного pollingaunimeda
Backend

Реальное время на Node.js: WebSockets и MongoDB вместо бесконечного polling

Как мы заменили AJAX-опрос каждые 5 секунд на WebSocket-соединение с Socket.io - и почему событийная модель Node.js навсегда изменила наш подход к серверной разработке.

Революция Node.js (2011): как JavaScript завоевал серверaunimeda
Backend

Революция Node.js (2011): как JavaScript завоевал сервер

Райан Даль показал Node.js на JSConf в 2009. К 2011 мы запускали его в production. Неблокирующий I/O, npm и осознание, что один язык может работать везде, изменили наём, разработку и мышление.

Оптимизация MySQL: переход с MyISAM на InnoDB и кэширование через Memcachedaunimeda
Backend

Оптимизация MySQL: переход с MyISAM на InnoDB и кэширование через Memcached

Как мы спасли продакшен-базу от деградации под нагрузкой: миграция с MyISAM на InnoDB, настройка пула буферов и внедрение Memcached - реальный кейс 2011 года.

Нужна IT-разработка для вашего бизнеса?

Разрабатываем сайты, мобильные приложения и AI-решения для бизнеса в Кыргызстане. Бесплатная консультация.

Получить консультацию Все статьи