Фёдор Борщёв

Заметки с тегом «Продукты»

Сноб: отчёт о первом этапе работ

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

Задача

Сноб — это интернет-медиа, статьи в котором пишут не только штатные редакторы, но и внешние участники проекта: любой человек может приобрести подписку и завести собственную колонку на snob.ru. Коду проекта больше 10 лет, писали его разные люди на совершенно разных технологиях — в копилке есть и Zend Framework с MySQL и Django с PostgreSQL.

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

Проект состоит не из одного бэкенда, а из трёх: основной сайт, блоги внешних участников проекта и редакционная админка. Архитектуру взаимодействия никто не продумывал — каждую новую систему городили на предыдущие как придётся. Это привело к тому, что данные между бекендами стали передаваться совершенно непредсказуемым образом: частично через запись в базу, частично — через вебхуки. Из-за этого пользователи периодически теряют данные: если материал, профиль или комментарий не укладывается в формат обмена между системами (или в момент сохранения пролетает птичка и моргает сеть) — данные портятся.

Кроме трёх бэкендов, у проекта есть ещё три фронтенда: старый от ПХП-движка, куча кода на Django и SPA на next.js, от которого предыдущая команда успела внедрить совсем небольшие части функциональности. Сверху всего этого стоит nginx, который одним ему ведомым образом решает, какая из этих систем будет отрабатывать запрос.

Старая версия блогов. Обратите внимание на форму входа в верхнем левом углу — переписать и сделать её приятной почти невозможно.

Поговорив с бизнесом, мы поняли, что самая большая проблема — в ПХП-движке на Zend Framework, который обслуживает блоги участников проекта. У бизнеса есть куча гипотез, которые можно проверить, но ни одного ПХП-шинка в команде не осталось, а внешних нанять невозможно — ни один нормальный программист не пойдет работать на 10-летний легаси без здоровой инженерной культуры.

Решение

Конечно, работать дальше с таким легаси нельзя — надо как можно скорее от него избавляться. Поскольку бизнес больше всего хотел решить проблему с блогами — с них мы и начнём. Мы поставили амбициозную цель — в конце работы оставить движок, которым пользуются и блогеры, и редакция: такое уже есть у Комитета, на их «Основе» работают все сайты издательства: vc, tjournal, dtf, и редакция там пишет посты так же, как и обычные пользователи.

Архитектура: однонаправленный поток данных

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

Чтобы всегда знать, кто, зачем и когда записал данные, мы ввели единый источник правды. Пусть правдивое состояние пользователей и постов всегда находится у нас, и мы сами отвечаем за то, чтобы данные обновились в легаси-системах — транслируем все изменения в базы данных, задействуя как можно меньше старого кода. Всю синхронизацию систем друг с другом мы отрубаем — данные везде пишем только мы. Получается, что правда течёт сверху вниз, как в компонентах react.js — от нашей системы к легаси.

Экспорт данных мы построили на celery и RabbitMQ. Получилась полноценная асинхронная архитектура: все посты, которые нужно отправить в легаси, лежат в RabbitMQ, и удаляются оттуда только после того, как данные попадают во все БД. Если с трансляцией что-то пойдёт не так — мы узнаем об этом по переполненной очереди в RabbitMQ.

Инфраструктура

Инфраструктура на проекте — ещё один источник проблем. Там были разные физические серверы, конфигурация которых мутировала в течении десятилетия. ПХП-движок вообще крутился на FreeBSD — такой привет из начала 2000х! Плюс, у нас не было доступа к серверам — нельзя было даже зайти по ssh и посмотреть, что происходит.

Конечно, мы совсем не хотели делать ещё одну систему в этой непрозрачной мешанине — пара недель ушла бы только на попытки разобраться в конфигурации nginx. Решение пришло из мира фронтенда: там часто делают отдельный бекенд для фронтендеров, который который облегчает хождение в основные бекенды — маршрутизирует запросы между микросервисами, переформатирует ответы в удобный фронтенду формат, сохраняет данные авторизации — это называется BFF (Backend for Frontend). В нашем проекте уже был свой BFF — ведь нам нужно рендерить страницы на сервере, чтобы ускорить загрузку и быть понятными для поисковых роботов. Нам ничего не мешает маршрутизировать весь трафик snob.ru, включая статику через свой BFF — таким образом мы заберём полный контроль над трафиком.

Рядом с легаси-инфраструктурой мы развернули свою собственную, где на входе пользователей встречает комбинация из traefik и express.js. Теперь мы сами решаем, какая из систем обрабатывает каждый запрос — каждый новый сервис сам регистрируется в traefik и получает свою долю трафика. Если ни один сервис не хочет обрабатывать запрос — он уходит в express.js, где мы кодом решаем, обработать сервис своим фронтендом, или отдать его в легаси.

Чтобы не надорваться от нагрузки, поверх подключили Cloudflare, который раздаёт статику через свой CDN:

2/3 легаси-трафика отдаётся через CDN

Заодно мы решили ещё одну проблему старой инфраструктуры — пропуская через себя весь трафик, мы наконец-то получили нормальную статистику происходящего в ней. Старый мониторинг строился на основе простой пинговалки в заббиксе: раз в минуту ходим в бекенд, если ответ не ок — шлём СМС админу. Чтобы понять, насколько это плохо — представьте ситуацию, в которой сайт падает от нагрузки и нормально обрабатывает только 50% пользователей. Если пинговалка попадает в ту половину пользователей, для которых сайт работает — админ об этом никогда не узнает. Наверное так же можно проверять работу ядерного реактора — если в контейнменте ничего не горит и не взрывается, значит реактор работает.

Теперь никакой пинговалки нет, а мониторинг строится на 4-х золотых сигналах — количестве запросов, времени ответа, количестве ошибок и запасу на оборудовании.

Графики 4 золотых сигналов в Datadog, в которую мы завели весь мониторинг

Если хоть какой-то параметр выходит из строя, робот на основе машинного обучения шлёт нам алёрт.

Так выглядит алёрт в Campfire — чате внутри нашего корпоративного бейскемпа

Как мы тестировали легаси

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

Получается, что как ни пиши автотесты в нашем коде, мы не можем быть уверены в качестве системы в целом — не существует никакого способа проверить, что когда мы выкатимся на продакшен, ничего не упадёт.

Единственный подход к тестированию, который применим в данном случае — канареечный: когда мы выкатываем всю систему в прод, но показываем её минимальному количеству пользователей. Примерно за три месяца до запуска у нас в продакшене появилась рабочая система — через новый интерфейс можно было написать пост, который появился бы во сразу всех базах. При желании можно было даже вывести этот пост на главную страницу snob.ru! Конечно, система была непроработанной — сначала можно было написать только заголовок и текст, указать автора: ни о каком сложном форматировании речи не шло.

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

Запуск

Несмотря на то, что к дедлайну система была уже в продакшене, оставались самые опасные вещи — переписать DNS и включить боевой стриминг данных реальных пользователей. Опасность была в том, что старая система в совершенно непредсказуемых местах ходила сама в себя (помните вебхуки?). В коде были конструкции вида urlopen('<http://snob.ru/secret_api/secret_endpoint>');! Большая часть этих адресов оставалась работоспособной — почти весь трафик мы перенаправляли в легаси, обрабатывая самостоятельно только нужную нам часть. Но какие-то легаси-адреса всё равно сломалась — к примеру перестал работать старый механизм загрузки фотографий.

Поскольку, не переписывая DNS, мы не могли этого проверить — оставалось тестировать всё на боевой системе. Для этого мы собрали процесс, который позволяет быстро переключить трафик со старого кода на новый и обратно. Чтобы включить новый прод, достаточно было поменять адрес в Cloudflare и раскатать плейбук Ansible с обновлёнными настройками — весь процесс занимал около трёх минут. Чтобы ничего не забыть, сделали простой чеклист в бейскемпе:

Кусок чеклиста, которым мы проверяли продакшен-запуск

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

Что дальше

Сейчас у нас есть движок, в котором решены все проблемы интеграции, а код и основные пользовательские сценарии покрыты тестами. Остаётся потихоньку забирать функциональность у легаси-кода и тушить старые части системы, переводя весь snob.ru на новые рельсы. До встречи через полгода :-)

Команда

Никита Алёшников, бэкенд-разработчик
Фёдор Борщёв, технический директор
Михаил Бурмистров, ведущий фронтенд-разработчик
Самат Галимов, технический директор
Вячеслав Набатчиков, бэкенд-разработчик
Всеволод Скрипник, бэкенд-разработчик, руководитель проекта
Денис Сурков, бэкенд-разработчик
Владимир Тарановский, фронтенд-разработчик

Наш дорогой заказчик:

Марина Геворкян, генеральный директор
Валерия Тищенко, бренд-директор, продакт
Артём Алексеев, дизайнер
Мария Семенюк, директор по маркетингу
Виктория Владимирова, директор по дистрибуции
Борис Тавакалов, ведущий разработчик и хранитель знаний legacy-системы
Михаил Лавкин, системный администратор legacy-системы
Данияр Шекебаев, аналитик
Александр Тарасов, техподдержка

Фабрика фич и просранное время

Когда в компании появляется быстрая разработка (а такое бывает, да), возникает большой соблазн превратить её в фабрику фич. Фабрика фич — это такой маленький заводик, который клепаает фичи одну за одной — без оглядки на реальность. Никто не прогнозирует и не замеряет воздействие на бизнес — все просто пишут код, а продукт тем временем превращается в болото, в котором никто, включая разработчиков, не знает, как должна работать та или иная фича.

Чтобы вылечить такие ситуации, обычно внедряют цикл Шухарта, когда вместо больших фич команда двигается маленькими шажками — выдвигает гипотезу, проверяет её маленькой задачкой, замеряет воздействие на бизнес, и только потом планирует большие задачи. Типичная проблема с внедрением цикла Шухарта  — когда бизнесовым ребятам пофиг на все эти гипотезы: у них прямо сейчас фича горит, надо просто сделать и, вообще, нет времени объяснять.

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

Конечно, в «просранное время» ни в коем случае нельзя переводить гипотезы, которые прошли по продуктовому циклу, но не выстрелили — если вы сели, придумали, как быстро проверить гипотезу, проверили и она не выстрелила — вы молодцы, и никакого времени вы не просрали.

90% фич вылетает в трубу

Наверное, где-то в мире есть ребята, у которых гипотезы не выстреливают с вероятностью 80% или даже 75%. Но у нас с вами это не так. Фича, которую вы пилите прямо сейчас, улетит у трубу с вероятностью 90%. Пользователи не заметят новую кнопку, робот не сработает, потому что годится только для 0,1% заказов, а письмо, которое вы верстали неделю, никто не откроет.

90% фич вылетает в трубу

Повторите про себя пару раз, и как только вы осознаете — вам сразу станет легче жить. Вы перестанете подходить к новым фичам с завышенными ожиданиями (вот сделаем и заживём!). Вы перестанете проектировать раздутое говно — зачем, если вы выкинете это с вероятностью 90%?

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

Помните мой совет со входом через Инстаграм? Зная о том, что этот вход не будет никому нужен с вероятностью 90%, вы сделаете интеграцию не с инстаграмом, а с auth0, чтобы в будущем сразу проверить 10 других способов входа, 1 из которых окажется рабочим.

Просто всегда помните, что ваша гениальная идея с вероятностью 90% — говно.

Мне сказали — я копаю

В любой здоровой компании есть атмосфера критики. Кто бы ни пришёл с идеей: акционер, CEO или линейный сотрудник, его идею обязательно обсуждают и валидируют. Автору задают важные вопросы: «мы это делаем, чтобы что?», «если это не заработает, то из-за чего?» и тд. Такая резка фич, ещё до продакт-менеджера.

Почему-то внутри разработки такая практика обычно умирает. Какие бы странные задачи ни приходили, насколько бы очевидными упрощения в них ни были, программисты делают ровно то, что написано в требованиях. Это грустно, потому что часто программисты — самый большой центр затрат в компании. А что это за центр затрат, который не контролирует, куда он расходует ресурсы?

Мне сказали — я копаю

При этом программисты — прекрасные скептики: долгая работа с алгоритмами, которые описывают вещи из реального мира, приучает к критическому мышлению. Когда твои гениальные построения постоянно разбиваются о сложсочиненные системы в реальной жизни — скептиком становишься сам того не желая.

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

Feature flags

Фиче-флаги — это настройки, которые позволяют не трогая программистов менять поведение программы, к примеру включать и выключать фичи. Типа вот этому пользователю показываем общих в списке друзей, а тому — нет.

Чаще всего набор фиче-флагов формирует фронтенд, и отсылает на бекенд в момент каждого запроса. Так можно легко ставить a\b тесты — просто выбираем две когорты, одной добавляем фичу, а другой — нет, и смотрим на поведение.

Пример реализации — GitHub, который передает фиче-флаги в HTTP-заголовках. Прямо сейчас в API гитхаба таким образом включается-выключается одновременно 30 фич.

Feature flags

Есть ещё одно очень полезное применение фиче-флагов — полное отключение функций приложения в зависимости от среды. К примеру, у нас ЦРМ есть фича — уведомлять пользователя по СМС о статусе заказа. Но я не хочу, чтобы СМС уходили с тестовых стендов или из CI, даже если кому-то хватит ума прописать боевые ключи на них. Поэтому я делаю фиче-флаг ENABLE_NOTIFICATIONS и включаю его только в переменных окружения на проде. По умолчанию флаг выключен, поэтому где мы ни развернули мой бекенд — он никогда не пошлет сообдщений живым людям, если его явно об этом не попросить.

См. также:

  • Подробное объяснение пользы фиче-флагов от самого Мартина Фаулера
  • Launch Darkly — централизованное управление включенностью фич
  • Unleash — то же самое, но бесплатно и self-hosted