Tactoom.com изнутри — социальная блог-платформа на NodeJS/NoSQL

Материал из Tech_support
Перейти к навигации Перейти к поиску

http://habrahabr.ru/post/130345/

Итак, пришло время раскрыть некоторые карты и рассказать о том, как устроен Tactoom изнутри.

В этой статье я расскажу о разработке и выведении в production веб-сервиса с использованием: NodeJS (fibers), MongoDB, Redis, ElasticSearch, Capistrano, Rackspace.


Вступление

Три недели назад мы с Давидом (DMiloshev) запустили инфосоциальную сеть Tactoom.com. О том, что это такое можно почитать здесь.

На фоне шума, недавно поднятого вокруг NodeJS, вероятно, многим интересно, что же эта технология представляет из себя не на словах, а на деле.

NodeJS — это вовсе не панацея. Это просто еще одна технология, по сути, ничем не лучше других. Для того чтобы добиться хорошей производительности и масштабируемости, вам придется хорошенько попотеть — так же, как и везде.

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

NodeJS приложение делится на два вида процессов:

  1. Web процесс (http)
  2. Cloud процесс (очереди)

Все процессы абсолютно независимы друг от друга, могут находиться на разных серверах и даже в разных точках земного шара. При этом, масштабируется приложение как раз при помощи мультипликации этих процессов. Общение между ними происходит сугубо через централизированный сервер сообщений (redis).

Web процессы обслуживают прямые http запросы пользователей. Каждый процесс может обрабатывать множество запросов одновременно. Учитывая специфику Eventloop, в зависимости от соотношения CPU/IO каждого конкретного запроса, предел параллельной обработки может либо снижаться, либо повышаться для отдельного процесса на момент времени.

Cloud процессы производят операции, которые не связаны напрямую с пользовательскими запросами. Например: отправка email-ов, денормализация данных, поисковая индексация. Как и Web, один Cloud процесс может обрабатывать множество задач разных типов одновременно.

Стоит отметить, что здесь очень важна «атомарность» задач/запросов. То есть, нужно следить за тем, чтобы емкая задача/вычисление было разбито на множество более мелких частей, которые затем будут равномерно распределены по остальным процессам. Это повысит скорость выполнения задачи, отказоустойчивость и снизит потребление памяти и коэффициент блокировки каждого процесса и сервера целиком.

Web → Cloud

Я стараюсь организовать Web процессы таким образом, чтобы повысить общий коэффициент времени IO против CPU, а значит сфокусировать их на быстрой выдаче http при высокой конкурентности запросов. Это значит, что Web делегирует high-cpu логику в Cloud, ждет ее выполнения, затем получает результат вычислений. Соответственно, в силу асинхронной архитектуры nodejs, во время ожидания Web может выполнять другие запросы.

Кластеризация

Архитектура Web и Cloud очень схожа, за исключением того, что вместо http сокета Cloud «слушает» redis очередь.

Кластеризация node процессов происходит по следующим принципам:

  1. На каждом физическом сервере запущен один процесс-супервизор (node-cluster)
  2. Дочерние процессы супервизора и есть наши Web-ы и Cloud-ы, количество которых всегда равно количеству ядер сервера.
  3. Супервизор контролирует потребление памяти каждым дочерним процессом и, в случае превышения заданной нормы, перезапускает его (предварительно дождавшись завершения текущих запросов этого процесса).

Nodejs-cluster.png

Fibers

Весь высокоуровневый слой приложения написан с использованием node-sync (fibers), без которого я вообще слабо себе представляю его разработку. Дело в том, что такие сложные вещи, как та же сборка статики, реализовать на «официальной» callback-driven парадигме весьма сложно, если не глупо. Тем, кто еще не видел код того же npm, настоятельно рекомендую на него посмотреть, и попытаться понять, что там происходит, а главное — зачем. А холивары и троллинг, которые разрастаются вокруг асинхронной парадигмы nodejs чуть ли не каждый день, мягко говоря, вызывают у меня недоумение.

Подробнее о node-sync вы можете узнать в моей статье: node-sync — псевдо-синхронное программирование на nodejs с использованием fibersсохраненная копия

Web

Общая логика Web приложения реализована на фреймворке expressjs в стиле «express». За исключением того, что каждый запрос заворачивается в отдельный Fiber, внутри которого все операции выполняются в синхронном стиле.

В силу невозможности переопределить некоторые участки функциональности expressjs, в частности роутинга, его пришлось вынести из npm и включить в основной репозиторий проекта. Это же касается и ряда других модулей (особенно тех, которые разработаны LearnBoost), ибо contributing в их проекты дается очень большим трудом и вообще не всегда удается.

CSS генерируется через stylus. Это действительно очень удобно. Шаблонизатор — ejs (как на сервере, так и на клиенте). Загрузка файлов — connect-form.

Web работает очень быстро, поскольку все модули и инициализация загружаются в память процесса при запуске. Я стараюсь держать среднее время отклика Web процесса на любую страницу — до 300ms (исключая загрузку изображений, регистрацию и т.д.). Проводя профайлинг, я с удивлением обнаружил, что 70% этого времени отнимает работа mongoose (mongodb ORM для nodejs) — об этом ниже.

i18n

Долго искал подходящее решение для интернационализации в nodejs, и мои поиски сошлись на node-gettext с небольшим допиливанием. Работает как часы, файлы локалей подтягиваются «на лету» серверными nodejs процессами при обновлении.

Кэш

Функциональность кэширования со всей своей логикой уместилась в два экрана кода. В качестве cache-backend используется redis.

Память

У Web процессов память течет рекой, как позднее оказалось, из-за mongoose. Один процесс (днем, во время средней нагрузки), съедает до 800MB за два часа, после чего перезапускается супервизором. Утечки памяти в nodejs искать довольно сложно, если вы знаете интересные способы — дайте мне знать.

Данные

Как показала практика, schema-less парадигма mongodb идеально подходит для модели Tactoom. Сама БД ведет себя хорошо (весит 376MB, из них 122MB — индекс), данные выбираются исключительно по индексам, поэтому результат любого запроса — не больше 30ms, даже при высокой нагрузке (большинство запросов вообще <1ms).

Если интересно, во второй части могу более подробно рассказать о том, как удалось «приручить» mongodb для ряда нетривиальных задач (и как не удалось).

mongoosejs (mongodb ORM для nodejs)

О нем хочу отдельно сказать. Выбор списка 20-ти пользователей: запрос и выбор данных в mongo занимает 2ms, передача данных — 10ms, потом mongoose делает что-то еще 200ms (уже молчу про память) и в итоге получаю объекты. Если переписать это на более низкоуровневый node-mongodb-native, то все это займет 30ms.

Постепенно, мне пришлось переписать практически все на mongodb-native, при этом, повысив быстродействие системы в целом раз в 10.

Статика

Вся статика Tactoom хранится на Rackspace Cloud Storage. При этом я использую статический домен cdnX.infosocial.net, где X — 1..n. Этот домен пробрасывает через DNS на внутренний домен контейнера в Cloud Storage, позволяя браузерам грузить статические файлы параллельно. Каждый статический файл хранится в двух копиях (plain и gzip) и имеет уникальное имя, в которое зашита версия. Если версия файла обновится — адрес изменится, и браузеры загрузят новый файл.

Сборка статики приложения (клиентский js и css, картинки) происходит через самописный механизм, который определяет измененные файлы (через git-log), делает minify, делает gzip копию и загружает их на CDN. Скрипт сборки так же следит за измененными изображениями и обновляет их адреса в соответствующих css файлах. Список (mapping) статики адресов всех файлов хранится в Redis. Этот список загружает в память каждый Web процесс при запуске либо при обновлении статических версий. По сути, дэплоймент любых изменений статики осуществляется одной командой, которая делает все сама. Причем это не требует никакой перезагрузки, поскольку nodejs приложения подхватывают измененные адреса статических файлов на лету через redis pub/sub.

Пользовательская статика тоже хранится на Rackspace, но в отличие от статики приложения, не имеет версий, а просто проходит определенную канонизацию, позволяющую по хэшу картинки получить адреса всех ее размеров на CDN.

Для определений хоста (cdnX), на котором хранится конкретный статический файл, используется consistent hashing.

Серверная архитектура Tactoom-servers.png

По сути, Tactoom раскидан на 3 гермозоны:

  1. Rackspace — площадка для быстрого масштабирования и хранения статики
  2. Площадка в Европе — здесь наши физические сервера
  3. Секрет (сюда ротэйтятся логи, производятся фоновые вычисления и собирается статистика)

В мир смотрит только один сервер — nginx, с открытыми портами 80 и 4000. Последний используется для COMET соединений. Остальные сервера общаются между собой по прямым ip, закрыты от мира через iptables.

:80 nginx проксирует запросы через upstream конфигурацию на Web серверы. На данный момент есть два апстрима: tac_main и tac_media. Каждый из них содержит список Web серверов, на которых работает node-cluster по 3000 порту, у каждого Web сервера есть свой приоритет при распределении запросов.

tac_main — кластер Web серверов, которые находятся близко к базе данных и отвечают за выдачу большинства веб-страниц для зарегистрированных пользователей Tactoom.

tac_media — кластер Web серверов, находящихся близко к CDN. Через них происходят все операции по загрузке и ресайзингу изображений.

Сервера webN и cloudN изображены, чтобы показать, где я добавляю сервера при хабраэффекте и других приятных событиях. Новые сервера поднимаются в течение 10 минут — по образу, сохраненному на CDN.

:4000

Тут обычный proxy-pass на сервер comet, где работает nodejs COMET приложение Beseda, о которой я расскажу во второй части.

tac1, tac2, data1

Это главные сервера Tactoom: XEON X3440 4x2.53 ГГц 16 ГБ 2x1500 ГБ Raid1. На каждом работает Mongod процесс, все они объединены в ReplicaSet с автоматическим failover-ом и дистрибьюцией операций чтения на slave-ы.

На tac1 — главный Web кластер, на tac2 — Cloud кластер. В каждом кластере по 8 nodejs процессов.

В ближайшее время создам еще один upstream tac_search, на который будут роутиться исключительно поисковые запросы. В нем будет Web-cluster, который поставлю рядом с elasticsearch (про него во второй части) сервером.

Выводы

Цитируя лозунг создателей NodeJS: «Because nothing blocks, less-than-expert programmers are able to develop fast systems.» «Из-за того, что ничего не блокируется, менее-чем-эксперты могут разрабатывать быстрые системы.»

Это обман. Я использую nodejs уже почти 2 года и на собственном опыте знаю, что для того, чтобы на нем разрабатывать «быстрые системы», нужно не меньше опыта (а то и больше), чем на любой другой технологии. В реальности же с callback-driven парадигмой в nodejs и особенностями javascript в целом, скорее легче сделать ошибку (и потом очень долго ее искать), чем выиграть в производительности. С другой стороны, троллинг господина Ted Dziuba тоже полный бред, ибо пример с «числами фибоначчи» высосан из пальца. Так будет делать только человек, не понимающий, как работает Eventloop и зачем он вообще нужен (что, кстати, доказывает пункт 1).


После доклада на DevConf этой весной, мне часто задают вопросы о том, стоит ли делать новый проект на NodeJS. Мой ответ всем: Если у вас куча времени и вы готовы проинвестировать его в развитие новой, сырой и спорной технологии — валяйте. Но если перед вами сроки/заказчики/инвесторы и у вас нет большого опыта с серверным JS за спиной — не стоит.

Как показала практика, поднять проект на NodeJS — реально. Это работает. Но это стоило мне очень многого. Чего только стоит node open-source сообщество, о котором я еще постараюсь успеть написать.

Часть 2

Вторая часть статьи будет на днях. Вот краткий список того, о чем я в ней напишу: 1. Поиск (elasticsearch) 2. Почта (google app engine) 3. Деплоймент (capistrano, npm) 4. Очереди (redis, kue) 5. COMET сервер (beseda)

Слишком много информации для одной статьи. Если в комментариях увижу интересные вопросы, на них отвечу во второй части.