Задача — поднять сервер и обрабатывать несколько url, например http://127.0.0.1/habr и http://127.0.0.1/habrahabr. Сервер должен обрабатывать исключения, а также проект рассчитан на высокую нагрузку.
Цель статьи – разобраться, как создать высоконагруженное приложение, удобное для командной работы и понятное для новичков.
Первое что необходимо сделать, это поднять сервер на Node.js
var http = require('http');
var file = new static.Server('.');
http.createServer(function(req, res) {
file.serve(req, res);
}).listen(80);
Проблема в том, что сервер работает только на одном процессе системы. Немного переработаем код, добавив кластеризацию, для этого используем стандартный модуль cluster:
const cluster = require('cluster');
const http = require('http');
const domain = require('domain');
const numCPUs = require('os').cpus().length;
if (cluster.isMaster) {
for (var i = 0; i < numCPUs; i++) {
cluster.fork();
}
cluster.on('exit', function(worker, code, signal){
console.log('worker ' + worker.process.pid +' died');
cluster.fork();
});
cluster.on('online', function(worker) {
console.log('Worker ' + worker.process.pid + ' is online');
});
} else {
http.createServer(function(req, res){
// Создаем домен
var d = domain.create();
// Вешаем обработчик ошибки, который вернет 500й статус и текст проблемы
d.on('error', function(err) {
res.statusCode = 500;
res.setHeader('content-type', 'text/plain');
res.end('Ошибка!\n'+ err.stack);
});
// Добавляем наши переменные, которые тоже могут сгенерировать ошибки самостоятельно
d.add(req);
d.add(res);
// Запускаем потенциально опасный код внутри домена
d.run(function () {
var route_json = require('./application/route.json');
if( route_json[req.url] !== undefined){//Пользователь вручную задал контроллер
console.log(route_json[req.url].controller);
}else{
url = urlapi.parse(decodeURI(req.url), true);//парсим url
url_arr = url.pathname.slice(1).split('/');//Преобразуем url в массив
}
res.end('hello world');
});
}).listen(3031).on('connection', function(socket) {
socket.setNoDelay(); // Отключаем алгоритм Нагла.
});
}
С основным кодом сервера мы разобрались, теперь у нас есть сервер с асинхронным обработчиком исключений, кластеризацией и обработкой url. Так как мы используем парадигму MVC, то за эталон возьмем codeigniter. Структура файлов выглядит следующим образом:
Описание структуры:
- app.js — главный код приложения
- core — должна содержать фалы ядра приложения, библиотеки, модули и т.п.
- aplication — папка приложения
- controller- папка c контроллерами
- model- папка c моделями
- model- папка c представлениями
- route.json — пользовательский роутинг
Требуется обработка кода контроллера. Для решения данной задачи, существует несколько методов:
- require — в данной публикации не рассматривается.
- eval — не рекомендованный метод, по причине того что он работает в несколько раз медленней чем vm, к тому же это не самый безопасный метод
- vm — это виртуальная машина, в котором код компилируется в песочнице. Плюсы данного метода в том, что в случае утечек или проблем с работой, можно уничтожать не весь процесс, а только процесс в песочнице, но это уже отдельная статься.
Из документации видно, что vm выполняется в контексте, можно запустить в новом контексте, либо в текущем. Наиболее правильным вариантом решения будет выполнять код в новом контексте.
Полный код примера:
const cluster = require('cluster');
const http = require('http');
const domain = require('domain');
const numCPUs = require('os').cpus().length;
if (cluster.isMaster) {
for (var i = 0; i < numCPUs; i++) {
cluster.fork();
}
cluster.on('exit', function(worker, code, signal){
console.log('worker ' + worker.process.pid +' died');
cluster.fork();
});
cluster.on('online', function(worker) {
console.log('Worker ' + worker.process.pid + ' is online');
});
} else {
http.createServer(function(req, res){
// Создаем домен
var d = domain.create();
// Вешаем обработчик ошибки, который вернет 500й статус и текст проблемы
d.on('error', function(err) {
res.statusCode = 500;
res.setHeader('content-type', 'text/plain');
res.end('Ошибка!\n'+ err.stack);
});
// Добавляем наши переменные, которые тоже могут сгенерировать ошибки самостоятельно
d.add(req);
d.add(res);
// Запускаем потенциально опасный код внутри домена
d.run(function () {
var route_json = require('./application/route.json');
var fs = require('fs');//библиотека для работы с файлами
if( route_json[req.url] !== undefined){//Пользователь вручную задал контроллер
var path = './application/controller/'+route_json[req.url].controller+'.js';
}else{
var urlapi = require('url');//Подключаем библиотеку для парсинга url
var url = urlapi.parse(decodeURI(req.url), true);//парсим url
var url_arr = url.pathname.slice(1).split('/');//Преобразуем url в массив
var path = './application/controller/'+url_arr[0]+'.js';
}
//Читаем код контроллера из папки
fs.readFile(path, 'utf8',
function(err, code) {
var vm = require('vm');
var timestart = parseInt(new Date().getTime());
var pid = cluster.worker.process.pid;
var context = {
// -- подключаемые объекты к контексту
pid:pid,
res:res,
req:req,
timestart:timestart,
require: require,
console: console
};
var vmContext = vm.createContext(context);
var script = vm.Script(code);
script.runInNewContext(vmContext);
});
});
}).listen(3031).on('connection', function(socket) {
socket.setNoDelay(); // Отключаем алгоритм Нагла.
});
}
Пример кода контроллера:
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Headers', 'origin, content-type, accept');
res.setHeader("Cache-Control", "no-cache, must-revalidate");
res.writeHead(200, {"Content-Type": "text/plain"});
res.write('CONTROLLER RUN');
res.end();
Таким образом, у нас есть каркас приложения, который загружает и выполняет код без перезагрузки основного приложения (исполняет код в песочнице).
Данное решение отлично подойдет для командной разработки больших приложений. В данной статье мы рассмотрели cluster и vm, домены в Node.js.
Ссылки:
Комментарии (26)
zxcabs
01.11.2016 20:13+6Серьезно на каждый запрос читать файл с диска и запускать его в отдельном контексте? Кто то на пхп перепрограммировал и пытается принести это ноду.
Более того, открываем доку по апи ноды, раздел про домены, и видим что они выпиливаются и не рекомендуются к использованию.keenondrums
02.11.2016 14:00+1Я так понимаю, человек пытался реализовать хот своп контроллера. Понятно что, но не понятно зачем. Из исходного постулата о том, что надо «сервер перезапускать, работают несколько человек и т.д.» складывается впечатление, что предлагается разрабатывать сразу на продакшн сервере всем без всяких VCS, тестирования и прочей ерунды.
P.S. Автор, для автоматического перезапуска сервера во время разработки предлагаю поглядеть в сторону Nodemonpan-alexey
02.11.2016 14:04-4Тут не говориться что так и нужно делать. Но про возможности ноды, и PHP шнику удомно.
Alexeyco
03.11.2016 09:43+1Так, давайте не будем бросать тень на всех PHP-разработчиков. Нам НЕ удобно разрабатывать на общем окружении.
rumkin
01.11.2016 23:08+1Новый велосипед засчитан, но лучше так не делать. Начните разрабатывать через тесты и забудете о перезапуске и ручном тестировании в браузере.
Suvitruf
02.11.2016 06:13Есть ли смысл в такой кластеризации?
Мы всегда такое решали просто запуском нескольких инстансов. Скажем, если у нас 8 ядер, то можно запустить под supervisord'ом 8 инстансов сервиса. Он нам их и переподнимать будет в случае чего.
mayorovp
02.11.2016 07:40+1Нельзя внутрь vm передавать require. Потому что он будет загружать модули за пределами vm — и магии с горячим редактированием кода не произойдет.
Armleo
02.11.2016 14:10Гайд 'Как не писать код в node.js'
Почему?
1) Domain is deprecated
2) Почему не использовать express?
3) Модуль кластера дает возможность делать тоже самое что вы тут делаете домейном…
4) Что делать если сервер упадет?
5) Как насчет Hot-Patching?
6) Чем не устроил require?
Надо будет написать статью мне про то как надо…pan-alexey
02.11.2016 14:16Буду ждать, статью как надо, а пока по порядку
1 раз уж домены есть, то пускай будут.
2 концепция была именно хотспот контроллера
3 кластером можно сделать, но домены позволяют выводить строку, где произошло исключение
4 если сервер упадет, нужно самому это додумать, демонтировать и т.п.
5 не скажу
6 requier не использовал, т.к. хотел рассказать про vm.Armleo
03.11.2016 09:252 Извините за (возможно) ламерский вопрос. Что такой ХотСпот Контроллер?
3 Неправильно выразился С помощью вот этого
4 Речь про то что если сервер упадет из-за ошибки вне домейна то cервер НЕ ответит 503 клиентом и клиенты будут ждать ответа от этого worker-a.
6 Аргумент сильный :)
pan-alexey
03.11.2016 12:532 hot swap, прошу прощения, пишу с мобилки
3 через процесс можно, но постоянно плодить процессы, это не мой выбор.
4 можно продумать, придумать алгоритм, но это уже отдельная история…
6 никто не запрещает использовать requier, но мало где используют vm, хотел показать как его можно использоватьArmleo
03.11.2016 16:173 Извините использую кластеры вы плодите процессы… Я чего то не так понял?
В общем статью выложил жду модерации… Посмотрим что из этого получится :)
С раннего утра пишу статью… После первой части понял описывать есть что.pan-alexey
04.11.2016 14:16Спасибо, жду ссылку. Буду рад для себя узнать что-то новое. Можно отправить мне ссылку в ЛС?
lair
Простите, какую задачу?
Вам удалось создать высоконагруженное приложение?
Почему?