Pull to refresh

Impress Application Server простыми словами

Reading time 13 min
Views 21K
Это не первая вводная статья про Impress на Хабре, но за последний год я получил много вопросов и приобрел некоторый опыт в объяснении архитектуры и философии этого сервера приложений и, надеюсь, стал лучше понимать проблемы и задачи разработчиков, начинающих его освоение. Да и в самом сервере произошло достаточно изменений, чтобы назрела актуальность совершенно новой вводной статьи.

Impress Application Server (IAS) — это сервер приложений для Node.js с альтернативной архитектурой и философией, не похожий на мэйнстрим разработки под нодой и призванный упростить и автоматизировать широкий круг повторяемых типовых задач, поднять уровень абстракции прикладного кода, задать рамки и структуру приложений, оптимизировать как производительность кода, так и производительность разработчиков. IAS покрывает сейчас только серверные задачи, но делает это комплексно, например, можно объединить на одном порту API, веб-сокеты, стриминг, статику, Server-Sent Events, проксирование и URL-реврайтинг, обслуживать несколько доменов и несколько приложений, как на одном сервере, так и на группе серверов, работающих в связке, как одно целое, как один сервер приложений.

Введение


Для начала, я хочу перечислить ряд проблем в общепринятом подходе для node.js, которые побудили меня начать разработку сервера приложений:
  1. Прикладной код часто смешан с системным. Дело в том, что нода, и большинство производных фреймворков, слишком низкоуровневые и каждое приложение обязательно содержит часть системного кода, не относящегося к задачам предметной области. Вот и случается, например, что добавление HTTP заголовка становится методом класса Patient и находится в одном файле с заданием роутинга URLов к сему пациенту и с отправкой событий через веб-сокеты. Это чудовищно.
  2. Нода дает чрезмерную свободу в плане архитектуры приложений, которую сложно сразу переварить не только начинающему, но даже бывалому специалисту. Кроме концепции middleware, которой, согласитесь, маловато для полноценной разработки, нет распространенных архитектурных паттернов. Разделение проекта на файлы, разделение логики на классы, применение паттернов, создание внутренних API в приложениях, выделение слоев и даже структура каталогов — все это оставлено на усмотрение разработчика. В результате структура и архитектура проектов очень отличается у разных команд и специалистов, что усложняет понимание и стыковку кода.
  3. В мире ноды существует фанатичное поклонение REST и, как следствие, отказ от хранения состояния в памяти сервера. Это при том, что приложения Node.js живут в памяти долго (т.е. не подгружаются при каждом запросе и не завершаются между запросами). Слабое использование памяти игнорирование возможности развернуть там модель решаемой задачи на долгое время (благодаря чему, можно бы было сократить I/O до самого минимума) — это преступление против производительности.
  4. Большое количество модулей в npm — это мусорные модули, а среди немногих хороших, бывает не просто найти подходящий (количество скачиваний и звезд не всегда адекватно отражает качество кода и скорость устранения проблем). Еще сложнее составить цельное приложение из набора хороших, пусть даже очень хороших модулей. Все вместе они могут вести себя нестабильно и непредсказуемо. Модули не достаточно экранированны друг от друга, чтобы исключить конфликты интеграции (например, какой-то модуль может переопределить res.end или отправить http заголовки, а другие не ожидают такого поведения).

Есть еще много мелких проблем, а глубокое горе с отловом ошибок в Node.js, это тема для трех томов, залитых слезами, кровью и кофе (чаем). Как следствие всего перечисленного — нода, все еще вызывает опасения и, в большинстве случаев, используется как дополнительный инструмент в связке с другими серверными технологиями, выполняя подсобные работы, как то: скриптование сборки клиентских приложений, прототипирование или обеспечение доставки уведомлений по веб-сокетам. Очень редко можно встретить крупный проект, имеющий серверную часть исключительно на ноде.

Постановка задачи


Кроме негативной мотивации (перечисленные проблемы), были еще позитивные побуждающие факторы для разработки IAS (идеи и задачи):
  1. Масштабирование node.js приложений более чем на один сервер, каждый из которых имеет свой cluster (кластер процессов, связанных межпроцессовым взаимодействием IPC).
  2. Обслуживание множества приложений в рамках как одного процесса, так и кластера процессов или фермы серверов с кластером процессов на каждом.
  3. Автоматическая замена кода в памяти, если он изменился на диске, даже без перезапуска приложения, через наблюдение за файловой системой. Как только файлы, подгруженные приложением, меняются, то IAS читает их в память. В какой-то момент в памяти может быть несколько версий кода, старый выгружается как только завершена обработка всех запросов, пришедших до изменения, а новый уже используется для следующих запросов.
  4. Синхронизация структур данных в памяти между процессами. Конечно не всех структур памяти, а только развернутого в ней глобального фрагмента модели предметной области. Поддерживаются аддитивные изменения и транзакци, т.е. если какой-то параметр инкрементируется параллельно в разных процессах, то изменения эти сливаются, ведь их порядок не важен.

Философия Impress


  1. Максимальное использование памяти. Быстрее асинхронного I/O это только когда вообще нет I/O или оно сокращено до минимума и выполняется в отложенном режиме (lazy), а не во время запросов.
  2. Монолитная архитектура и высокая связанность кода, все основные модули интегрированы, согласованы и оптимизированы для работы вместе. Благодаря этому нет лишних проверок, а поведение при решении типовых задач — всегда предсказуемо.
  3. Мультиплексор портов, хостов, IP, протоколов, серверов, процессов, приложений, обработчиков и методов. Таким образом, вы можете совместить на одном порту статику, API, веб-сокеты, SSE, стриминг видео и больших файлов, обрабатывать несколько приложений на разных доменах или многодоменные сайты и т.д.
  4. Принцип прикладной виртуальной машины изолированной от окружения при помощи песочниц (sandboxes). Для каждого приложения есть свой контекст (область видимости), в который загружены свои библиотеки и данные. В приложении архитектурно предусмотрены места для всевозможных обработчиков: инициализации и финализации, моделей данных и структуры БД, конфигурации и установки (первого старта), обновления, миграции и т.д.
  5. Разделение прикладного и системного кода. Вообще, разделять слои абстракций (более высокого и более низкого уровня) в приложении, гораздо важнее, чем разделять логику с моделью и представлением, в рамках одного слоя абстракций (их иногда даже эффективнее смешать).
  6. Мапинг URL на файловую систему с наследованием и переопределением по дереву каталогов. Но с возможностью и программно добавлять обработчики прямо в память и прописывать роутинг руками в добавок к автоматическому роутингу по структуре каталогов.
  7. Краткость кода (см. примеры ниже) достигается благодаря развитому встроенному API, который берет на себя все необходимое в подавляющем большинстве случаев, может расширяться и переиспользоваться от проекта к проекту. Так же краткости способствует особый стиль работы с зонами видимости и разбиение кода на файлы с логическими частями удобного размера.

Область применения


IAS рассчитан для создания нескольких типов приложений:
  1. Одностраничные веб-приложения с API и динамическим изменением страниц на клиенте без перезагрузки с сервера.
  2. Многостраничные веб-приложения с некоторой степенью динамики на страницах через API (логика разделена на клиентскую и серверную).
  3. Многостраничные приложения с перезагрузкой страниц при каждом событии (вся логика на сервере).
  4. Приложения с двусторонним обменом данными или потоком событий с сервера, интерактивные приложения (обычно это надстройка над вариантами 1 и 2).
  5. Сетевое API для доступа к серверу для нативных мобильных и оконных приложений.

Поддерживаются несколько способов создания API
  • RPC API — когда URL идентифицирует сетевой метод с набором параметров, порядок вызова важен и как на клиенте, так и на сервере хранится состояние между между вызовами;
  • REST API, когда URL идентифицирует ресурс, над ресурсом можно выполнять ограниченное кол-во операций (например HTTP verbs или CRUD), запросы атомарны, нет разницы в порядке вызова, и нет состояния между вызовами;
  • Шина событий: одно- или двунаправленный поток клиет-серверного взаимодействия через WebSockets или SSE (Server-Sent Events) используемый для уведомлений или синхронизации состояний объектов между клиентом и сервером;
  • Или смешанный способ.

Обработчики


Аналогом middleware для IAS является handler (обработчик) — это асинхронная функция, которая имеет два параметра (client и callback) и находится в отдельном файле (из которого и экспортируется). Когда вызывается callback, то сервер приложений IAS узнает, что обработка завершилась. Если функция не вызывает callback дольше таймаута, то IAS возвращает HTTP статус 408 (Request timeout) и фиксирует проблему в логах. Если при вызове обработчика происходит исключение, то IAS берет на себя ответ клиенту, отлов ошибки и восстановление работы оптимальным способом, вплоть до удаления и повторного создания песочницы с испорченными или утекшими структурами данных.

Пример API обработчика:
module.exports = function(client, callback) {
  dbAlias.equipment.find({ type: client.fields.type }).toArray(function(err, nodes) {
    if (!err) callback(nodes);
    else callback({ message: 'Equipment of given type not found' }, 404);
  });
}

Каждый HTTP запрос может вызвать исполнение нескольких обработчиков. Например, если запрошен URL domain.com/api/example/method.json, а IAS установлен в /impress, то исполнение начнется с каталога /impress/appplications/domain.com/app/api/example/method.json/ и проходит следующие этапы:
  • проверяются права доступа по файлу access.js из этого каталога, сессии (если есть) и аккаунту пользователю (если имеется привязанный к сессии),
  • выполняется обработчик request.js из этого каталога (если найден), он выполняется при вызове любого HTTP метода (get, post...),
  • выполняется один из обработчиков соответствующих методу HTTP запроса, например get.js, put.js, post.js и т.д. (если найден),
  • выполняется обработчик end.js (если найден), он будет вызван при любом HTTP методе,
  • произойдет сериализация данных ответа или шаблонизация страницы (если это предусмотрено типом возвращаемого ответа),
  • результат выполнения запроса отправляется на клиент,
  • уже после этого, когда клиент получил ответ и мы не задерживаем его, выполняются обработчик lazy.js (если найден), который может, например, сделать отложенные операции, изменить/пересчитать или сохранить данные в БД,
  • на любом этапе исполнения в прикладном коде может произойти ошибка, вызывающая необработанное исключение, но нам не нужно оборачивать код в try/catch или создавать domain, это уже сделано в IAS, при ошибке будет вызван обработчик error.js (если найден).

Если в запрашиваемом каталоге нет нужного обработчика, то IAS будет искать его на один каталог выше, пока не дойдет до /app. Если обработчик есть в папке, то он может программно вызвать обработчик из каталога выше (или ближайший вверх по дереву) через client.inherited(). Таким образом, можно использовать дерево каталогов для формирования наследования и переопределения обработчиков. Например, вы можете формировать данные ответа в обработчике /api/example/request.js, а выдавать их в трех форматах: /api/example/method.json, /api/example/method.html (содержит еще и шаблоны для вывода в html), /api/example/method.csv (может содержать дополнительные действия, например, формирования заголовка таблицы). Или сделать общий обработчик ошибок для всего API в файле /api/error.js. Такой подход дает большую гибкость и позволяет сократить размеры кода, однако, мы платим за это известными ограничениями.

Расширения у каталогов означают автоматическую отдачу из них контента определенного типа, а значит, установку определенных HTTP заголовков и преобразование результата в нужный формат данных. Все это можно переопределить вручную, но использование расширений сокращает количество кода. Из коробки поддерживаются такие расширения: .json, .jsonp, .xml, .ajax, .csv, .ws, .sse и этот список просто расширяем при помощи плагинов.

Пространства имен


Внутри обработчика видны следующие имена, через которые мы можем обращаться к функциям IAS и подключенным библиотекам:
  • client — объект, содержащий распарсенные поля запроса, ссылки на оригинальные request и response, соответственно в client.req и client.res, API по работе с запросом, ссылки на сессию и авторизованного пользователя;
  • application — объект, отвечающий за приложение, и содержащий его конфигурацию, параметры и соответствующее API сервера приложений;
  • db — неймспейс, содержащий ссылки на все загруженные драйвера СУБД и установленные соединения с базами данных, обращаться к ним можно через db[alias] или db.alias;
  • api — неймспейс, содержащий ссылки на все встроенные и внешние библиотеки, которые были разрешены из конфигурации приложения. Например api.fs.readFile(...) или api.async.parallel(...);
  • api.impress — ссылка на API сервера приложений;
  • системные глобальные идентификаторы, общепринятые для JavaScript и Node.js: require, console, Buffer, process, setTimeout, clearTimeout, setInterval, clearInterval, setImmediate, clearImmediate. Но мы можем в конфигурации запретить использование некоторых из них, например, отключив приложению require, и предоставив ему только определенный набор библиотек, автоматически загруженных в его неймспейс api.

Делать require в обработчиках не нужно, достаточно установить библиотеки в папку /impress через npm install и подключить их через конфигурацию /сonfig/sandbox.js (сначала в конфиге IAS, а потом локально в конфиге приложения). Далее библиотеки видны в обработчиках через api.libName, точно так же становятся видны и встроенные библиотеки, например, api.path.extname(...) и т.д.

Все базы данных и драйверы СУБД видны через db.name. Соединения настраиваются в /config/databases.js (для каждого приложения отдельно), устанавливаются при старте и автоматически восстанавливаются при потере связи. В комплекте идут драйверы для MongoDB, PostgreSQL и MySQL, обернутые в плагины для IAS, при желании за 30 минут можно обернуть в плагины драйвера любой СУБД.

Для типа контента html используется простой встроенный шаблонизатор, он нужен скорее не для полной генерации страниц на стороне сервера, а для сборки layout (основной разметки и расположения кусков интерфейса), а так же, для подстановки немногочисленных значений из структур данных в html. Шаблонизатор содержит инклады и итераторы, но более сложную шаблонизации нужно реализовывать уже в браузере при помощи React, Angular, EJS и т.д., запрашивая шаблоны и данные отдельно и собирая их в браузере (с переиспользованием шаблонов), что типично для динамических веб-приложений. Встроенный же шаблонизатор, начинает рендеринг с файла html.template и подставляет в него данные из client.context.data. Конструкция @fieldName@ подставит значение из поля, конструкция @[file]@ вставит файл file.template, а конструкция @[name]@ ... @[/name]@ реализует итератор по хешу или массиву с именем name.

Для обработчиков, возвращающих сериализованные данные (.json, .jsonp, .csv и т.д.) шаблонизация не нужна. Для них структура данных client.context.data просто сериализуется в JSON (с отсеканием рекурсии). Для удобства можно возвращать структуру данных из обработчика первым параметром callback({ field: "value" }); Если один обработчик вернул в callback данные или присвоил их в client.context.data, то следующие за ним (до конца жизни текущего HTTP запроса) могут читать и изменять данные.

Обработчики могут изменять http код статуса, добавлять свои http заголовки, но в штатном режиме они работают только с объектом client, у которого есть методы безопасного API: client.error(code), client.download(filePath, attachmentName, callback), client.cache(timeout), client.end(output) и т.д. Начиная с версии 0.1.157 в IAS реализована частичная поддержка обработчиков middleware, имеющих 3 параметра: req, res и next. Но нужно это крайне редко, а код, портированный из проектов на express или connect, обычно можно переписать в несколько раз короче и проще.

Создавать обработчики обоих типов, т.е. handler (с 2я параметрами) и middleware (с 3я параметрами) можно не только из файлов, а добавляя роутинг вручную, через вызовы методов, например:
application.get('/helloWorld.ajax', function(req, res, next) {
  res.write('<h1>Middleware handler style</h1>');
  next();
});

Структура приложения


Серверный код не ограничивается обработчиками, приложение так же может содержать модель предметной области, специализированные библиотеки и утилиты, используемые во многих обработчиках, и другие «места», для размещения логики и данных. Все приложения, запускаемые в IAS размещаются в каталоге /applications и имеют следующую структуру:
  • /app — корневой каталог обработчиков, соответствующий корню сайта hostname,
  • /config — конфигурация приложения,
  • /doc — документация и вспомогательные материалы,
  • /files — каталог для размещения загруженных пользователем файлов (в нем автоматически строится 2-х или 3-х уровневая система подкаталогов, чтобы не перегружать файловую систему большим количеством файлов),
  • /init — код инициализации, запускаемый при старте сервера (тут можно программно создавать обработчики, подготавливать структуры данных в памяти, открывать tcp порты, и т.д.),
  • /lib — каталог для библиотек и утилит, которые загружаются при старте (но после инициализации) и доступны из всего кода приложения,
  • /log — каталог для логов (если в конфигурации настроено отдельное логирование для этого приложения),
  • /model — каталог для моделей предметной области (так же загружается при старте, но после инициализации),
  • /setup — размещенные в этом каталоге js-файлы будут запущены только 1 раз при рестарте IAS или всего сервера, это место необходимо для скриптов обновлений или миграции, которые необходимы для поддержания полноценного жизненного цикла приложения уже во время его эксплуатации,
  • /tasks — каталог для размещения запланированных задач, которые бывают двух типов: запускаемые приодически, через определенный интервал или запускаемые в назначенное время,
  • /tmp — каталог для временных файлов.

В ближайших версиях появятся еще такие каталоги (issue #195):
  • /client — каталог размещения исходников клиентской части,
  • /static — собранные клиенты потом будут помещаться в /static, а в качестве сборщика можно будет использовать несколько наиболее распространенных средств.

Функциональность IAS


Пусть эта статья останется вводной, так что, я не буду сейчас подробно описывать весь арсенал IAS и перегружать читателя. Ограничусь простым перечислением основного: регистрация сервисом (демоном), прозрачное масштабирование на много процессов и много серверов, встроенная система пользователей и сессий (в т.ч. анонимных и аутентифицированных), поддержка SSE (Server-Sent Events) и веб-сокетов с системой каналов и подписки на сообщения, поддержка проксирования запросов, URL-реврайтинг, интроспекция сетевого API и выдача индексов каталогов, управление доступом к каталогам через access.js (аналог .htaccess), конфигурирование приложений, логирование, прокручивание логов, отдача статики с кешированием в память, gzip компессия, поддержка HTTP заголовков «if-modified-since» и HTTP 304 (Not Modified), поддержка HTTPS, стриминг файлов с поддержкой отдачи по частям (с указанного места и до указанного места, что обычно используют плееры, например HTML5 video-тег через HTTP заголовки Content-Range и Accept-Ranges), есть скрипты быстрого развертывания сервера для чистых машин (CentOS, Ubuntu, Debian), встроенные механизмы межпроцессового взаимодействия через IPC, HTTP и ZeroMQ, специальное API для синхронизации состояния между процессами, встроенный механизм мониторинга здоровья серверов, подсистема запуска отложенных задач, возможность порождать воркеры (параллельные процессы), валидация структур данных и схем БД, генерация структур данных из схем для SQL-совместимых СУБД, автоматическая обработка ошибок и длинного стека, оптимизация сбора мусора, экранирование песочниц (sandboxes), поддержка HTTP basic authentication, обработка виртуальных хостов и виртуальных путей, приклеивание IP (sticky), плагины (в т.ч. passport, geoip, nodemailer, минификации js, трансляции sass и т.д.), подсистема юнит-тестирования, утилиты для upload/download файлов и многое другое.

Заключение


Impress (IAS) активно развивается, каждую неделю появляется от 4 до 7 минорных версий. Сейчас актуальна версия 0.1.195 и на подходе версия 0.2, в которой мы зафиксируем структуру приложений и базовое API, соблюдая обратную совместимость для всех 0.2.x версий. В 0.2.x мы будем заниматься только вопросами оптимизации и исправлением ошибок, а расширение функциональности будет возможно только если это не потребует редизайна приложений, основанных на 0.2.x. Все крупные нововведения и эксперименты будут параллельно вводиться в ветке 0.3.x. Приглашаю всех желающих развивать проект, а со своей стороны обещаю поддерживать код, как минимум, до тех пор, пока это актуально. Версия же 1.0 появится только тогда, когда я пойму, что независимые разработчики полностью в состоянии поддерживать код. Сейчас готовится документация, которая до этого была невозможна из-за того, что структура и архитектура часто менялась, я опубликую ссылку на нее по готовности версии 0.2. До этого подробнее ознакомиться с IAS можно по примерам, которые устанавливаются вместе с IAS, как приложение по умолчанию.

Немного цифр по состоянию на 2015-01-11: загрузок из npm вчера: 1 338, за эту неделю: 5 997, за последний месяц: 21 223, звезд на github: 168, вклад в репозиторий: 8 человек, строк кода: 6 120, размер исходников: 207 Кб (из них ядро: 118Кб), усредненная цикломатическая сложность кода: 20, кол-во закрытых issues в github: 151, открытых issues: 9, дата первой опубликованной версии: 2013-06-08, кол-во сборок в Travis CI: 233, кол-во коммитов github: 468.

Ссылки


NPM: www.npmjs.com/package/impress
Github: github.com/tshemsedinov/impress
Tags:
Hubs:
+17
Comments 15
Comments Comments 15

Articles