Стек, который позволил Medium обеспечить чтение на 2.6 тысячелетия
Предлагаю общественности мой перевод статьи Dan Pupius'а об архитектура сервиса Medium и используемых технологиях. Хочу особо отметить, что статья является переводом, поэтому местоимение "я", используемое в тексте далее относится к автору оригинального текста, а не к переводчику.
Фон
Medium это сеть. Это место, где обмениваются историями и идеями, которые важны — место, где вы развиваетесь, и где люди провели 1.4 миллиарда минут — 2.6 тысячелетия.
У нас более 25 миллионов уникальных читателей в месяц, и каждую неделю публикуются десятки тысяч постов. Но мы хотим, чтобы на Medium мерилом успеха было не количество просмотров, а точки зрения. Чтобы значение имело качество идеи, а не квалификация автора. Чтобы Medium был местом, где обсуждения развивают идеи, а слова по-прежнему важны.
Я руковожу инженерной командой. Раньше я работал инженером в Google, где я работал над Google+ и Gmail, а также был одним из со-основателей проекта Closure. В прошлой жизни я гонял на сноуборде, прыгал из самолёта и жил в джунглях.
Команда
Я не мог бы гордиться командой сильнее, чем сейчас. Это потрясающая группа талантливых, любознательных, внимательных людей, которые собрались вместе, чтобы делать великие вещи.
Мы работаем в многофункциональных, нацеленных на решение конкретной задачи, командах, благодаря чему, когда некоторые специализируются, у нас всякий может обратиться к любому компоненту стека. Мы верим, что обращение к различным дисциплинам делает инженера лучше. О других наших ценностях я писал ранее.
Команды имеют много свободы в том, как они организуют работу, однако как компания в целом, мы ставим квартальные цели и поощряем итеративный подход. Мы используем GitHub для код-ревью и отслеживания багов, а также Google Apps для электронной почты, документов и электронных таблиц. Мы интенсивно используем Slack — и его ботов — а многие команды используют Trello.
Первоначальный стек
С самого начала мы использовали EC2. Основные приложения были написаны на Node.js, и мы мигрировали на DynamoDB перед публичным запуском.
У нас был node-сервер для обработки изображений, который делегировал GraphicsMagick фактические задачи по обработке. Другой сервер выступал в роли обработчика очереди SQS, и выполнял фоновые задачи. Мы использовали SES для электронной почты, S3 для статичных ресурсов, CloudFront в качестве CDN и nginx как обратный прокси. Для мониторинга мы использовали DataDog и PagerDuty для оповещений.
В качестве основы редактора, используемого на сайта был TinyMCE. К моменту запуска мы уже использовали Closure Compiler и некоторые компоненты Closure Library, но для шаблонизации использовали Handlebars.
Текущий стек
Для такого, на первый взгляд, простого сайта как Medium, может быть удивительно, как много сложного находится за сценой. Это же просто блог, так? Вы, вероятно, можете просто выкатить что-то на Rails за пару дней. :)
В любом случае, довольно рассуждений. Начнём с самого низа.
Производственное окружение
Мы используем Amazon Virtual Private Cloud. Для управление конфигурацией мы используем Ansible, что позволяет хранить конфигурации в системе управления версиями и легко и контролируемо выкатывать изменения.
Мы придерживаемся сервис-ориентированной архитектуры, поддерживая дюжину сервисов (в зависимости от того, как их считать, несколько из них существенно мельче остальных). Основной вопрос при развёртывании нового сервиса в специфичности работы, которую он выполняет, какова вероятность того, что придётся вносить изменения в несколько других сервисов, а также характеристики потребления ресурсов.
Наше основное приложение по-прежнему написано на Node, что позволяет нам использовать один код как на сервере, так и на клиенте, так, кое-что мы активно используем как в редакторе, так и в обработке поста. С нашей точки зрения Node не плох, но проблемы возникают если мы блокируем цикл обработки событий (event loop). Чтобы сгладить эту проблему, мы запускаем несколько экземпляров на одной машине и распределяем "дорогие" подключения по разным экземплярам, изолируя их таким образом. Мы также обращаемся к среде исполнения V8, чтобы понять, какие задачи занимают много времени; в целом, задержки связаны с восстановлением объектов при десериализации JSON.
У нас есть несколько вспомогательных сервисов, написанных на Go. Мы обнаружили, что приложения на Go очень легко собирать и развёртывать. Нам нравится его типобезопасность без избыточности JVM и необходимости тонкой настройки, как в случае с Java. Лично я — фанат использования командой "упрямых" языков; это повышает согласованность, снижает неоднозначность и определённо снижает шанс выстрелить себе в ногу.
Сейчас мы раздаём статику через CloudFlare, хотя мы пропускаем 5% трафика через Fastly и 5% через CloudFront, чтобы держать их кеши прогретыми на случай если нам понадобится переключиться в экстренной ситуации. Недавно мы включили CloudFlare и для трафика приложений — в первую очередь для защиты от DDOS, но нас также порадовало повышение производительности.
Мы используем nginx и HAProxy совместно в качестве обратных прокси и балансировщиков нагрузки, чтобы перекрыть всю диаграмму Венна наших потребностей.
Мы по-прежнему используем DataDog для мониторинга и PagerDuty для уведомлений, но теперь мы интенсивно используем ELK (Elasticsearch, Logstash, Kibana) для отладки возникших на продакшене проблем.
Базы данных
DynamoDB по-прежнему является нашей основной базой данных, однако это не было спокойным плаванием. Одной из извечных проблем, с которыми мы сталкнулись были ["горячие ключи"] (https://medium.com/medium-eng/how-medium-detects-hotspots-in-dynamodb-using-elasticsearch-logstash-and-kibana-aaa3d6632cfd) (Прим. переводчика: имеются ввиду ключи по которым выболняется большое количество запросов за небольшой промежуток времени), во время событий, ставших вирусными или при отправке постов миллионам подписчиков. У нас есть кластер Redis, который мы используем в качестве кеша перед Dynamo, что решает проблему для операций чтения. Оптимизации с целью повысить удобство разработчиков часто расходятся с таковыми для повышения стабильности, но мы работаем над этим.
Мы начали использовать Amazon Aurora для хранения новых данных из-за того, что она позволяет более гибко, чем Dynamo, извлекать и фильтровать данные.
Для хранения отношений между сущностями, которые представляют сеть Medium мы используем Neo4j, у нас запущен один мастер и две реплики. Люди, посты, теги и коллекции — это узлы графа. Рёбра создаются при создании сущности и при выполнении действий, таких как подписки, рекомендации или подсвечивания. Мы обходим граф чтобы фильтровать и рекомендовать посты.
Платформа данных
С самого начала мы были очень жадными до данных, инвестировали в нашу инфраструктуру анализа данных, чтобы облегчить принятие бизнес и продуктовых решений. В последнее время мы также может использовать существующий конвейер данных для обратной связи с продакшеном, позволяя работать таким функциям, как Explore.
В качестве хранилища данных мы используем Amazon Redshift, он предоставляет масштабируемую систему хранения и обработки данных, поверх которой строятся наши приложения. Мы постоянно импортируем наши основные данные (пользователи, посты) из Dynamo в Redshift и логи событий (просмотр поста, прокручивание страницы с постом) из S3 в Redshift.
Выполнение задач планируется Conduit, нашим внутренним приложением, которое управляет планированием, зависимостями данных и мониторингом. Мы используем планирование, основанное на требованиях (assertion-based scheduling), когда задачи запускаются только если их зависимости удовлетворены (например, ежедневная задача, которая зависит от логов со всего дня). Эта система доказала свою незаменимость — генераторы данных отделены от потребителей, что упрощает конфигурирование и делает систему предсказуемой и отлаживаемой.
Хотя SQL-запросы в Redshift нас устраивают, нам требуется загружать и выгружать данные в и из Redshift. Для ETL мы всё чаще обращаемся к Apache Spark благодаря его гибкости и возможности масштабироваться в соответствии с нашими нуждами. С течением времени Spark скорее всего станет основным компонентом нашего конвейера данных.
Мы используем Protocol Buffers для схем данных (и правил изменения схем) чтобы держать все слои распределённой системы синхронизированными, включая мобильные приложения, веб-сервисы и хранилище данных. Мы аннотируем схемы деталями конфигурации, такими как имена таблиц и индексов, правилами валидации записей, такими как максимальная длина строк или допустимые диапазоны значений чисел.
Люди хотят быть на связи, так что разработчики мобильных и веб приложений могут подключаться к нам единым образом, а исследователи данных (Data Scientists) могут интерпретировать поля единым образом. Мы помогаем нашим людям работать с данными, рассматривая схемы в качестве спецификаций, тщательно документируя сообщения и поля, и публикуя документацию, сгенерированную на основе .proto файлов.
Изображения
Наш сервер обработки изображений написан на Go и использует стратегию "водопад" (waterfall strategy) для отдачи обработанных изображений. Серверы используют groupcache, который предоставляет альтернативу memcache и позволяет снизить объём выполняемой дважды работы. Кеш в памяти резервируется постоянным кешем в S3; после чего изображения обрабатываются по запросу. Это даёт нашим дизайнерам свободу изменять способы отображения изображений и оптимизировать для разных платформ без необходимости запускать большие пакетные задачи по генерированию масштабированных изображений.
Хотя в настоящий момент сервер обработки изображений, в основном, используется для масштабирования и обрезки изображений, ранние версии сайта поддерживали светофильтры, размытие и другие эффекты. Обработка анимированных GIF-файлов доставляла огромную боль, но это тема отдельного поста.
TextShots
Прикольная функция TextShorts реализуется небольшим сервером на Go, использующим PhantomJS в качестве рендерера.
Мне всё время хотелось поменять движок рендеринга на что-нибудь вроде Pango, но на практике возможность выводить изображения в HTML гораздо более гибкая и удобная. А частота, с которой эта функция используется позволяет там довольно легко справляться с нагрузкой.
Произвольные домены
Мы позволяем людям настроить произвольные домены для их публикаций на Medium. Мы хотели, чтобы можно было авторизовываться один раз и для всех доменов, а также HTTPS для всех, так что заставить это всё работать было не тривиально. У нас есть набор серверов HAProxy, которые управляют сертификатами и направляют трафик на основные серверы. При настройке домена по-прежнему требуется ручная работа, но мы автоматизировали большую часть, за счёт интеграции с Namecheap. Сертификаты и связывание с публикациями выполняется выделенными серверами.
Веб фронтенд
В веб части мы предпочитаем оставаться близко к железу. У нас есть собственный фреймворк для одностраничных приложений (SPA), который использует Closure в качестве стандартной библиотеки. Мы используем Closure Templates для рендеринга как на стороне клиента, так и на стороне сервера, и Closure Compiler для минификации кода и разделения на модули. Редактор — это самая сложная часть нашего приложения, Ник о нём уже писал.
iOS
Оба наших приложения нативные, и минимально используют web view.
На iOS мы используем смесь и самодельных фреймворком и встроенных компонентов. На сетевом уровне мы используем NSURLSession для выполнения запросов и Mantle для парсинга JSON и восстановления моделей. У нас есть уровень кеширования, построенный поверх NSKeyedArchiver. Мы используем общий механизм рендеринга элементов списка с поддержкой произвольных стилей, что позволяет нам легко создавать новые списки с различными типами содержимого. Просмотр поста выполнен с помощью UICollectionView с собственным внешним видом (layout). Мы используем общие компоненты для рендеринга всего поста целиком и предпросмотра поста.
Мы собираем каждый коммит и распространяем сборку среди наших сотрудников, так что мы можем опробовать приложение так быстро, настолько это возможно. Частота выкладок приложения в AppStore привязана к циклу проверки приложений, но мы стараемся публиковать приложения даже если изменения минимальны.
Для тестов используем XCTest и OCMock.
Android
На Android мы используем последнюю версию SDK и вспомогательных библиотек. Мы не используем всеохватывающие фреймворки, предпочитая вместо этого использовать стандартные подходы к решению распространённых проблем. Мы используем guava для всего, чего не хватает в Java. В то же время, мы предпочитаем использовать сторонние средства для решения более узких задач. Мы используем свой API на основе protocol buffers, а затем генерируем объекты, которые используем в приложении.
Мы используем mockito и robolectric. Мы пишем высокоуровневые тесты в которых обращаемся к активити и исследуем, что и как — мы создаём базовые версии тестов когда только добавляем экран или готовимся к рефакторингу. Они развиваются по мере того, как мы воспроизводим баги для защиты от регрессии. Мы пишем низкоуровневые тесты для исследования конкретных классов, по мере того, как мы добавляем новые возможности. Они позволяют судить о том, как наши классы взаимодействуют.
Каждый коммит автоматически выкладывается в Play Store как альфа-версия и раздаётся нашим сотрудникам. (Это относится и к приложению для нашего внутреннего варианта Medium — Hatch). Как правило, по пятницам мы пердаём альфа версию нашим бета-тестерам и даём им поиграться с нею на выходных, после чего, в понедельник, выкладываем для всех. Так как последняя версия кода всегда готова к релизу, то если мы находим серьезный баг, то мы сразу же публикуем исправление. Если мы беспокоимся о новой функции, то даём бета-тестерам поиграть с ней подольше; если нам всё нравится, то релизим ещё чаще.
A|B тестирование & Feature Flags
Все наши клиенты использую флаги функций (feature flags), выданные сервером и называемые variants. Они используются для A|B тестирования и отсключения не завершённых функций.
Разное
Есть ещё много всего вокруг продукта, о чём я не сказал: Algolia, которая позволяет быстрее работать с функциями поиска, SendGrid для входящей и исходящей электронной почты, Urban Airship для уведомлений, SQS для обработки очередей, Bloomd для фильтров Блума, PubSubHubbub и Superfeedr для RSS и так далее, и так далее.
Сборка, тестирование и выкладка
Мы используем непрерывную интеграцию и доставку (continuous integration and delivery), выкладывая как можно быстрее. Всем этим управляет Jenkins.
Исторически, в качестве системы сборки мы использовали Make, но для более новых проектов мы мигрируем на Pants.
У нас есть как модульные тесты, так и функциональные тесты уровня HTTP. Все коммиты тестируются перед мерджем. Совместно с командой из Box мы работали над использованием Cluster Runner, чтобы распределять тесты и делать тестирование быстрее, у него есть также интеграция с GitHub.
Мы выкладываем в тестовое окружение так быстро, как только можем — в настоящий момент каждые 15 минут, успешные редакции затем становятся кандидатами для выклдывания на продакшен. Основные серверы приложений обновляются примерно пять раз в день, в некоторых случаях до 10 раз.
Мы используем синие/зелёные сборки (blue/green builds). Перед выкладкой на продакшен мы направляем трафик на тестовый экземпляр и отслеживаем частоту ошибок прежде чем продолжать. Откатывание назад выполняется средствами DNS.
Что дальше?
Много всего! Есть много всего, что нужно сделать для усовершенствования продукта и чтобы сделать чтение и написание постов лучше. Мы также начинаем работать над монетизацией для авторов и издателей. Это новое для нас поле и мы вступаем на него с открытым разумом. Мы считаем, что будущему требуются новые механизмы для финансирования контента, и мы хотим убедиться, что наши функции стимулируют качество и ценность.
Присоединяйтесь к нам
Обычно мы всегда заинтересованы в общении с целеустремлёнными инженерами, у которых есть опыт работы над приложениями для конечных потребителей. У нас нет требований касательно языков, которые вы знаете, так как мы считаем, что хорошие инженеры могут быстро разобраться в новой дисциплине, но вы должны быть любопытными, знающими, решительными и чуткими. За основу можно принять iOS, Android, Node или Go.
Мы также готовим команду Product Science, так что мы ищем людей с опытом построения конвейеров данных и больших аналитических систем.
Ещё я ищу инженерных лидеров, которые помогут развивать команду. Они должны интересоваться теорией управления организацией, хотеть работать руками и быть готовыми руководить подичнёнными.
Вы можете прочитать больше в Medium Engineering.
Блогадарности Nathaniel Felsen, Jamie Talbot, Nick Santos, Jon Crosby, Rudy Winnacker, Dan Benson, Jean Hsu, Jeff Lu, Daniel McCartney и Kate Lee.