Хабрахабр

Как мы разрабатывали Туту.ру — вечный вопрос технического долга


Это одно из самых крутых облегчений проекта. На картинке — график суммарного времени, затрачиваемого CPU на обработку всех пользовательских запросов. В конце видно переход на PHP 7.0. с версии 5.6. Это 2016 год, переключение во второй половине дня с 24 ноября.

Для этого мы перемалываем огромное количество расписаний, собираем в кэш ответы множества систем авиакомпаний и периодически делаем невероятно длинные join-запросы к базе данных. Туту.ру с точки зрения вычислений — это в первую очередь возможность купить билет из точки А в точку Б. С недавнего времени критичные по производительности участки стали рефакториться на Go. В целом мы написаны на PHP и до недавних пор были полностью на нём (если язык правильно готовить, то можно даже строить на нём системы реального времени).

Причём это происходит быстрее, чем нам бы хотелось. У нас постоянно возникает технический долг. Плохая: по мере роста поддерживаемой функциональности техдолг тоже пропорционально растёт. Хорошая новость: его не надо закрывать весь.

Вот ты что-то предсказал не так, как архитектор, то есть совершил ошибку прогнозирования или принимал решение в условиях недостаточной информации. Вообще технический долг — это плата за ошибку при принятии решения. Дальше можно сразу поменять, а можно подождать. В какой-то момент понимаешь, что надо что-то менять в коде (часто на уровне архитектуры). Поэтому хорошая практика — время от времени реструктуризировать его. Если подождал — на техдолг набежали проценты. Ну или признавать себя банкротом и писать весь блок заново.

Как всё начиналось: монолит и общие функции

Проект Туту.ру начинался в 2003 году как обычный веб-сайт Рунета тех времён. То есть это была куча файликов вместо базы данных, PHP-страницы на фронте HTML+JS. Там была пара отличных хаков моего коллеги Юрия, но это лучше он сам когда-нибудь расскажет. Я присоединился к проекту в 2006 году сначала как внешний консультант, который мог помочь как советом, так и кодом, а потом, в 2009-м, перешёл в штат на позицию технического директора. В первую очередь нужно было навести порядок в направлении авиабилетов: это была нагруженная и самая сложная по архитектуре часть.

Раздел авиабилетов мы решили делать как отдельный проект, то есть объединялось всё это только на фронте. В 2006 году, напомню, было расписание электричек и была возможность купить билет на поезд. На тот момент код казался нам нормальным, но несколько недоделанным. Все три проекта (расписания электричек, ж/д и авиа) в итоге были написаны по-своему. Потом он старел, обкладывался костылями и на ж/д-направлении превратился в тыкву к 2010 году. Неперфекционистским.

Рефакторить было нереально: проблемы были в архитектуре. В ж/д мы техдолг отдать не успели. В итоге оставили на фронте только старые URL, а дальше блок за блоком переписывали. Решили снести и переделать всё заново, но это тоже было сложно на живом проекте. В качестве основы взяли подходы, использованные за год до этого при разработке авиационного направления.

Тогда было понятно, что это не единственный способ, но для нас разумных альтернатив не было. Переписывали на PHP. Из альтернатив были безумно производительные C и C++, но любые пересборки или внедрения изменений на них тогда напоминали кошмар. Выбрали его потому, что уже были опыт и наработки, было понятно, что это неплохой язык в руках senior-разработчиков. Были кошмаром. Ладно, не напоминали.

NET с точки зрения высоконагруженного проекта даже не рассматривали. MS и весь . Java — хороший вариант, но она требовательна к ресурсам по памяти, никогда не прощает junior-ошибок и тогда не давала возможности выпускать релизы быстро — ну или мы такой не знали. Тогда вариантов, кроме Linux-based, не было вообще. JS — чисто под фронт. Python мы и сейчас не рассматриваем как бекэнд, только для задач работы с данными. Go не было. Ruby on Rails — разработчиков тогда (да и сейчас) было не найти. Остался PHP. Оставался ещё Perl, но эксперты оценили его как неперспективный для веб-разработки, поэтому тоже отказались от него.

Где-то лучше одно, где-то — другое. Следующая холиварная история — это PostgreSQL против MySQL. В целом тогда хорошей практикой было выбирать то, что получалось лучше, поэтому мы выбрали MySQL и его форки.

Это зачатки современного API-centric-подхода, когда у каждой библиотеки есть фасад наружу, за который можно дёргать прямо внутри кода из других частей проекта. Подход разработки был монолитным, тогда других подходов просто не было, но с ортогональной структурой библиотек. То есть что-то вроде test-driven-development, но пикселизированное и страшное. Библиотеки писались «слоями», когда каждый уровень имеет на входе определённый формат и отдаёт дальше в код тоже определённый формат, и между ними крутятся юнит-тесты.

Но при этом кодовая база разных проектов довольно сильно пересекалась на системном уровне. Всё это размещалось на нескольких серверах, что позволяло масштабироваться под нагрузкой. И затрагивали часто. Это по факту означало, что изменения в проекте ж/д могли затронуть наше же авиа. А авиа работает с ней же, следовательно, нужно совместное тестирование. Например, в ж/д надо было расширить работу с платежами — это доработка общей библиотеки. Даже на 2009 год метод был довольно передовым. Зависимости мы экранировали тестами, и это было более-менее нормально. Было и пересечение по базам данных, что приводило к неприятным эффектам в виде тормозов по всему сайту при локальных проблемах в одном продукте. Но всё равно нагрузка могла с одного ресурса сложить другой. Ж/д убивал авиа несколько раз по диску из-за тяжёлых запросов к базе данных.

Монолит как есть. Масштабировали мы добавлением инстансов и балансировкой между ними.

Эпоха шины

Дальше мы пошли по довольно маргинальному пути. С одной стороны, начали выделять сервисы (сегодня этот подход называется микросервисным, но мы не знали слова «микро»), но для взаимодействия начали использовать шину для передачи данных, а не REST или gRPC, как это делают сейчас. Выбрали AMQP как протокол, а RabbitMQ — как брокер сообщений. К тому времени мы довольно лихо освоили запуск демонов для PHP (да-да, там имеется вполне работающая реализация fork() и всего остального для работы с процессами), поскольку довольно долго в монолите использовали для распараллеливания запросов к системам бронирования такую вещь, как Gearman.

Какие-то сетевые потери, ретрансмиты, задержки. Сделали брокер поверх кролика, и оказалось, что всё это под нагрузкой не особо живёт. В общем, много узнали. Например, кластер из нескольких брокеров «из коробки» ведёт себя несколько иначе, чем заявлено разработчиком (никогда такого не было, и вот опять). Например, самый нагруженный сервис по RPS имеет при 400 rps, 99-й перцентиль round-trip от клиента до клиента включая шину и обработку сервисом порядка 35 ms. Но в итоге получили требуемые для сервисов SLA. Сейчас суммарно на шине мы наблюдаем около 18 krps.

Его мы сразу писали без монолита на сервисной архитектуре. Потом появилось направление автобусов. Да, всё это крутилось на виртуальных машинах, внутри которых демоны на PHP общаются через шину. Поскольку писалось всё с нуля, то получилось очень хорошо, быстро и удобно, хотя и приходилось постоянно дорабатывать инструменты для нового подхода. На 2014-й про это только начинали говорить, однако на прод мы такой подход не рассматривали. Демоны запускались внутри Docker-контейнеров, но никаких решений для оркестрации типа Openshift или Kubernetes тогда не было.

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

Поэтому параллельно начали принимать точечные меры по улучшению жизни монолита. Переезд на сервисы — дело хорошее, но долгое, а с нагрузкой и надёжностью надо разобраться уже сейчас. е. Разделили бекэнды на типы продуктов, т. п. стали более гибко управлять роутингом запросов в зависимости от их типа: авиа отдельно от ж/д и т. Когда знали, что в железных дорогах, например, — пик новогодних продаж, то добавляли несколько инстансов виртуальных машин. Можно было прогнозировать нагрузку, масштабировать независимо. Сейчас ФПК и другие перевозчики сделали много билетов со стартом продаж за 60, 90 и даже 120 дней, и этот пик размазался. Он начинался тогда ровно за 45 дней до последнего рабочего дня года, и 14-15 ноября у нас была удвоенная нагрузка. Но про сезонность билетов и пути миграции дембеля мои коллеги из ж/д лучше расскажут, а я продолжу про архитектуру. Но в последний рабочий день апреля всегда будет нагрузка на электрички перед майскими, и есть ещё пики.

Это было важно потому, что она опасно росла, и падение было критичным. Где-то в 2014-м начали дербанить большую базу данных на много маленьких. Стоит отметить, что для распределения нагрузки и масштабирования мы использовали для чтения реплики. Мы стали выделять отдельные маленькие базочки (на 5–10 таблиц) под конкретный функционал, чтобы сбои меньше аффектили другие сервисы, и чтобы всё это можно было легче масштабировать. Воспоминания о таких периодах до сих пор вызывают неприятный холодок где-то между ушами. Восстановление реплик для большой базы после сбоя репликации могло занимать часы, и всё это время приходилось «лететь на честном слове и на одном крыле». Поэтому мы используем Github Orchestrator, который автоматизирует работу с репликами и proxySql для распределения нагрузки и защиты от сбоев конкретной БД. Сейчас у нас — около 200 инстансов разных баз, и администрирование руками такого количества инсталляций — дело трудоёмкое и ненадёжное.

Как сейчас

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

Переезд на него прошёл с небольшим геморроем, на весь проект от начала тестов до полного перевода всего продакшна ушло чуть более полугода, но зато после этого потребление ресурсов упало почти вдвое. Когда вышел PHP 7, мы увидели в тестах очень большой прогресс в производительности и снижении потребления ресурсов. График времени загрузки CPU — вверху поста.

Стоит сказать, что задача заменить весь монолит на сервисы в явном виде не ставится. Монолит сохранился до сих пор и, по моей оценке, составляет примерно 40 % от кодовой базы. При этом монолит покрыт тестами так, что мы можем деплоиться два раза в неделю с достаточным уровнем качества. Двигаемся прагматично: всё новое делается на микросервисах, если же надо доработать старый функционал в монолите, то стараемся перевести его на сервисную архитектуру, если только доработка не совсем уж мелкая. Нагрузочного тестирования почти не делаем. Фичи покрываются по-разному, юнит-тесты довольно полные, UI-тесты и Acceptance-тесты покрывают почти весь функционал портала (у нас около 15 000 тест-кейсов), тесты на API более-менее полные. Мы генерим нагрузку, если видим, что прошлый прогон на старом релизе отличается по таймингам, смотрим, насколько критично. Точнее, у нас стейджинг похож на прод по структуре, но не по мощности, и обложен такими же мониторингами. В любом случае все фичи выходят под рубильником, чтобы можно было в любую секунду отключить, если что-то пойдёт не так. Если новый релиз и старый примерно одинаковые, то выпускаем в прод.

Потом переходим на 2 %, на 5 %, на 10 % и так доходим до всех юзеров. Тяжёлые фичи всегда тестируем под 1 % пользователей. То есть всегда можем увидеть атипичную нагрузку до убивающего серваки всплеска и отключить заранее.

Это хороший способ разрубать гордиев узел, когда локальный рефакторинг уже не помогает. Там, где было нужно, мы брали (и будем брать) 4-5 месяцев на проект реинжиниринга, когда команда фокусируется на конкретной задаче. За два месяца после реинжиниринга выросли на порядок по клиентам за счёт фич. Так мы сделали несколько лет назад с авиа: переделали архитектуру, сделали — сразу получили моментальное ускорение в разработке, смогли запустить много новых фич. Радость. Стали более аккуратно управлять ценами, подключением партнёров, всё стало быстрее. Чтобы оставаться в бизнесе, необходимо развиваться. Надо сказать, сейчас пришла пора поступить аналогичным образом ещё раз, но такова судьба: способы построения приложений меняются, появляются новые решения, подходы, инструменты.

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

Часть микросервисов пишем на Golang: вычислительная скорость и работа с памятью там отличные. На сегодняшний день мы используем для обмена информацией между сервисами не только кролика, но и REST, и gRPC. В принципе, выбранный подход позволяет разрабатывать сервисы практически на любых языках, но мы решили ограничивать зоопарк, чтобы не увеличивать сложность системы. Был заход на внедрение поддержки nodeJS, но в итоге оставили ноду только для серверного рендеринга, а бизнес-логику оставили на PHP и Go.

Задача в течение года-полутора — 90 % всего крутить внутри платформы. Сейчас идём в микросервисы, которые будут работать в Docker-контейнерах под оркестрацией OpenShift. Так быстрее деплоиться, быстрее проверять версии, меньше отличия прода от devel-окружения. Почему? е. Разработчик может думать больше о фиче, которую реализует, а не о том, как развернуть окружение, как его настроить, где запустить, т. Опять же — вопросы эксплуатации: микросервисов много, их надо автоматизировать по управлению. больше пользы. Вручную — очень большие расходы, риски ошибок при ручном управлении, а платформа даёт нормальное масштабирование.

Сейчас около 1 миллиона пользователей в день приходит на портал. Каждый год у нас рост нагрузки — на 30–40 %: всё больше людей осваивает фокусы с Интернетом, перестаёт ходить в физические кассы, мы добавляем новые продукты и фичи к уже существующим. Что-то совсем не требует вычислительных ресурсов, а, например, поиски — довольно ресурсоёмкая составляющая. Разумеется, не все пользователи одинаково полезны генерируют нагрузку. Всё остальное в сравнении с поиском билета внутри ж/д-систем и авиа достаточно простое. Там одна-единственная галочка «плюс-минус три дня» в авиации увеличивает нагрузку в 49 раз (при поиске туда-обратно получается матрица 7 на 7). Самое лёгкое в ресурсах — приключения и поиск туров (там не самый простой с точки зрения архитектуры кэш, но всё равно туров куда меньше, чем комбинаций билетов), потом — расписание электричек (оно легко кэшируется стандартными средствами), а уже потом — всё остальное.

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

Показать больше

Похожие публикации

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»