Кто использует Node.js: Trello (Часть 1)
Довольно часто встречаю вопросы типа: «А кто вообще из крупных/известных/заметных IT компаний использует в производстве Node.js?». 19 января 2012 года в блоге компании Fog Creek, основанной небезызвестным Джоелом Спольски (Joel Spolsky), была опубликована статья «The Trello Tech Stack». Странно, что на Хабре это как-то обошли вниманием. Поэтому, чтобы исправить этот недостаток, а заодно показать пример использования Node.js в крупном проекте, я сделал перевод этой статьи.
Сама статья довольно объемная, поэтому разбита на две части:
Часть 1
- CoffeeScript
- Клиент
* Backbone.js
* HTML5 History API
* Mustache - Pushing and Polling
* Socket.io and WebSockets
* AJAX запросы
Часть 2
- Сервер
* node.js
* HAProxy
* Redis
* MongoDB - Итак, нравится ли нам это?
The Trello Tech Stack
Разработка Trello началась с HTML макета, который Justin и Bobby — дизайнеры команды Trello — собрали за неделю. Я был сражен тем, как круто он выглядел и ощущался. После того, как Daniel и я присоединились к проекту для разработки прототипа и рабочей версии Trello, настоящим вызовом было сохранить восхитительные впечатления от первоначального макета во время создания сервера и клиента.
Первоначальный макет Trello.
Макет привел нас к одностраничному веб-приложению, которое создает пользовательский интерфейс на стороне клиента и принимает обновления данных по каналу связи с севером. Это значительно отличалось от любой работы, которую мы прежде выполняли в Fog Creek. Таким образом, с технической точки зрения разработка Trello была приключением.
Сначала нас удивляло, насколько интересной и разнообразной может быть структура [приложения], прежде чем управление [проектом] стало беспокоить, наши проблемы были решены на первичном совещании с Joel, когда он сказал: «Используйте решения, которые будут отлично работать в течение двух лет».
Так мы и сделали. Мы последовательно выбирали перспективные (и часто доставляющие проблемы) технологии, которые будут предоставлять потрясающий опыт по сравнению с более зрелыми альтернативами. Мы занимаемся этим около года, и это нам нравится.
CoffeeScript
Разработка Trello начиналась на чистом JavaScript как на стороне клиента, так и на стороне сервера, и придерживалась этого пути до мая месяца, когда мы в качестве эксперимента переписали пару файлов на CoffeeScript, чтобы посмотреть, понравится ли нам это. Нам очень понравилось, и очень скоро мы переписали остальной код и продолжили разработку полностью на CoffeeScript.
CoffeeScript — это язык, который компилируется в легкий для чтения JavaScript. Он уже существовал, когда мы начали разработку Trello, но меня беспокоила дополнительная сложность, вызванная отладкой скомпилированного кода, вместо непосредственной отладки исходных файлов. Когда мы попробовали это, тем не менее, конвертация оказалась настолько качественной, что при отображении конечного кода на исходные файлы во время отладки в Chrome требовалось небольшое умственное усилие. А выигрыш от краткости кода и легкости чтения при использовании CoffeeScript был очевиден и убедителен.
JavaScript — классный язык. Хорошо написанный CoffeeScript сглаживает и сокращает JavaScript, наряду с этим сохраняет ту же семантику, и не привносит существенной проблемы в отладку.
Клиент
- Backbone.js (MVC фреймворк)
- HTML 5 (History API: pushState)
- Mustache (шаблоны)
Серверы Trello, в сущности, не работают с HTML. Фактически, они не выполняют много клиентского кода. Страница Trello — это легковесная (2КиБ) оболочка (thin shell), которая скачивает с сервера клиентское приложение в виде одного минимизированного и сжатого JS файла (включающего библиотеки сторонних производителей и наши скомпилированные CoffeeScript файлы и Mustache шаблоны) и одного CSS файла (скомпилированного из наших LESS файлов и включающего встроенные (inlined) изображения). Все это занимает менее 250КиБ, и мы раздаем это с Amazon’s CloudFront CDN. Таким образом мы получаем загрузку с малой задержкой (low-latency) у большинства пользователей. В случае довольно высокоскоростного соединения (high-bandwidth) получаем загрузку и запуск приложения в окне браузера примерно за полсекунды. Еще имеем выгоду от кэширования, и таким образом при последующих посещениях Trello та часть [загрузка] пропускается.
Параллельно, мы убрали со стартовой страницы загрузку данных по AJAX и пробуем установить WebSocket соединение с сервером.
Backbone.js
Когда поступает ответ с данными, начинает действовать Backbone.js. Замысел с Backbone в том, чтобы отображать каждую полученную с сервера модель (Model) с помощью вида (View), и Backbone предоставляет легкие пути для:
- слежения за DOM событиями в HTML, который был создан видом, и привязывания их к обработчикам в модели, которая синхронизируется с сервером;
- слежения за изменениями модели и отображения изменившихся HTML блоков.
Изящно! Используя этот подход мы получаем нормальное, понятное и поддерживаемое клиентское приложение. Мы специальным образом строим кэш модели на стороне клиента, чтобы обрабатывать обновления и упростить повторное использование модели на стороне клиента.
History.pushState
Теперь, когда у нас все клиентское приложение загружается в окно браузера, мы не хотим тратить время на переходы между страницами. Мы используем метод интерфейса HTML 5 History.pushState для перемещения между страницами. Этим способом мы обеспечиваем правильные и согласующиеся ссылки в location bar [элементе браузера], и при переходе просто загружаем данные и передаем их соответствующему контроллеру, основанному на Backbone.
Mustache
Мы используем Mustache — язык шаблонов с минимальным использованием логических конструкций — для отображения наших моделей в HTML. В то время как «использование полной мощи [подставьте_сюда_ваше_название] шаблонов» звучит хорошо, на практике оказывается, что это требует серьезной дисциплины от разработчиков для поддержания понятного кода. Мы были очень счастливы подходом Mustache «краткость — сестра таланта», который позволяет нам повторно использовать код шаблонов без смешивания его с логикой клиента и внесения путаницы.
Pushing and Polling
Обновления в реальном масштабе времени (realtime) — вещь не новая, но это важная часть при создании инструмента для совместной работы, поэтому мы потратили некоторое время на эту часть Trello.
Socket.io and WebSockets
Для тех браузеров, которые поддерживают это, мы использовали WebSocket соединения, чтобы сервер проталкивал изменения, сделанные другими людьми, в браузеры, слушающие соответствующие каналы. Мы используем модифицированные (*) Socket.io клиентские и серверные библиотеки, которые позволяют нам держать открытыми много тысяч WebSocket соединений на каждом нашем сервере с минимальными затратами в плане CPU и использования памяти. Таким образом, когда некоторое действие происходит на доске [элемент Trello], за которой вы наблюдаете, это действие передается в процесс на нашем сервере и распространяется в браузер с минимальной задержкой, как правило значительно меньше секунды.
(*) В настоящий момент Socket.io сервер имеет некоторые проблемы с масштабированием до более чем 10К одновременных соединений с клиентами при использовании нескольких процессов и Redis хранилища. И клиент имеет некоторые проблемы, которые могут привести к открытию множественных соединений с одним и тем же сервером или к тому, что клиент не сможет определить, что его соединение обслужено. Есть некоторые проблемы с внесением наших изменений (хаков!) обратно в проект — во многих случаях они работают только с WebSockets (единственный транспорт Socket.io, который мы используем). Мы работаем над теми изменениями, которые подходят для общего использования, для внесения обратно в проект.
AJAX запросы
Они не замысловатые, но работают.
Первоначальный набросок архитектуры.
Когда браузер не поддерживает WebSockets (привет, IE), мы просто делаем крошечные AJAX запросы изменений каждые пару секунд, пока пользователь активен, и снижаем интервал до десяти секунд, когда пользователь переходит в неактивное состояние (idle). Так как настройки нашего сервера позволяют обслуживать HTTPS запросы с минимальными накладными расходами и держать TCP соединения открытыми, мы в состоянии поделиться неплохим опытом по использованию простых запросов, когда это необходимо.
Мы пробовали Comet через низкоуровневые транспорты для Socket.io, и все они были на тот момент нестабильными, так или иначе. К тому же, кажется рискованным использовать Comet и WebSockets как основу для главной возможности (feature) приложения. Нам хотелось иметь возможность вернуться к наиболее простым и устоявшимся технологиям, если мы наткнемся на проблему.
Мы наткнулись на проблему сразу после запуска. Наша реализация WebSocket сервера начала странно себя вести при внезапных и массивных нагрузках, вызванных TechCranch эффектом, и мы были рады, что способны вернуться к простым запросам, и регулировать производительность сервера настройкой интервалов запросов во время активности и во время бездействия. Это позволило нам плавно снижать производительность, когда у нас за неделю число пользователей выросло с 300 до 50 000. Сейчас мы вернулись к использованию WebSockets, но имеем работающую систему коротких запросов, которая все еще кажется весьма благоразумным резервом.
Продолжение следует...