Построение MVC приложения на Node.js с кластертзацией и исполнением кода в песочнице

в 15:30, , рубрики: asynchronous, domain, framework, javascript, mvc, node.js, vm

Добрый день, уважаемые читатели ! Данная статья рассчитана на новичков, которые только открывают мир 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

Автор: pan-alexey

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js