NodeJS Эффективное программирование Юра Богданов технический директор и соучредитель Eventr
NodeJS Цель проекта: «Предоставить естественную неблокирующую, событийно-ориентированную инфраструктуру для написания программ с высокой конкурентностью» (с) Ryan Dahl «to provide a purely evented, non-blocking infrastructure to script highly concurrent programs»
NodeJS NodeJS – серверная JavaScript платформа –Использует Google V8 (Chromium: Google Chrome, Chrome OS, etc.) –Превращает V8 в мощную машину для серверных приложений –Сливается в гармонии с философией JavaScript –Молодой, но живой Event loop - неблокирующий ввод/вывод –Все выполняется параллельно, кроме вашего кода
Для чего подходит NodeJS Много I/O + большая конкурентность –RIA «богатые» приложения –API –Proxy Realtime –Чаты –Онлайн игры –Трансляции –Publish/Subscribe
Event loop Это цикл (libev) Это один процесс, один поток Выполняет одну задачу на один момент времени Ожидает события параллельно (libeio, pooled threads) В каждой итерации последовательно запускает функции-колбэки из трех разных очередей: 1.nextTick функции 2.Таймеры (setTimeout, setInterval) 3.Сигналы ввода/вывода (libeio) Завершает работу, если все очереди пусты
Время CPU – процессорное время –Интерпретация кода –Бизнес-логика приложения, алгоритмы –Рендеринг шаблонов I/O – время ввода/вывода –Запросы в базу данных (network) –Чтение файлов –Чтение кэша I/O CPU
callback I/O CPU занятосвободно Blocking I/O Event Loop монолит А что если во время ожидания I/O заниматься друмиги полезными делами?
I/O Event loop Примерно так выглядит более реальный запрос: CPU+I/O
I/O CPU callback I/O CPU Event loop Примерно так выглядит более реальный запрос:
Event loop Примерно так выглядит более реальный запрос: I/O CPU Свободно для других задач callback I/O CPU
Event Loop mysql.query(SELECT count(*) FROM users, function(err, count) { console.log(There are %d users in db, count); }) console.log(Hello, ); Отправка запроса в БД
Event Loop mysql.query(SELECT count(*) FROM users, function(err, count) { console.log(There are %d users in db, count); }) console.log(Hello, ); Hello, Ожидание ответа БД...
Event Loop mysql.query(SELECT count(*) FROM users, function(err, count) { console.log(There are %d users in db, count); }) console.log(Hello, ); Hello, There are users in db Пришел ответ из БД
Первый запрос Blocking I/O, 1 процесс
Второй запрос, после 10ms ожидает выполнения первого Blocking I/O, 1 процесс
Третий запрос, после 50ms ожидает выполнения первого и второго Blocking I/O, 1 процесс
Event loop, 1 процесс
Blocking I/O, 1 процесс Event loop, 1 процесс Время: 0ms Пришел первый запрос, Запрашиваем I/O, освобождаемся, Ждем других запросов Время: 0ms Пришел первый запрос, Запрашиваем I/O, освобождаемся, Ждем других запросов
Blocking I/O, 1 процесс Event loop, 1 процесс Время: 10ms Пришел второй запрос, Запрашиваем I/O, освобождаемся, Ждем других запросов Время: 10ms Пришел второй запрос, Запрашиваем I/O, освобождаемся, Ждем других запросов
Blocking I/O, 1 процесс Event loop, 1 процесс Время: 50ms Пришел третий запрос, Запрашиваем I/O, освобождаемся, Ждем других запросов Время: 50ms Пришел третий запрос, Запрашиваем I/O, освобождаемся, Ждем других запросов
Blocking I/O, 1 процесс Event loop, 1 процесс Время: 70ms Пришел ответ на I/O первого запроса, запускаем callback1 Время: 70ms Пришел ответ на I/O первого запроса, запускаем callback1
Blocking I/O, 1 процесс Event loop, 1 процесс Время: 80ms Пришел ответ на I/O второго запроса, callback2 ожидает своей очереди Время: 80ms Пришел ответ на I/O второго запроса, callback2 ожидает своей очереди
Blocking I/O, 1 процесс Event loop, 1 процесс Время: 100ms сallback1 завершился, запускаем callback2 Время: 100ms сallback1 завершился, запускаем callback2
Blocking I/O, 1 процесс Event loop, 1 процесс Время: 120ms Пришел ответ на I/O третьего запроса, callback3 ожидает своей очереди Время: 120ms Пришел ответ на I/O третьего запроса, callback3 ожидает своей очереди
Blocking I/O, 1 процесс Event loop, 1 процесс Время: 130ms сallback2 завершился, запускаем callback3 Время: 130ms сallback2 завершился, запускаем callback3
Blocking I/O, 1 процесс Event loop, 1 процесс Время: 160ms callback3 завершился Ждем других запросов Время: 160ms callback3 завершился Ждем других запросов
Время CPU vs I/O RIA трэнд
Приложение my_app.js library / my_module.js node_modules / express sync narrow
Приложение my_app.js library / my_module.js node_modules / express sync narrow require.paths.unshift(./library) var MyModule = require(my_module) var Sync = require(sync)
Приложение my_app.js library / my_module.js node_modules / express sync narrow var MyModule = function() { // … } module.exports = MyModule
Приложение my_app.js library / my_module.js node_modules / express sync narrow require.paths.unshift(./library) var MyModule = require(my_module) var Sync = require(sync) console.log(my module:, MyModule);+
Приложение my_app.js library / my_module.js node_modules / express sync narrow $ node my_app.js my module: function (){}
Приложение my_app.js library / my_module.js node_modules / express sync narrow mongoose $ npm install mongoose
HTTP var http = require(http); http.createServer(function(req, res){ res.writeHead(200, { Content-Type : text/plain }); res.end(Hello, World\n); }).listen(3080) console.log(Server running at Подключаем HTTP модуль
HTTP var http = require(http); http.createServer(function(req, res){ res.writeHead(200, { Content-Type : text/plain }); res.end(Hello, World\n); }).listen(3080) console.log(Server running at Создаем HTTP сервер
HTTP var http = require(http); http.createServer(function(req, res){ res.writeHead(200, { Content-Type : text/plain }); res.end(Hello, World\n); }).listen(3080) console.log(Server running at «вешаем» сервер на 3080 порт
HTTP var http = require(http); http.createServer(function(req, res){ res.writeHead(200, { Content-Type : text/plain }); res.end(Hello, World\n); }).listen(3080) console.log(Server running at Сервер создан, выводим сообщение Сервер создан, выводим сообщение
HTTP var http = require(http); http.createServer(function(req, res){ res.writeHead(200, { Content-Type : text/plain }); res.end(Hello, World\n); }).listen(3080) console.log(Server running at Функция будет вызвана индивидуально для каждого запроса
HTTP var http = require(http); http.createServer(function(req, res){ res.writeHead(200, { Content-Type : text/plain }); res.end(Hello, World\n); }).listen(3080) console.log(Server running at Отправляем HTTP заголовок
HTTP var http = require(http); http.createServer(function(req, res){ res.writeHead(200, { Content-Type : text/plain }); res.end(Hello, World\n); }).listen(3080) console.log(Server running at Отправляем HTTP тело и закрываем сокет
HTTP var http = require(http); http.createServer(function(req, res){ res.writeHead(200, { Content-Type : text/plain }); res.end(Hello, World\n); }).listen(3080) console.log(Server running at $ node my_app.js
HTTP var http = require(http); http.createServer(function(req, res){ res.writeHead(200, { Content-Type : text/plain }); res.end(Hello, World\n); }).listen(3080) console.log(Server running at $ node my_app.js Server running at $ node my_app.js Server running at
HTTP var http = require(http); http.createServer(function(req, res){ res.writeHead(200, { Content-Type : text/plain }); res.end(Hello, World\n); }).listen(3080) console.log(Server running at $ node my_app.js Server running at $ node my_app.js Server running at $ curl
HTTP var http = require(http); http.createServer(function(req, res){ res.writeHead(200, { Content-Type : text/plain }); res.end(Hello, World\n); }).listen(3080) console.log(Server running at $ node my_app.js Server running at $ node my_app.js Server running at $ curl Hello, World $ $ curl Hello, World $
HTTP var http = require(http); http.createServer(function(req, res){ setTimeout(function(){ res.end(World!\n); }, 1000); res.writeHead(200, { Content-Type : text/plain }); res.write(Hello,\n); }).listen(3080) console.log(Server running at Hello сразу, World – через 1 сек
HTTP var http = require(http); http.createServer(function(req, res){ setTimeout(function(){ res.end(World!\n); }, 1000); res.writeHead(200, { Content-Type : text/plain }); res.write(Hello,\n); }).listen(3080) console.log(Server running at $ node my_app.js Server running at $ node my_app.js Server running at $ curl Hello, $ curl Hello,
HTTP var http = require(http); http.createServer(function(req, res){ setTimeout(function(){ res.end(World!\n); }, 1000); res.writeHead(200, { Content-Type : text/plain }); res.write(Hello,\n); }).listen(3080) console.log(Server running at $ node my_app.js Server running at $ node my_app.js Server running at $ curl Hello, World! $ $ curl Hello, World! $ Через секунду
HTTP var http = require(http); var i = 0; http.createServer(function(req, res){ res.writeHead(200, { Content-Type : text/plain }); res.end(i = + i + \n); i++; }).listen(3080) console.log(Server running at Итератор, общий для всех запросов – javascript замыкание (closure)
HTTP var http = require(http); var i = 0; http.createServer(function(req, res){ res.writeHead(200, { Content-Type : text/plain }); res.end(i = + i + \n); i++; }).listen(3080) console.log(Server running at $ node my_app.js Server running at $ node my_app.js Server running at $ curl i = 0 $ $ curl i = 0 $
HTTP var http = require(http); var i = 0; http.createServer(function(req, res){ res.writeHead(200, { Content-Type : text/plain }); res.end(i = + i + \n); i++; }).listen(3080) console.log(Server running at $ node my_app.js Server running at $ node my_app.js Server running at $ curl i = 0 $ curl i = 1 $ $ curl i = 0 $ curl i = 1 $
Callback-driven парадигма Ломает мозг Синтаксический шум С ложно выполнить ряд действий в определенной последовательности Необходимость вручную «пробрасывать» ошибки Бесконечная индентация – aka «спагетти код»
Callback-driven парадигма function asyncFunction(arg1, arg2, argN, callback) { } Неблокирующая функция принимает callback последним аргументом
Callback-driven парадигма function asyncFunction(arg1, arg2, argN, callback) { } function callback(err, result1, result2, resultN) { } Неблокирующая функция принимает callback последним аргументом Callback принимает ошибку первым аргументом, остальные – результат
Callback-driven парадигма function sum(a, b) { if (a > b) { throw new Error('a cannot be greater than b'); } return a + b; }
Callback-driven парадигма function sum(a, b) { if (a > b) { throw new Error('a cannot be greater than b'); } return a + b; } function asyncSum(a, b, callback) { if (a > b) { return callback(new Error('a cannot be greater than b')); } callback(null, a + b); }
Callback-driven парадигма function sum(a, b) { if (a > b) { throw new Error('a cannot be greater than b'); } return a + b; } function asyncSum(a, b, callback) { if (a > b) { return callback(new Error('a cannot be greater than b')); } callback(null, a + b); } throw!
Callback-driven парадигма try { var result = sum(2, 3); console.log('result = %d', result); } catch (err) { console.error(err); }
Callback-driven парадигма try { var result = sum(2, 3); console.log('result = %d', result); } catch (err) { console.error(err); } asyncSum(2, 3, function(err, result){ if (err) return console.error(err); console.log('result = %d', result); })
Callback-driven парадигма function getUser(id, callback) { }
Callback-driven парадигма function getUser(id, callback) { } getUser(1234, function(err, user) { if (err) return console.error(err); console.log(user:, user); })
Callback-driven парадигма function getUser(id, callback) { readConfig(config.json, function(err, config){ if (err) return callback(err); }) }
Callback-driven парадигма function getUser(id, callback) { readConfig(config.json, function(err, config){ if (err) return callback(err); dbConnect(config.host, function(err, db){ if (err) return callback(err); }) }
Callback-driven парадигма function getUser(id, callback) { readConfig(config.json, function(err, config){ if (err) return callback(err); dbConnect(config.host, function(err, db){ if (err) return callback(err); db.getUser(id, function(err, user){ if (err) return callback(err); callback(null, user); }) }
Callback-driven парадигма function getUser(id, callback) { readConfig(config.json, function(err, config){ if (err) return callback(err); afterReadConfig(id, config, callback); }) } function afterReadConfig(id, config, callback) { dbConnect(config.host, function(err, db){ if (err) return callback(err); db.getUser(id, function(err, user){ … }) }
Callback-driven парадигма function getUser(id, callback) { readConfig(config.json, function(err, config){ if (err) return callback(err); afterReadConfig(id, config, callback); }) } function afterReadConfig(id, config, callback) { dbConnect(config.host, function(err, db){ if (err) return callback(err); afterDbConnect(id, db, callback); }) } function afterDbConnect(id, db, callback) … ++++
Callback-driven парадигма Error: User not found at afterDbConnect (/path/to/script.js:24:14) at /path/to/script.js:20:9 at dbConnect (/path/to/script.js:7:5) at afterReadConfig (/path/to/script.js:18:5) at /path/to/script.js:13:9 at readConfig (/path/to/script.js:3:5) at getUser (/path/to/script.js:11:5) at Object. (/path/to/script.js:28:1) at Module._compile (module.js:404:26) at Object..js (module.js:410:10)
Callback-driven парадигма Error: User not found at afterDbConnect (/path/to/script.js:24:14) at /path/to/script.js:20:9 at dbConnect (/path/to/script.js:7:5) at afterReadConfig (/path/to/script.js:18:5) at /path/to/script.js:13:9 at readConfig (/path/to/script.js:3:5) at getUser (/path/to/script.js:11:5) at Object. (/path/to/script.js:28:1) at Module._compile (module.js:404:26) at Object..js (module.js:410:10)
Callback-driven парадигма function getUser(id, callback) { readConfig(config.json, function(err, config){ if (err) return callback(err); afterReadConfig(id, config, callback); }) } function afterReadConfig(id, config, callback) { dbConnect(config.host, function(err, db){ if (err) return callback(err); afterDbConnect(id, db, callback); }) } function afterDbConnect(id, db, callback) … Синтаксический шум – плата за Evented I/O
Callback-driven парадигма function getUser(id, callback) { readConfig(config.json, function(err, config){ if (err) return callback(err); afterReadConfig(id, config, callback); }) } function afterReadConfig(id, config, callback) { dbConnect(config.host, function(err, db){ if (err) return callback(err); afterDbConnect(id, db, callback); }) } function afterDbConnect(id, db, callback) … Синтаксический шум – плата за Evented I/O PROFIT
node-sync Function.prototype.sync = function(context, arguments…) Использует сопрограммы (coroutines) с++ Основан на node-fibers Позволяет писать синхронно на nodejs 0 ctave/node-sync laverdet /node-fibers
var Sync = require(sync); function getUser(id, callback) { Sync(function(){ var config = readConfig.sync(null, config.json); var db = dbConnect.sync(null, config.host); var user = db.getUser.sync(db, id); return user; }, callback) } Запускаем новое «волокно» (Fiber) node-sync
var Sync = require(sync); function getUser(id, callback) { Sync(function(){ var config = readConfig.sync(null, config.json); var db = dbConnect.sync(null, config.host); var user = db.getUser.sync(db, id); return user; }, callback) } «Волокно» вернет значение или ошибку в callback node-sync
var Sync = require(sync); function getUser(id, callback) { Sync(function(){ var config = readConfig.sync(null, config.json); var db = dbConnect.sync(null, config.host); var user = db.getUser.sync(db, id); return user; }, callback) } Функция readConfig вызывается синхронно и возвращает значение node-sync
var Sync = require(sync); function getUser(id) { var config = readConfig.sync(null, config.json); var db = dbConnect.sync(null, config.host); var user = db.getUser.sync(db, id); return user; }.async() То же самое, только проще (коллбэка нет) То же самое, только проще (коллбэка нет) node-sync
var Sync = require(sync); function getUser(id) { var config = readConfig.sync(null, config.json); var db = dbConnect.sync(null, config.host); var user = db.getUser.sync(db, id); return user; }.async() node-sync getUser(1234, function(err, user) { if (err) return console.error(err); console.log(user:, user); })
var Sync = require(sync); function getUser(id) { var config = readConfig.sync(null, config.json); var db = dbConnect.sync(null, config.host); throw new Error(something went wrong); return user; }.async() node-sync getUser(1234, function(err, user) { if (err) return console.error(err); console.log(user:, user); })
var Sync = require(sync); function getUser(id) { var config = readConfig.sync(null, config.json); var db = dbConnect.sync(null, config.host); var user = db.getUser.future(db, id); var friends = db.getUserFriends.future(db, id); return { user : user.result, friends : friends.result }; }.async() node-sync getUser и getUserFriends выполняются параллельно getUser и getUserFriends выполняются параллельно
var Sync = require(sync); function getUser(id) { var config = readConfig.sync(null, config.json); var db = dbConnect.sync(null, config.host); var user = db.getUser.future(db, id); db.getUserFriends(id, friends = new Sync.Future()); return { user : user.result, friends : friends.result }; }.async() node-sync другой способ получения «тикета» future
Callback-driven парадигма $pages = $db->fetchRows(SELECT * FROM pages); foreach ($pages as $page) { $contents = fetchUrl($page->url); }
Callback-driven парадигма $pages = $db->fetchRows(SELECT * FROM pages); foreach ($pages as $page) { $contents = fetchUrl($page->url); } OK
Callback-driven парадигма $pages = $db->fetchRows(SELECT * FROM pages); foreach ($pages as $page) { $contents = fetchUrl($page->url); } db.fetchRows(SELECT * FROM pages, function(err, pages){ pages.forEach(function(page){ fetchUrl(page.url, function(err, contents) { }) }); OK
Callback-driven парадигма $pages = $db->fetchRows(SELECT * FROM pages); foreach ($pages as $page) { $contents = fetchUrl($page->url); } db.fetchRows(SELECT * FROM pages, function(err, pages){ pages.forEach(function(page){ fetchUrl(page.url, function(err, contents) { }) }); x 100,000 OK
Callback-driven парадигма db.fetchRows(SELECT * FROM pages, function(err, pages){ var narrow = new Narrow(10, function(page, callback){ fetchUrl(page.url, function(err, contents) { }) narrow.pushAll(pages); });
Callback-driven парадигма db.fetchRows(SELECT * FROM pages, function(err, pages){ var narrow = new Narrow(10, function(page, callback){ fetchUrl(page.url, function(err, contents) { }) narrow.pushAll(pages); }); x10 - OK x 100,000 - OK
node-sync - использует сопрограммы (coroutines) streamline.js – транслирует код node-async – целый инструментарий для асинхронного программирования Ваша собственная flow-control библиотека :) Callback-driven решения? 0 ctave/node-sync
Масштабирование Nodejs – is just node (c) Ryan Dahl
Масштабирование Nodejs – is just node (c) Ryan Dahl 1 ядро CPU = 1 nodejs процесс
Масштабирование node-cluster Расширяемый Поддержка POSIX сигналов «Горячая» перезагрузка (zero-downtime) «Аккуратное» завершение (graceful shutdown) Автоматом перезапускает мертвые процессы Не оставляет «зомби» Автоматом определяет соличество ядер CPU Поддержка REPL Статистика PID файлы Логи
HTTP Cluster var http = require(http), cluster = require(cluster); var server = http.createServer(function(req, res){ res.writeHead(200, { Content-Type : text/plain }); res.end(Hello from + process.pid + \n); }); cluster(server).listen(3080); console.log(Server at (pid: %d), process.pid); Подключаем модуль cluster
HTTP Cluster var http = require(http), cluster = require(cluster); var server = http.createServer(function(req, res){ res.writeHead(200, { Content-Type : text/plain }); res.end(Hello from + process.pid + \n); }); cluster(server).listen(3080); console.log(Server at (pid: %d), process.pid); Оборачиваем http сервер в кластер и «вешаем» на 3080 порт Оборачиваем http сервер в кластер и «вешаем» на 3080 порт
HTTP Cluster var http = require(http), cluster = require(cluster); var server = http.createServer(function(req, res){ res.writeHead(200, { Content-Type : text/plain }); res.end(Hello from + process.pid + \n); }); cluster(server).listen(3080); console.log(Server at (pid: %d), process.pid); Дополнительно выводим PID
HTTP Cluster var http = require(http), cluster = require(cluster); var server = http.createServer(function(req, res){ res.writeHead(200, { Content-Type : text/plain }); res.end(Hello from + process.pid + \n); }); cluster(server).listen(3080); console.log(Server at (pid: %d), process.pid); $ node my_app.js Server at (pid: 9254) Server at (pid: 9255) Server at (pid: 9256) $ node my_app.js Server at (pid: 9254) Server at (pid: 9255) Server at (pid: 9256)
HTTP Cluster var http = require(http), cluster = require(cluster); var server = http.createServer(function(req, res){ res.writeHead(200, { Content-Type : text/plain }); res.end(Hello from + process.pid + \n); }); cluster(server).listen(3080); console.log(Server at (pid: %d), process.pid); $ node my_app.js Server at (pid: 9254) Server at (pid: 9255) Server at (pid: 9256) $ node my_app.js Server at (pid: 9254) Server at (pid: 9255) Server at (pid: 9256) $ curl Hello from 9255 $ $ curl Hello from 9255 $
HTTP Cluster var http = require(http), cluster = require(cluster); var server = http.createServer(function(req, res){ res.writeHead(200, { Content-Type : text/plain }); res.end(Hello from + process.pid + \n); }); cluster(server).listen(3080); console.log(Server at (pid: %d), process.pid); $ node my_app.js Server at (pid: 9254) Server at (pid: 9255) Server at (pid: 9256) $ node my_app.js Server at (pid: 9254) Server at (pid: 9255) Server at (pid: 9256) $ curl Hello from 9255 $ curl Hello from 9256 $ $ curl Hello from 9255 $ curl Hello from 9256 $
NodeJS + Много I/O Много Запросов + Event Loop = PROFIT
Юра Богданов 0 ctave Github: yuriybogdanov Twitter: bogdanov About.me: in/yuriybogdanov LinkedIn: Спасибо за внимание.