… или как перейти с PHP + JavaScript на JavaScript + JavaScript


Идея реализовать проект на сервер-сайд JavaScript была уже давно. Проблема была в отсутствии подходящего серверного программного обеспечения. Существующие открытые проекты не устраивали по разным причинам. Устанавливать дополнительный модуль для Apache было не самой хорошей идеей, потому что производительность и оптимизация использования памяти при этом были бы не на высоте. С помощью jslibs можно настроить FastCGI, но очень не хотелось оставлять ни малейших шансов «502 Bad Gateway», проект ngx_http_js_module так и остался в зачаточной стадии, а ngxv8 недостаточно развит для реализации реальных приложений. Поэтому я решил сделать собственную реализацию серверного javascript. Причем постараться сразу запрограммировать всю базовую функциональность, чтобы можно было ее тестировать в условиях, близких к реальности.

В качестве основного веб-сервера было решено использовать nginx, в качестве «движка» javascript — TraceMonkey (javascript-движок из Mozilla Firefox, бывший SpiderMonkey), и написать модуль для nginx, который бы ��х «склеил». Ничего сложного, на первый взгляд, но очень хотелось иметь определенную функциональность (и это получилось!), чтобы можно было нормально работать дальше. Большинство идей заимствованы, кстати, из PHP.
  • Корректная работа в multi-thread условиях
  • Возможность выполнять скрипт, указанный в URL, а не настраивать отдельно скрипт-обработчик и функцию-обработчик для каждого location
  • Возможность вызывать include(), sleep(), alert() из скрипта, использовать __FILE__ и __LINE__
  • Ограничение памяти, выделяемой каждому скрипту, и времени работы скрипта
  • Защита открываемых скриптом файлов, указав в настройках список разрешенных папок. Примерно как open_basedir в PHP
  • Автоматический разбор данных запроса (параметров GET, POST, и, конечно же, cookies), чтобы не писать обработку данных на javascript
  • Поддержка запросов application/x-www-form-urlencoded и multipart/form-data
  • Поддержка basic-авторизации
  • Работа с базами данных (в первую очередь, MySQL и SQLite)
  • Работа с файловой системой: чтение и запись файлов, проверка существования файлов, и т.п.
  • Кэширование байт-кода скриптов, как, например, в eAccelerator
Плюс некоторые другие возможности (инструменты для шаблонизации, для создания конфигурационных файлов, и т.п.), но их в основной список я не включил — их позволяют сделать языковые возможности TraceMonkey.

От слов — к делу! Как скомпилировать и настроить, как протестировать и сравнить...

Глубоко в детали сборки не вдаюсь, иначе текст получится неимоверных размеров. Пользоват��ли, имеющие опыт «сборки» программ под Linux, будут чувствовать себя вполне комфортно, а для всех остальных могу предложить бинарную сборку и возможность пропустить процесс самостоятельной компиляции.

Понадобятся:
  • Linux
  • Компиляторы C и C++, autoconf 2.13
  • Исходники nginx
  • TraceMonkey из репозитория
  • Библиотека NSPR
  • Наш модуль
  • MySQL и SQLite (опционально) + средства разработки
Порядок сборки следующий.

Сначала NSPR последней версии (на момент написания — 4.8.2):

wget ftp://ftp.mozilla.org/pub/mozilla.org/nspr/releases/v4.8.2/src/nspr-4.8.2.tar.gz<br/>tar -xzf nspr-4.8.2.tar.gz<br/>cd nspr-4.8.2/mozilla/nsprpub<br/>./configure --prefix=/usr/local --with-pthreads<br/>make<br/>sudo make install<br/>
Затем TraceMonkey из репозитория (на момент написания, в репозитории версия 1.8.5, а скачать файл с исходниками можно только для 1.7.0):

hg clone http://hg.mozilla.org/tracemonkey/<br/>cd tracemonkey/js/src<br/>autoconf2.13<br/>./configure --prefix=/usr/local --with-nspr-prefix=/usr/local --with-system-nspr --with-pthreads --enable-threadsafe<br/>make<br/>sudo make install<br/>
Этот шаг может быть проблемным по нескольким причинам. Во-первых, не у всех есть команда hg. А во-вторых, из репозитория скачиваются все исходники Mozilla Firefox. Поэтому, первую строчку кода можно заменить и скачать исходники только TraceMonkey:

# hg clone http://hg.mozilla.org/tracemonkey/<br/>wget http://js.nnov.ru/files/tracemonkey-20100119.tar.gz<br/>tar -xzf tracemonkey-20100119.tar.gz<br/>
И затем уже скомпилировать.

Далее nginx (0.8.32) и модуль javascript:

wget http://sysoev.ru/nginx/nginx-0.8.32.tar.gz<br/>tar -xzf nginx-0.8.32.tar.gz<br/>cd nginx-0.8.32/src/http/modules<br/>svn co http://nginx-javascript.googlecode.com/svn/trunk/ javascript<br/>cd ../../..<br/>./configure --prefix=/usr/local/nginx-javascript --add-module=src/http/modules/javascript<br/>make<br/>sudo make install<br/>
Если все получилось — то переходим к настройке. Счастливые обладатели бинарной сборки обнаружат, что конфигурация уже выполнена, но лишний раз проверить не помешает. Достаточно выполнить следующие шаги:
  • Добавить в mime.types тип application/x-javascript-serverside для файлов, которые будут обрабатываться как javascript:
    # /usr/local/nginx-javascript/conf/mime.types<br/>types {<br/>    ...<br/>    application/x-javascript-serverside jsx;<br/>    ...<br/>}<br/>
    Расширение .jsx выбрано вместо стандартного .js, чтобы сервер не обрабатывал обычные java-скрипты как серверные
  • Разрешить в разделе location / файла nginx.conf обработку javascript. Заодно сменим номер порта, на котором будет работать сервер:
    # /usr/local/nginx-javascript/conf/nginx.conf<br/>...<br/>    server {<br/>        listen 8081;<br/>        ...<br/>        location / {<br/>            ...<br/>            javascript on;<br/>            ...<br/>        }<br/>    }<br/>...<br/>
  • Запустить nginx:
    /usr/local/nginx-javascript/sbin/nginx<br/>
  • Создать тестовый скрипт hello.jsx:
    // /usr/local/nginx-javascript/html/hello.jsx<br/>print("Hello, people!");<br/>
  • Проверить, что hello.jsx выглядит в браузере как надо:
    curl http://localhost:8081/hello.jsx<br/>
Добившись того, что сервер с javascript заработал, мне стало интересно, насколько выгоднее такое решение, ��ем стандартное Apache + PHP. Так как вопросы внутренней оптимизации TraceMonkey и PHP меня волновали несколько меньше (например, какой интерпретатор быстрее выполняет цикл из миллиона шагов? Подозреваю, что разница небольшая), то тестировался, в первую очередь, скрипт «Hello, people!».

В сравнении участвовали:
  • Apache/2.2.14 (prefork) + PHP/5.2.12 (модуль)
  • nginx/0.8.32 (1 рабочий процесс) + javascript
  • nginx/0.8.32 (8 рабочих процессов) + javascript
Среда тестирования — 4-ядерный Xeon с 2ГБ оперативной памяти и Debian Etch. Весь трафик локальный. В подробности «железа» не вдаюсь, в детали конфигурации тоже — настройки более или менее стандартные.

Сначала цикл тестов из 1000 запросов один за другим:

# Apache 2.2.14 (prefork) + PHP 5.2.12 (module)<br/>ab -n 1000 http://localhost:8085/hello.php<br/>Time per request: 5.278 [ms] (mean, across all concurrent requests)<br/># nginx (1 worker) + javascript<br/>ab -n 1000 http://localhost:8081/hello.jsx<br/>Time per request: 1.298 [ms] (mean, across all concurrent requests)<br/># nginx (8 workers) + javascript<br/>ab -n 1000 http://localhost:8088/hello.jsx<br/>Time per request: 1.322 [ms] (mean, across all concurrent requests)<br/>
Теперь цикл тестов из 1000 запросов при создании 100 одновременных подключений:

# Apache 2.2 (prefork) + PHP 5.2 (module)<br/>ab -n 1000 -c 100 http://localhost:8085/hello.php<br/>Time per request: 1.648 [ms] (mean, across all concurrent requests)<br/># nginx (1 worker) + javascript<br/>ab -n 1000 -c 100 http://localhost:8081/hello.jsx<br/>Time per request: 1.277 [ms] (mean, across all concurrent requests)<br/># nginx (8 workers) + javascript<br/>ab -n 1000 -c 100 http://localhost:8088/hello.jsx<br/>Time per request: 0.544 [ms] (mean, across all concurrent requests)<br/>
Выводы из тестирования:
  • Если запросы к серверу идут последовательно, один за другим, nginx+javascript работает значительно быстрее (у нас примерно в 3 раза). При этом nginx с одним рабочим процессом работает даже чуть-чуть быстрее. В реальности такая ситуация практически никогда не происходит: чаще много клиентов открывают одновременно разные страницы
  • Если запросы к серверу отправляются одновременно, скорость работы apache+php увеличивается (у нас они показали почти такую же скорость, как nginx+javascript с одним рабочим процессом). Но и скорость работы nginx+javascript с несколькими рабочими процессами тоже возрастает (у нас — более, чем в 2 раза). А nginx+javascript с одним рабочим процессом осталась практически неизменной
Кроме того, что такая реализация серверного javascript позволяет добиться увеличения производительности по сравнению с традиционным PHP, javascript позволяет использовать достаточно хитрые языковые конструкции.

// Выводит параметры id запросов GET, POST и cookies:<br/>print($request.get['id'], " ", $request.post['id'], " ", $request.cookie['id']);<br/>// Отправляет заголовок Content-Type:<br/>$result.headers.push("Content-Type: text/html; charset=UTF-8");<br/>// Открывает базу данных, выполняет запрос SELECT с параметром, переданным в GET, и забирает одну строку результата:<br/>var row = (new SQLite("database")).query("SELECT * FROM `table` WHERE `id`=?", $request.get['id']).fetch();<br/>// Читает файл:<br/>print(File.open("index.html").getChars());<br/>// Выводит IP-адрес клиента, открывашего страницу:<br/>print({$server.remoteAddr});<br/>
В последнем примере нет ошибки синтаксиса, XML-документы действительно можно использовать внутри скриптов. При этом в них можно вставлять обращения к переменным и вызовы функций, заключая их в фигурные скобки. Эту технологию, E4X, очень удобно применять для создания шаблонов. Еще ряд примеров можно найти на http://js.nnov.ru/nginx/examples.html.

Конечно, есть еще ряд проблем, которые нужно постепенно решить:
  • Поддержка загрузки файлов (coming soon!)
  • Поддержка cURL и GD, без которых очень сложно жить
  • Оптимизация системных вызовов stat(), которые сейчас используются для определения определения реального пути к файлу
Но, в общем и целом, можно пользоваться. Кстати, небольшой сайт http://js.nnov.ru сделан на JavaScript. Просьба жестких тестов на отказоустойчивость пока не проводить :-)

З.Ы.> Отдельное спасибо FTM за инвайт, благодаря которому топик уже не в песочнице
UPD> Сразу бы опубликовал в тематическом, но были проблемы с кармой. Спасибо всем участвующим!