Архитектура Tumblr

Tumblr - одна из самых популярных в мире платформ для блоггинга, которая делает ставку на привлекательный внешний вид, юзабилити и дружелюбное сообщество. Хоть проект и не особо на слуху в России, цифры говорят сами за себя: 24й по посещаемости сайт в США с 15 миллиардами просмотров страниц в месяц. Хотите познакомиться с историей этого проекта, выросшего из простого стартапа?

Введение

Как и всем успешным стартапам, Tumblr удалось преодолеть опасную пропать между начинающим проектом и широко известной компанией. Поиск правильных людей, эволюция инфраструктуры, поддержка старых решений, паника по поводу значительного роста посещаемости от месяца к месяцу, при этом в команде только 4 технических специалиста - все это заставляло руководство Tumblr принимать тяжелые решения о том над чем стоит работать, а над чем - нет. Сейчас же технический персонал расширился до 20 человек и у них достаточно энергии для преодоления всех текущих проблем и разработки новых интересных технических решений.

Поначалу Tumblr был вполне типичным большим LAMP приложением. Сейчас же они двигаются в направлении модели распределенных сервисов, построенных вокруг существенно менее распространенных технологий. Основные усилия сейчас вкладываются в постепенный уход от PHP в пользу более "правильных" и "современных" решений, оформленных в виде сервисов. Параллельно с переходом к новым технологиям идут изменения и в команде проекта: от небольшой группы энтузиастов к полноценной команде разработчиков, имеющей четкую структуру и сферы ответственности, но тем не менее жаждущей реализовывать новый функционал и обустраивать совершенно новую инфраструктуру проекта.

Платформа

  • CentOS на серверах, Mac OS X для разработки
  • Apache - основной веб-сервер
  • PHP, Scala, Ruby - языки программирования
  • Finagle - асинхронный RPC сервер и клиент
  • MySQL, HBase - СУБД
  • memcachedRedis - кэширование
  • Varnish, nginx - отдача статики
  • HAProxy - балансировка нагрузки
  • kestrel, gearman - очередь задач
  • Thrift - сериализация
  • Kafka - распределенная шина сообщений
  • Hadoop - обработка статистики
  • ZooKeeper - хранение конфигурации и состояний системы
  • git - система контроля версий
  • Jenkins - непрерывное тестирование

Статистика

  • Около 500 миллионов просмотров страниц в день
  • Более 15 миллиардов просмотров страниц в месяц
  • Посещаемость растет примерно на 30% в месяц
  • Пиковые нагрузки порядка 40 тысяч запросов в секунду
  • Около 20 технических специалистов в команде
  • Каждый день создается около 50Гб новых постов и 2.7Тб обновлений списков последователей
  • Более 1Тб статистики обрабатывается в Hadoop ежедневно
  • Используется порядка 1000 серверов:
    • 500 веб-серверов c Apache и PHP-приложением
    • 200 серверов баз данных (существенная их часть - резервные)
      • 47 пулов
      • 30 партиций (шардов)
    • 30 серверов memcached
    • 25 серверов Redis
    • 15 серверов Varnish
    • 25 серверов HAProxy
    • 8 серверов nginx
    • 14 серверов для очередей задач

Типичное использование

  • Tumblr используется несколько по-другому, чем другие социальные сети:

    • При более чем 50 миллионах постов в день, каждый из них попадает в среднем к нескольким сотням читателей. Это и не несколько пользователей с миллионами читателей (например, популярные личности в Twitter) и не миллиарды личных сообщений.
    • Ориентированность на длинные публичные сообщения, полные интересной информацией и картинками/видео, заставляет пользователей проводить долгие часы каждый день за чтением Tumblr.
    • Большинство активных пользователей подписывается на сотни других блоггеров, что практически гарантирует много страниц нового контента при каждом заходе на сайт. В других социальных сетях поток новых сообщений переполнен ненужным контентом и толком не читается.
    • Как следствие, при сложившемся количестве пользователей, средней аудиторией каждого и высокой активностью написания постов, системе приходится обрабатывать и доставлять огромное количество информации.
  • Публичные блоги называют Tumblelog'ами, они не так динамичны и легко кэшируются.

  • Сложнее всего масштабировать Dashboard, страницу, где пользователи в реальном времени читают что нового у блоггеров, на которых они подписаны:
    • Кэширование практически бесполезно, так как для активных пользователей запросы редко повторяются.
    • Информация должна отображаться в реальном времени, быть целостной и не "задерживаться".
    • Около 70% просмотров страниц приходится именно на Dashboard, почти все пользователи им пользуются.

Старая архитектура

  • Когда проект только начинался, Tumblr размещался в Rackspace и последние выдавали каждому блогу с собственным доменом A-запись. Когда они переросли Rackspace, они не смогли полноценно мигрировать в новый датацентр, в том числе из-за количества пользователей. Это было в 2007 году, но у них по-прежнему часть доменов ведут на Rackspace и перенаправляются в новый датацентр с помощью HAProxy и Varnish. Подобных "унаследованных" проблем у проекта очень много.
  • С технической точки зрения проект прошел по пути типичной эволюции LAMP:

    • Исторически разработан на PHP, все началось с веб-сервера, сервера баз данных и начало потихоньку развиваться.
    • Чтобы справляться с нагрузкой они начали использовать memcache, затем добавили кэширование целых страниц и статических файлов, потом поставили HAProxy перед кэшами, после чего сделали партиционирование на уровне MySQL, что сильно облегчило им жизнь.
    • Они делали все, чтобы выжать максимум из каждого сервера.
    • Было разработано два сервиса на C: генератор уникальных идентификаторов на основе HTTP и libevent, а также Staircar, использующий Redis для обеспечения уведомлений в реальном времени на Dashboard.
  • Dashboard использует подход "разбрасывать-собирать", так как из-за отсортировонности данных по времени традиционные схемы партиционирования работали не очень хорошо. По их прогнозам текущая реализация позволит им рости еще в течении полугода.

Новая архитектура

  • Приоритетным направлением стали технологии, основанные на JVM, по причине более быстрой разработки и доступности квалифицированных кадров. Мотивация несколько спорная, особенно если учесть, что речь идет в первую очередь о Scala, а не о Java.
  • Основная цель - вынести все из PHP приложения в отдельные сервисы, что сделает его лишь тонким клиентом к внутреннему API.
  • Почему выбор пал именно на Scala и Finagle?

    • Многие разработчики имели опыт с Ruby и PHP, так что Scala был привлекательным (цитата, логики мало)
    • Finagle был одним из основных факторов в пользу JVM: это библиотека, разработанная в Twitter, которая решает большинство распределенных задач вроде маршрутизации запросов и обнаружение/регистрацию сервисов - не пришлось реализовывать это все с нуля.
    • В Scala не принято использовать общие состояния, что избавляет разработчиков от забот с потоками выполнения и блокировками.
    • Им очень нравится Thrift в роли программного интерфейса из-за его высокой производительности (он кроссплатформенный и к JVM никак не относится)
    • Нравится Netty, но не хочется связываться с Java, еще один аргумент в пользу Scala.
    • Рассматривали Node.js, но отказались так как под JVM проще найти разработчиков, а также из-за отсутствия стандартов, "лучших практик" и большого количества качественно протестированного кода.
  • Старые внутренние сервисы также переписываются с C + libevent на Scala + Fingle.

  • Был создан общий каркас для построения внутренних сервисов:
    • Много усилий было приложено для автоматизации управления распределенной системой.
    • Создан аналог скаффолдинга - используется некий шаблон для создания каждого нового сервиса.
    • Все сервисы выглядят одинаково с точки зрения системного администратора: получение статистики, мониторинг, запуск и остановка реализованы одинаково для всех сервисов.
    • Созданы простые инструменты для сборки сервисов без вникания в детали используемых стандартных решений.
  • Используется 6 внутренних сервисов, над которыми работает отдельная команд. На запуск сервиса с нуля уходит около 2-3 недель.
  • Новые, нереляционные СУБД, такие как HBase и Redis, вводятся в эксплуатацию, но основным хранилищем по-прежнему остается сильно партиционированный MySQL.
  • HBase используется для сервиса сокращенных ссылок для постов, а также всех исторических данных и аналитики. HBase хорошо справляется с ситуациями, где необходимы миллионы операций записи в секунду, но он не достаточно стабилен, чтобы полностью заменить проверенное временем решение на MySQL в критичных для бизнеса задачах.
  • Партиционированный MySQL плохо справляется с отсортированными по времени данными, так как один из серверов всегда оказывается существенно более "горячим", чем остальными. Также сталкивались с значительными задержками в репликации из-за большого количества параллельных операций добавления данных.
  • Используется 25 серверов Redis с 8-32 процессами на каждом, что означает порядка 300-400 экземпляров Redis в сумме.
    • Используется для уведомлений в реальном времени на Dashboard (о событиях вроде "кому-то понравился Ваш пост").
    • Высокое соотношений операций записи к операциям чтения сделало MySQL не очень подходящим кандидатом.
    • Уведомления не так критичны, их потеря допустима, что позволило отключить персистентность Redis.
    • Был создан интерфейс между Redis и отложенными задачами в Finagle.
    • Сервис коротких ссылок также использует Redis как кэш, а HBase для постоянного хранения.
    • Вторичный индекс Dashboard также построен вокруг Redis.
    • Redis также используется для хранения задач Gearman, для чего был написан memcache proxy на основе Finale.
    • Постепенно отказываются от memcached в пользу Redis в роли основного кэша. Производительность у них сопоставима.
  • Внутренним сервисам необходим доступ к потоку всех событий в системе (создание, редактирование и удаление постов, нравится или не нравится и т.п.), для чего была созданна внутренняя шина сообщений (англ. firehose, пожарный шланг):
    • Пробовали использовать в этой роли Scribe, но так как оно по сути свелось к пропусканию логов через grep в реальном времени - нагрузки оно не выдержало.
    • Текущая реализация основана на Kafka, решению аналогичной задачи от LinkedIn на Scala.
    • MySQL также не рассматривался из-за большой доли операций записи.
    • Внутри сервисы используют HTTP потоки для чтения данных, хотя Thrift интерфейс также используется.
    • Поток сообщений хранит события за последнюю неделю с возможностью указать момент времени с которого считывать данные при открытии соединения.
    • Поддерживается абстракция "группы потребителей", которая позволяет группе клиентов вместе обрабатывать один поток данных вместе и независимо, то есть одно и то же сообщение не попадет дважды к клиентам из одной группы.
    • ZooKeeper используется для периодического сохранения текущей позиции каждого клиента в потоке.
  • Новая архитектура Dashboard основана на принципе ячеек или ящиков входящих сообщений:
    • Каждая "ячейка" отвечает за группу пользователей и читает новые события с шины сообщений, если один из её пользователей-подопечных подписан на автора только что опубликованного поста, то пост добавляется в "почтовый ящик" подписанного пользователя.
    • Когда пользователь заходит в Dashboard его запрос попадает в его ячейку, которая возвращает ему нужную часть непрочитанных постов.
    • Каждая ячейка состоит из трех групп серверов:
      • HBase для постоянного хранения копий постов и почтовых ящиков;
      • Redis для кэширование свежих данных;
      • Сервис, читающий данные из шины и предоставляющий доступ к ящикам посредством Thrift.
    • В HBase используется две таблицы:
      • Отсортированный список идентификаторов постов для каждого пользователя в ячейке, именно в том виде, как они будут отображены в итоге.
      • Копии всех постов по идентификаторам, что позволяет выдать все данные для отрисовки Dashboard без обращений к серверам вне одной ячейки.
    • Ячейки представляют собой независимые единицы, что позволяет легко масштабировать систему при росте числа пользователей.
    • Платой за относительно безболезненность масштабирования является чрезвычайная избыточность данных: при том что ежедневно создается лишь 50Гб постов, суммарный объем данных в ячейках растет на 2.7Тб в день.
    • Альтернативой было бы использование общего кластера со всеми постами, но тогда он бы стал единственной точкой отказа и потребовалось бы делать дополнительные удаленные запросы. Помимо этого выигрыш по объему был бы не велик - списки идентификаторов занимают значительно больше места, чем сами посты.
    • Пользователи, которые подписаны или на которых подписаны миллионы других пользователей, обрабатываются отдельно - страницы с их постами генерируются не заранее (как описывалось выше), а при поступлении запроса - это позволяет не тратить впустую много ресурсов (этот подход называется выборочная материализация).
    • Количество пользователей в одной ячейке позволяет управлять балансом между уровнем надежности и стоимостью содержания этой подсистемы.
    • Параллельное чтение их шины сообщений оказывает серьезную нагрузку на сеть, в дальнейшем из ячеек можно будет составить иерархию: только часть будет читать напрямую из шины сообщений, а остальным сообщения будут ретранслироваться.
  • Tumblr географически по-прежнему находится в одном датацентре (если не считать незначительное присутствие в Rackspace), распределение по нескольким лишь в планах.

Развертывание

  • Начиналось как несколько rsync-скриптов для распространения PHP-приложения. Как только машин стало больше 200 такой подход стал занимать слишком много времени.
  • Следующий вариант был основан на Capistrano: были созданы три стадии процесса развертывания (разработка, тестирование, боевой). Неплохо справлялся с десятками серверов, но на сотнях также был слишком медленным, так как основывался на SSH.
  • Итоговый вариант основан на Func, решении от RedHat, позволившим заменить SSH на более легковесный протокол.

Разработка

  • Поначалу философия была такова, что каждый мог использовать любые технологии, которые считал уместным. Но довольно скоро пришлось стандартизировать стек технологий, чтобы было легче нанимать и вводить в работу новых сотрудников, а также для более оперативного решения технических проблем.
  • Каждый разработчик имеет одинаковую заранее настроенную рабочую станцию, которая обновляется посредством Puppet:
    • Настроена публикация изменений, тестирование и развертывание новых версий.
    • Разработчики используют vim и Textmate.
  • Новый PHP код систематически инспектируется другими разработчиками.
  • Внутренние сервисы подвергаются непрерывному тестированию посредством Jenkins.

Структура команд

Проект разбит на 6 команд:

  • Инфраструктура: все, что ниже 5 уровня по модели OSI - маршрутизация, TCP/IP, DNS, оборудование и.т.п.
  • Платформа: разработка основного приложения, партиционирование SQL, взаимодействие сервисов.
  • Надежность (SRE): сфокусирована на текущие потребности с точки зрения надежности и масштабируемости.
  • Сервисы: занимается более стратегической разработкой того, что понадобится через один-два месяца.
  • Эксплуатация: отвечает за обнаружение и реагирование на проблемы, плюс тонкая настройка.

Найм

  • На интервью они обычно избегают математики и головоломок, основной упор идет в основном именно на те вещи, которым придется заниматься кандидату.
  • Основной вопрос: будет ли он успешно решать поставленные задачи? Цель в том, чтобы найти отличных людей, а не в том, чтобы никого не брать.
  • Разработчиков обязательно просят привести пример своего кода, даже во время телефонных интервью.
  • Во время интервью кандидатов не ограничивают в наборе инструментов, можно даже гуглить.
  • Поиск людей с опытом в крупных проектах достаточно сложен, так как всего нескольких компаниях по всему миру решают подобные проблемы.

Подводим итоги

  • Автоматизация - ключ к успеху крупного проекта.
  • При партиционировании MySQL может масштабироваться, но лишь при преобладании операций чтения.
  • Redis с отключенной персистентностью легко может заменить memcached.
  • Scala достойно себя проявляет в роли языка программирования для внутренних сервисов, во многом благодаря обширной Java-экосистеме.
  • Внедряйте новые технологии постепенно, поначалу работать с HBase и Redis было очень болезненно, они были включены в основной стек технологий только после испытаний в некритичных сервисах и подпроектах, где цена ошибки не так велика.
  • Проект должен строиться вокруг навыков его команды, а не наоборот.
  • Нужно нанимать людей только если они вписываются в команду и в состоянии довести работу до результата.
  • При выборе технологического стека одну из ключевых ролей играет доступность соответствующих специалистов на кадровом рынке.
  • Читайте публикации и статьи в блогах. Ключевые аспекты архитектуры, включая "ячейки" и частичную материализацию были позаимствованы из внешних источников.
  • Поспрашивайте своих коллег, кто-то из них мог общаться с специалистами из Facebook, Twitter, Google или LinkedIn - если нет прямого доступа, всегда можно получить нужную информацию через одно-два "рукопожатия".

Статья написана на основе интервьюBlake Matheny, директора по разработке платформы Tumblr.