Добрый день, уважаемые читатели Хабра! Данная статья рассчитана на новичков, которые только открывают мир JS, коим являюсь и я. В процессе изучения и проектирования сервера на Node.js разработчик постоянно сталкивается с необходимостью перезагрузки приложения. А в случае, если над проектом работает несколько человек, получаем довольную сложную задачу.

Задача — поднять сервер и обрабатывать несколько 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. Структура файлов выглядит следующим образом:

image

Описание структуры:

  • 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.

Ссылки:

  1. learn.javascript.ru/ajax-nodejs
  2. nodejs.org/api/cluster.html
  3. ru.wikipedia.org/wiki/Model-View-Controller
  4. code-igniter.ru/user_guide/libraries/uri.html
  5. ru.wikipedia.org/wiki/JSON
  6. nodejs.org/api/domain.html
  7. nodejs.org/api/vm.html
  8. https://github.com/pan-alexey/nodeigniter — исходники на github
Поделиться с друзьями
-->

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


  1. lair
    01.11.2016 18:34
    +7

    В процессе изучения и проектирования сервера на Node.js разработчик постоянно сталкивается с необходимостью перезагрузки приложения. А в случае, если над проектом работает несколько человек, получаем довольную сложную задачу.

    Простите, какую задачу?


    Цель статьи – разобраться, как создать высоконагруженное приложение

    Вам удалось создать высоконагруженное приложение?


    Данное решение отлично подойдет для командной разработки больших приложений

    Почему?


  1. inook
    01.11.2016 18:47
    +9

    НаPHPешили!


  1. Diaskhan
    01.11.2016 19:04
    +5

    Какая же все таки вырвиглазная асинхронная лапша.


  1. zxcabs
    01.11.2016 20:13
    +6

    Серьезно на каждый запрос читать файл с диска и запускать его в отдельном контексте? Кто то на пхп перепрограммировал и пытается принести это ноду.
    Более того, открываем доку по апи ноды, раздел про домены, и видим что они выпиливаются и не рекомендуются к использованию.


    1. keenondrums
      02.11.2016 14:00
      +1

      Я так понимаю, человек пытался реализовать хот своп контроллера. Понятно что, но не понятно зачем. Из исходного постулата о том, что надо «сервер перезапускать, работают несколько человек и т.д.» складывается впечатление, что предлагается разрабатывать сразу на продакшн сервере всем без всяких VCS, тестирования и прочей ерунды.
      P.S. Автор, для автоматического перезапуска сервера во время разработки предлагаю поглядеть в сторону Nodemon


      1. pan-alexey
        02.11.2016 14:04
        -4

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


        1. Alexeyco
          03.11.2016 09:43
          +1

          Так, давайте не будем бросать тень на всех PHP-разработчиков. Нам НЕ удобно разрабатывать на общем окружении.


  1. 3axap4eHko
    01.11.2016 21:39
    +3

    domain is deprecated


  1. glukki
    01.11.2016 22:13
    +1

    Кажется, хабр стали использовать для размещения публикаций для галочки.


    1. funca
      01.11.2016 22:29
      +2

      К счастью, не все авторы рождаются тут сразу Ализарами. По-моему для первой публикации норм.


      1. pan-alexey
        02.11.2016 14:05

        Спасибо, буду расти


  1. rumkin
    01.11.2016 23:08
    +1

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


  1. 776166
    01.11.2016 23:19
    +1

    И большую нагрузку выдерживает?


  1. Suvitruf
    02.11.2016 06:13

    Есть ли смысл в такой кластеризации?

    Мы всегда такое решали просто запуском нескольких инстансов. Скажем, если у нас 8 ядер, то можно запустить под supervisord'ом 8 инстансов сервиса. Он нам их и переподнимать будет в случае чего.


    1. mayorovp
      02.11.2016 07:37

      Так тут точно то же самое делается.


      1. MasMaX
        02.11.2016 11:35
        +2

        Но с внешней стороны это делать правильнее. Самому процессу должно быть пофик сколько его братьев работает. Он делает свою работу, а за распределение по кластерам другая программа (например еще PM2 есть).


  1. mayorovp
    02.11.2016 07:40
    +1

    Нельзя внутрь vm передавать require. Потому что он будет загружать модули за пределами vm — и магии с горячим редактированием кода не произойдет.


    1. pan-alexey
      02.11.2016 13:58

      Все так и есть. Учту


  1. Armleo
    02.11.2016 14:10

    Гайд 'Как не писать код в node.js'
    Почему?
    1) Domain is deprecated
    2) Почему не использовать express?
    3) Модуль кластера дает возможность делать тоже самое что вы тут делаете домейном…
    4) Что делать если сервер упадет?
    5) Как насчет Hot-Patching?
    6) Чем не устроил require?
    Надо будет написать статью мне про то как надо…


    1. pan-alexey
      02.11.2016 14:16

      Буду ждать, статью как надо, а пока по порядку
      1 раз уж домены есть, то пускай будут.
      2 концепция была именно хотспот контроллера
      3 кластером можно сделать, но домены позволяют выводить строку, где произошло исключение
      4 если сервер упадет, нужно самому это додумать, демонтировать и т.п.
      5 не скажу
      6 requier не использовал, т.к. хотел рассказать про vm.


      1. Armleo
        03.11.2016 09:25

        2 Извините за (возможно) ламерский вопрос. Что такой ХотСпот Контроллер?
        3 Неправильно выразился С помощью вот этого
        4 Речь про то что если сервер упадет из-за ошибки вне домейна то cервер НЕ ответит 503 клиентом и клиенты будут ждать ответа от этого worker-a.
        6 Аргумент сильный :)


  1. pan-alexey
    03.11.2016 12:53

    2 hot swap, прошу прощения, пишу с мобилки
    3 через процесс можно, но постоянно плодить процессы, это не мой выбор.
    4 можно продумать, придумать алгоритм, но это уже отдельная история…
    6 никто не запрещает использовать requier, но мало где используют vm, хотел показать как его можно использовать


    1. Armleo
      03.11.2016 16:17

      3 Извините использую кластеры вы плодите процессы… Я чего то не так понял?


      В общем статью выложил жду модерации… Посмотрим что из этого получится :)
      С раннего утра пишу статью… После первой части понял описывать есть что.


      1. Armleo
        03.11.2016 19:14

        del


      1. pan-alexey
        04.11.2016 14:16

        Спасибо, жду ссылку. Буду рад для себя узнать что-то новое. Можно отправить мне ссылку в ЛС?


  1. railsfun
    05.11.2016 11:11

    В названии — «Кластеризацией». Спасибо.