Революция Node.js (2011): как JavaScript завоевал сервер
Райан Даль представил Node.js на JSConf EU в ноябре 2009. Демо показывало простой веб-сервер, обрабатывающий несколько загрузок файлов одновременно без блокировки. Аплодисменты в записи слышны и искренни.
Решаемая проблема: Apache порождал поток или процесс на каждое соединение. При 1000 одновременных соединений Apache порождал 1000 потоков. Каждый поток использовал 2-8МБ стека. 1000 соединений могли съесть 8ГБ RAM только на накладные расходы потоков, не обработав ни байта бизнес-логики.
Node.js использовал один поток с event loop. 1000 соединений обрабатывал один и тот же процесс. Память измерялась мегабайтами, а не гигабайтами. I/O не блокировал поток — он регистрировал колбек и поток двигался дальше.
Мы начали экспериментировать с Node.js в начале 2011. К середине 2011 использовали в production для определённых нагрузок. К 2013 — стандарт для новых бэкенд-сервисов.
Event Loop на самом деле
// Концептуальный event loop (не реальная реализация)
while (true) {
event = eventQueue.pop();
if (event) {
event.callback(event.data);
}
}
Критическое ограничение: колбеки выполняются до завершения перед следующим. Node.js однопоточный. Колбек, работающий 500мс, блокирует все остальные колбеки на 500мс.
// ХОРОШО: I/O-bound — колбек запускает I/O, возвращается немедленно
app.get('/user/:id', function(req, res) {
db.query('SELECT * FROM users WHERE id = ?', [req.params.id], function(err, rows) {
res.json(rows[0]); // Вызывается когда БД вернула — event loop был свободен
});
});
// ПЛОХО: CPU-bound — блокирует event loop
app.get('/fibonacci/:n', function(req, res) {
var result = fibonacci(parseInt(req.params.n)); // fibonacci(40) = ~1.5 сек блокировки
res.json({ result: result });
});
Пример с fibonacci — реальная ошибка, которую мы допустили в 2012. Клиент хотел эндпоинт вычислений. Мы поместили его в Node.js. При нагрузочном тестировании сервер переставал отвечать на все другие запросы во время вычисления. PHP справился бы нормально — каждый PHP-запрос работал в своём процессе.
npm: революция зависимостей
Node.js поставлялся с npm по умолчанию. К 2013 в npm было 50 000 пакетов. К 2014 — больше, чем в любой другой экосистеме языков.
{
"name": "aunimeda-api",
"dependencies": {
"express": "~3.4.0",
"mongoose": "~3.8.0",
"async": "~0.2.0",
"moment": "~2.0.0"
}
}
npm install загружал и устанавливал всё. Обратная связь «нужна эта утилита» → «опубликована для 50 000 разработчиков» занимала часы.
Express.js и callback hell
// Пирамида судьбы — реальный код 2012 года
app.post('/order', function(req, res) {
User.findById(req.user.id, function(err, user) {
if (err) return res.send(500, err);
Product.findById(req.body.productId, function(err, product) {
if (err) return res.send(500, err);
if (!product.inStock) return res.send(400, 'Нет в наличии');
Order.create({ userId: user._id, productId: product._id }, function(err, order) {
if (err) return res.send(500, err);
// 3 уровня вложенности и 4 повторения if(err)...
res.send(201, order);
});
});
});
});
Библиотека async была решением 2012 года для callback hell:
async.waterfall([
function(cb) { User.findById(req.user.id, cb); },
function(user, cb) {
Product.findById(req.body.productId, function(err, p) { cb(err, user, p); });
},
function(user, product, cb) {
if (!product.inStock) return cb(new Error('Нет в наличии'));
Order.create({ userId: user._id, productId: product._id }, function(err, o) {
cb(err, user, o);
});
},
], function(err, order) {
if (err) return res.send(err.message === 'Нет в наличии' ? 400 : 500, err);
res.send(201, order);
});
Промисы (2013, с Bluebird как популярной библиотекой) и async/await (Node.js 7.6, 2017) в конечном счёте сделали это чистым. Но async.waterfall был прагматичным решением 2012 года.
Проблема «нанять один раз»
Самый глубокий эффект Node.js был не техническим. Он был организационным.
До Node.js: веб-проект требовал PHP или Ruby разработчиков для бэкенда И JavaScript разработчиков для фронтенда. Два набора навыков, два найма.
После Node.js: один JavaScript разработчик мог написать Express бэкенд, React фронтенд, CLI-инструменты сборки и WebSocket слой реального времени. Всё на одном языке. Всё с одним npm-инструментарием.
Это изменило работу небольших команд. Изменило найм в стартапах. Изменило значение «full-stack разработчик» — больше не «кто-то, знающий PHP и jQuery», а «кто-то, глубоко знающий JavaScript, фронтенд и бэкенд».
Node.js — не лучшая серверная среда для каждого случая (Python для ML, Go для высоконагруженных систем). Но он был первым убедительным аргументом, что один язык может быть правильным ответом для большей части веб-стека. Этот аргумент победил.