Node.JS / Разработка WEB-проекта на Node.JS: Часть 1

в 7:13, , рубрики: node.js, web программирование, web-разработка, метки: , ,

Прошла неделя с момента пиара на хабре моего проекта «Что делать?». Я напомню, что этот проект начинался, как эксперимент по разработке среднестатистического WEB-проекта целиком на JavaScript (Node.JS). Сейчас я хочу поделиться с сообществом результатами этого эксперимента, полученным полезным опытом, а также подробной картой с отмеченными на ней граблями.
Эпизод 1: начало пути
Я ставил перед собой следующие цели:Понять, насколько удобно разрабатывать обычные WEB-проекты на Node.JS;

Сравнить скорость разработки на Node.JS с другими используемыми технологиями (в моём случае это были PHP и Java);

Добраться до подводных камней, с которыми не сталкиваешься на многочисленных синтетических примерах;

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

Этот проект стал моим первым опытом разработки полноценного сайта на Node.JS, до этого я использовал его только для написания вспомогательных сервисов для рабочих проектов. Я начал со знакомства с WEB-фреймворками для этой платформы. На тот момент их существовало уже несколько и был выбор. На этом этапе нужно понимать, что если писать на JavaScript так же, как на PHP, или как на Java, или как на любом другом языке, это ни к чему хорошему не приведёт. На JavaScript нужно писать, как на JavaScript, иначе зачем он нам вообще (спасибо, Кэп)? Поэтому сразу проходим мимо тучи фреймворков, предлагающих различные реализации классического наследования, синхронного программирования и т.п. костылей, не свойственных языку. Я искал «тру»-JavaScript путь и хотел оценить именно его, а не порт PHP на JavaScript.
Из всех фреймворков выбор пал на Express. Он показался мне наименее перегруженным излишествами и при этом наиболее полно соответствовал моим требованиям и его можно было легко дополнить сторонними модулями.
От фреймворка я хотел следующего:MVC архитектура;

Роутинг;

Работа с СУБД;

Шаблонизатор;

Поддержка мультиязычности;

Поддержка многопоточности;

Минимальный оверхед.

Также он должен был быть прост в понимании и представлять из себя удобный инструмент, а не ещё одну технологию, требующую дополнительного изучения.
Эпизод 2: путь самурая

Поигравшись немного с Express на тестовом примере, разобравшись в его архитектуре и изучив исходный код, я принял решение сделать свой фреймворк, который бы удовлетворял все мои потребности и не был перегружен лишним для меня функционалом. Велосипед — скажите Вы. Опыт — скажу я. Один из лучших способов понять систему — разработать её аналог самому. Фреймворк был разработан, отлажен, и на нём была основана первая версия сайта. Сайт был запущен, и я стал наблюдать за его работой, параллельно исправляя всплывающие ошибки и добавляя новые фичи.
Эпизод 3: великий потоп

Естественно, что первое время на сайте было крайне мало посетителей, чтобы хоть как-то объективно оценить его работу. Однако, через несколько дней, я обнаружил, что сайт лежит. Упал он молча, в логах не было ничего подозрительного, но падению сопутствовала серия быстро идущих друг за другом http-запросов — это был чей-то робот. Эти запросы никак нельзя было назвать нагрузкой, многие роботы сканируют сайты с такой скоростью, и падать из-за этого он никак не должен. Я поднял сайт и протестировал его через утилиту siege. И действительно — сайт ложился даже на совсем маленьком количестве параллельных запросов, что совершенно не соответствовало моим ожиданиям. Логи молчали. Печаль наростала. Начались раскопки.
Эпизод 4: первый камень

Повторяя эксперимент, я следил за тем, что происходит с сервером и обнаружил проблему — процесс node выжирал всю доступную память и либо вырубался, либо зависал (переставал принимать http-запросы). Оговорюсь, что во время разработки я старался следить за расходом памяти, т.к. это один из самых опасных моментов в долго работающих приложениях. Лишние замыкания не делал, var не терял, в общем вёл себя аккуратно. В очередной раз перебрав свой код, я убедился, что ошибка не в нём и, честно-говоря, огорчился, т.к. там было бы проще всего её исправить. Отладчик для Node.JS существует, он довольно удобный, но отлаживать Node.JS приложение сложнее из-за его асинхронной работы. Каюсь, я не достиг 80-го уровня в работе с этим замечательным инструментом и вычислить утечку не смог. Так закончился мой отпуск. Времени на познание силы Node.JS стало гораздо меньше, и я решил отложить это дело до лучших времён. Не удовлетворённый результатом, я написал маленькую утилитку, которая перезапускала процесс каждые сутки. На таких костылях сайт проработал 4-5 месяцев. Тем временем, развитие Node.JS не стояло на месте. Новые версии выходили одна за другой, да и сторонние модули тоже развивались, избавляясь от детских болезней.
Эпизод 5: новая надежда

И вот, после довольно долгого перерыва, в change log'е Node.JS появились записи об исправленных утечках памяти (версии 0.6.6 — 0.6.7). Так же, начиная с версии 0.6.0, в стабильный релиз попал cluster api [ссылка], который позволял избавиться от лишних плясок для запуска нескольких процессов node. Всё это добавило энтузиазма и заставило посмотреть на проект под новым углом. К этому времени я уже видел, какие части моего фреймворка не используются, чего в нём не хватает, что не удобно. Плюс ко всему, я решил заменить MySQL (изначально испольховался он) на MongoDB.
Ещё раз проанализировав популярные фреймворки, я понял, что все они, по большому счёту, представляют из себя сборки различных модулей и небольшие доработки “от себя”, а также диктуют свои правила построения приложения. Всё это загоняет в жёсткие рамки при сомнительном выигрыше. Если необходимо обновить отдельный модуль, а фреймворк с ним конфликтует, то придётся ждать, когда обновлённая версия будет добавлена в него разработчиками. Или если Вы хотите использовать модуль, который данный фреймворк не поддерживает, например, любимый шаблонизатор. Что делать? Форкать и добавлять поддержку, а потом вручную мёрджить обновления в ожидании, пока разработчики фреймворка примут ваш pull request? Нет!
Модульная система Node.JS вполне позволяет не использовать фреймворки (читать — готовые сборки модулей), а работать с модулями напрямую. При этом дополнительный функционал, вместо заворачивания в фреймворк, нужно выносить в отдельный модуль. Такой подход сильно увеличивает гибкость системы и улучшает повторное использование кода.
Так у меня появился план рефакторинга проекта. Он состоял из следующих пунктов:Отказаться от использования фреймворка, перейти к прямому использованию модулей;

Выбрать из огромного количества модулей наиболее подходящие для решения моей задачи;

Заменить MySQL на MongoDB;

Для организации запуска нескольких процессов node использовать, теперь уже нативный, cluster api.

Ещё несколько выходных, и план был реализован. Всё прошло довольно гладко, количество кода уменьшилось, структура приложения стала более понятной. Всё было протестировано и готово к запуску. Тогда я опубликовал пиар-статью на хабре и стал ждать.
Эпизод 6: эпик вин

Не скажу, что «хабраэффект» был огромным. Пришло всего пара тысяч человек. Через пару часов сайт упал, но не из-за нагрузки, а из-за моей ошибки. Да-да, в одном труднодоступном месте я допустил опечатку в имени переменной, что и послужило причиной падения. В остальном никаких проблем не возникло. Странички отдавались очень быстро, процессор не напрягался, память не текла. Уже неделю я не перезапускал node, и сайт продолжает стабильно работать. Такой результат, после предыдущего печального опыта, очень радует.
Выводы

Нельзя писать на JavaScript только ради JavaScript. Выбирать язык по причине того, что он крутой/новый/популярный (нужное подчеркнуть) — глупо. Нужно чётко понимать, чего мы хотим от технологии и что она может нам дать, а также что она попросит взамен. Моей главной задачей было выявить все эти “что” и получить ответ на вопрос: «подходит ли Node.JS для разработки среднестатистических WEB-проектов». Мой ответ — да, подходит. Но, как и в любой другой технологии, существуют трудности, с которыми придётся столкнуться. Начну я, пожалуй, с минусов, чтобы закончить на позитивной ноте.
Хьюстон, у нас проблемы

-1

Первое, с чем придётся столкнуться при разработке на Node.JS — это отсутствие полноценной IDE, работающей из коробки. Конечно, существуют плагины для Eclipse и NetBeans, есть частичная поддержка и в других IDE. Но назвать их полноценными решениями на данный момент нельзя. Есть очень перспективная разработка Cloud9 IDE. Она представляет из себя IDE для разработки на JavaScript прямо в браузере. Эта IDE быстро развивается и уже активно используется многими Node.JS разработчиками, но при этом, по моему субъективному мнению, она пока не готова для работы над большими проектами, хотя для написания небольших модулей использовать её очень удобно. За время работы над проектом я сменил не одну IDE. Начал с notepad++, долгое время использвал Cloud9 IDE, пробовал и другие, названий которых уже не вспомню, но в конце концов вернулся к NetBeans. Хоть там и нет поддержки Node.JS API, но с чистым JavaScript там работать довольно удобно. Пока остаётся ждать и надеется на появления полноценной поддержки. Разработка такого плагина уже ведётся.
-2

Вторым неприятным моментом станет необходимость перезапуска приложения после внесения каждого изменения. Несмотря на существование утилит, автоматизирующих этот процесс, некоторое время на это всё равно уходит. Так же быстро, как на PHP, внести изменение и сразу увидеть результат не получится. Однако, приложение перезапускается достаточно быстро — от одной до нескольких секунд, в зависимости от используемых модулей. В этом Node.JS, несомненно, выигрывает у Java — там перезапуск серьёзного WEB-приложения происходит намного дольше.
-3

Следующей проблемой, с которой столкнутся начинающие Node.JS разработчики, будет, скорее всего, утечка памяти. За памятью надо следить всегда и везде, но быстро живущие PHP-скрипты или небольшой клиентский JavaScript сильно расслабляют. В таких приложениях многие разработчики особо не предают значения потере нескольких килобайт памяти, а некоторые вообще не следят за этим показателем. Node.JS таких поблажек не делает. Приложение на Node.JS работает долго, и в случае утечки, каждый запрос к сайту будет уносить с собой кусочек памяти, которая очень быстро закончится, что приведёт к известным последствиям. Специфика JavaScript также располагает к появлению этого типа ошибок. Одно лишнее замыкание в большой области видимости или один потерянный var могут подарить Вам незабываемые часы дебага. Но в этом есть и плюс — такой опыт заставит любого разработчика ценить память и контролировать её использование при программировании на других языках. Лично я не считаю эту особенность минусом.
-4

Ещё одна особенность, которая доставляет неудобства — это возврат ошибок из асинхронного кода. Для сравнения, в PHP весь код выполняется синхронно, поэтому отловить ошибку на любом уровне вложенности можно с помощью конструкции try-catch. Мы можем завернуть в неё работу контроллера и при возникновении исключительной ситуации сделать throw, и ошибка «всплывёт» к ожидающему её обработчику, который покажет пользователю красивую страничку позора. Несмотря на то, что в JavaScript тоже есть конструкция try-catch, нам она не поможет, т.к. большая часть кода работает асинхронно (операции ввода/вывода). При этом исключительные ситуации, как правило, возникают не при вызове метода, а при работе его callback`а, который выполняется уже вне конструкции try-catch. Для передачи информации об ошибках в Node.JS принято использовать первый параметр callback-функции. Т.е. если у нас возникла ошибка, мы вызываем callback-функцию либо с одним единственным параметром, описывающем её, либо первый параметр выставляем в undefined/null, а в последующих передаём результаты работы нашего функционала. В реальных приложениях вложенность вызовов может быть довольно большой и каждый раз передавать ошибку наверх очень неудобно. Полагаю, что проблему всё же можно решить, применив AOP, но это тема выходит за рамки данной статьи.
-5

Также заставят понервничать и самые простые ошибки, которые довольно часто добираются и до production-сервера. Опечатка в имени переменной в очень редко выполняемом блоке кода может привести к остановке всего сайта или одного из процессов, если используется cluser api. Чтобы избежать подобных неприятностей, можно использовать утилиты, мониторящие работу приложения и перезапускающие его при необходимости, но настоящие ниндзя просто хорошо тестируют свой код (хотя никто не застрахован от ошибки в стороннем модуле).
-6

Последнее, что хотелось бы добавить — сложные математические расчёты не для Node.JS. На эту тему на хабе уже был очень флеймовый топик и смысла повторять тут всё, что написано в нём и комментариях к нему я не вижу. Скажу только, что у автора топика (конечно же, я имею ввиду автора оригинала, а не перевода) проблемы с выбором технологии, либо он просто троль. В Node.JS всё же возможно производить сложные математические расчёты, путём разбиения задачи на короткие итерации, выполняемые за несколько витков event loop, но это уже извращения и фанатизм. Гораздо проще выносить такие задачи за пределы event loop или вообще выбрать другую технологию для разработки.
А ради чего всё это?

Если к этому времени у Вас не возник такой вопрос, значит Вы либо опытный JavaScript-разработчик, либо поисковый робот, либо Чак Норрис (здравствуй, Чак!). Для всех остальных я попытаюсь описать преимущества, которые даёт нам разработка WEB-проекта на Node.JS.
+1

Самым значимым преимуществом Node.JS я считаю асинхронный ввод/вывод и прозрачность работы с ним. В большинстве WEB-проектов самыми частыми операциями являются чтение данных из БД и их сохранение. Эти же операции обычно являются самыми медленными. Но далеко не всегда они зависят друг от друга, наоборот, в большинстве случаев они атомарны. Например, при добавлении нового комментария к статье, нам нужно:Сохранить сам комментарий;

Обновить пользователя (например, количество комментариев у пользователя и дату его последнего комментария);

Обновить статью (аналогично обновлению пользователя);

Записать в лог результат запроса.

Все эти операции не зависят друг от друга и СУБД способна выполнять их одновременно. При синхронном выполнении, например в PHP, эти операции будут выполняться друг за другом последовательно, каждый раз ожидая завершения предыдущей. В Node.JS мы имеем возможность отправить все 4 этих запроса к СУБД «параллельно». На самом деле запросы всё же отправляются последовательно (поэтому я взял в кавычки слово «параллельно»), т.к. Node.JS работает в одном процессе. Но Node.JS не дожидается результата работы предыдущего запроса, чтобы отправить следующий. Временем отправки запроса, по сравнению со временем его работы, при этом можно пренебречь. Когда запросы выполнятся, будут вызваны callback-функции для обработки их результата. Эти вызовы будут происходить так же последовательно, как и отправка запросов, а результат их будет обработан несравнимо быстрее времени их выполнения. Таким образом, общее время работы с данными будет приблизительно равно времени работы самого долгого запроса + небольшой оверхед на отправку запросов и обработку их результатов. Но в любом случае это будет быстрее, чем сумма времени выполнения всех запросов при их последовательной обработке (оверхед на отправку запросов и обработку их результатов в этом случае тоже никуда не уходит).
+2

Второй очень приятной возможностью является то, что ответ клиенту можно (и нужно) отправить сразу же, как он будет готов, не дожидаясь завершения работы всей логики запроса. Вернёмся к предыдущему примеру. При добавлении комментария мы записываем в БД некий лог. Эта операция нужна нам, но не нужна пользователю для получения ответа. С Node.JS мы можем сформировать и отправить ответ, а потом доделать то, что нужно нам: записать логи, очистить кеш и т.п. Время ожидания пользователя, соответственно, уменьшается.
+3

Третий положительный фактор — мы имеем одинаковый язык на сервере и на клиенте, это позволяет повторно использовать некоторый код (валидация форм, построение шаблонов на клиенте и т.п.). Да и в целом, это очень упрощает разработку, особенно, когда клиентская часть приложения сложная и требует серьёзной работы, а не обработки пары-тройки событий на кнопках.
+4

Процесс Node.JS живёт долго и обрабатывает все http-запросы внутри себя. Это значит, что для каждого нового запроса не выполняется инициализация, как, например, на PHP. Настройки загружены, соединения с БД и с кешем открыты, код скомпилирован и готов к работе. Благодаря такой архитектуре и гибкости JavaScript открывается огромный простор для различных техник оптимизации. Например, один раз разобрав шаблон, можно хранить его в виде функции, принимающей на вход данные и возвращающей готовый HTML. Или можно легко организовать локальное (для процесса) кеширование, наиболее часто используемых данных, что даст прирост в скорости работы с ними даже по сравнению с memcached. Да, для PHP есть решения, которые позволяют частично ускорить процесс инициализации — APC для op-code, поддержка постоянных соединений в FastCGI и т.п. Но при сравнении ускоренного процесса инициализации с его отсутствием в принципе — выигрыш всегда будет за последним.
+5

Вокруг Node.JS за его относительно короткую жизнь уже образовалась солидная экосистема, включающая сотни модулей и, соответственно, их разработчиков. Во многом благодаря удобству github сообщество имеет отличный инструмент для развития этой экосистемы, которое идёт семимильными шагами, и свой вклад в это развитие может внести любой желающий. С помощью таких инструментов, как The Node Toolbox и npm, процесс поиска, выбора и установки необходимых модулей становится простым и быстрым.
+6

Сам язык JavaScript и API Node.JS очень гибкие и лаконичные. Программы получаются компактными и легко читаемыми. Вы вряд ли увидите в них классы, состоящие практически целиком из геттеров и сеттеров, или десятки файлов с описаниями интерфейсов. Такие вещи, как замыкания и лямбда-функции, позволяют писать очень красивый код, однако при особом таланте разработчика способны превратить его в филиал ада.
Я долго смеялся, увидев сравнение производительности Java и Node.JS. Тут можно долго рассуждать и приводить различные аргументы, но я скажу просто, как думаю. В event loop'е я вертел такую производительность, если код программы при этом увеличивается в десятки раз. У меня есть опыт разработки большого WEB-проекта на Java + Spring Framework + Hibernate. Там 10 строк кода, красиво описывают то, что будет делать 11-ая. Грубо, конечно, но это приблизительно отражает ситуацию. Возможно, для какого-то класса задач это и актуально, но не для среднестатистических WEB-проектов. Стоит добавить, что такое сравнение на «Hello world», не отражает реального положения дел. При появлении логики приложения, работы с БД, кеширования и других компонентов системы, сильно вырастает вероятность словить тормоза из-за не оптимального использования технологии, чем из-за её несовершенства. К тому же нужно помнить, что ресурсов сервера может не хватать в двух случаях:Код написан плохо;

У Вашего сайта огромная посещаемость.

В первом случае поможет только выпрямление рук с последующим переписыванием кода и технологии тут совершенно не при чём. Во втором — у вас уже достаточно денег, чтобы купить ещё один сервер, т.к. это экономически более выгодно, чем поддержка в десятки раз большего объёма кода.
Заключение

Node.JS это не волшебная палочка. Это мощный инструмент со своими плюсами и минусами, который следует использовать только тогда, когда нужен именно он. Результатом своего эксперимента я доволен. Проект работает быстро, имеет простой и понятный код, который легко развивать и поддерживать. Несмотря на некоторые недостатки, Node.JS удобно использовать для разработки WEB-проектов и я верю в его развитие в этом направлении.
Продолжение следует

Во второй части я более подробно остановлюсь на архитектуре приложения. Расскажу, какие модули я использовал и как организовано взаимодействие между ними.
Спасибо всем, кто дочитал. Буду рад увидеть Ваши отзывы и мнения в комментариях.

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


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