Контекст
На момент моего прихода в компанию backend сайта Maxim Demidov уже существовал и был написан на PHP внешней аутсорс-командой. По словам руководства, поддержка обходилась дорого, а скорость развития продукта оставляла желать лучшего. После прекращения сотрудничества с аутсорсом компания осталась с системой, которую внутри практически никто не понимал.
Фронтенд-разработчик, занимавшийся сайтом, был сильно загружен бизнес-задачами и не мог полноценно поддерживать backend. В какой-то момент стало понятно, что дальнейшее развитие существующей системы становится проблемой.
Основные жалобы бизнеса были конкретными:
- многие API отвечали по 1,5–3 секунды;
- новый функционал внедрялся тяжело;
- backend регулярно создавал проблемы в эксплуатации;
- сервер по неизвестным причинам аварийно завершал работу примерно раз в несколько дней;
- периодически возникали проблемы с обработкой заявок от клиентов.
Меня наняли как backend-разработчика и попросили оценить ситуацию, понять ограничения системы и предложить решение.
Почему появился новый backend
Перед началом работы уточнялись бизнес-требования: предполагаемая нагрузка, количество пользователей, планы по развитию продукта и требования к скорости разработки.
После обсуждения стало понятно, что система не относится к высоконагруженным проектам. Основным требованием была возможность быстро развивать продукт и внедрять новые функции. Дополнительно руководство хотело постепенно переходить на Python и строить новые внутренние продукты вокруг этого стека, чтобы не поддерживать несколько независимых технологических направлений.
После анализа новый backend был предложен на Python, а основным фреймворком выбран Django. Причина прагматичная: для небольшой команды скорость поставки функциональности значительно важнее теоретической производительности. Django давал готовые инструменты для работы с БД, построения REST API, авторизации, админ-интерфейсов, миграций и организации архитектуры — это позволяло сосредоточиться на бизнес-логике, а не на инфраструктурном коде.
Первое знакомство со старой системой
Документация формально присутствовала, но практической пользы почти не приносила. Автоматических тестов не было. После ухода аутсорса внутри компании практически никто не мог уверенно объяснить устройство системы. Даже навигация по проекту вызывала сложности: кодовая база состояла из большого количества файлов и директорий, структура которых плохо отражала реальные бизнес-домены.
Кроме этого регулярно возникали ситуации, когда определённый функционал переставал работать без видимых причин, а спустя время снова начинал работать корректно.
В какой-то момент стало очевидно, что стоимость полноценного погружения в существующую архитектуру может оказаться сопоставимой со стоимостью создания новой системы.
Архитектурный подход
Решено было не выполнять полный перенос данных и не менять существующую базу. Сначала изучалась структура БД и качество модели данных — вывод: схема в целом жизнеспособна, хотя и обнаружилось большое количество устаревших таблиц и лишних полей, оставшихся от предыдущих этапов развития. Поэтому новый backend строился поверх существующей базы, без отдельной миграции данных.
Стек новой системы:
- Django + Django REST Framework;
- MySQL;
- Elasticsearch;
- Nginx;
- набор фоновых Python-воркеров для служебных задач.
От Docker сознательно отказались: сервер имел ограниченный объём оперативной памяти, а количество зависимостей было небольшим, поэтому дополнительный уровень контейнеризации не приносил заметной пользы.
Поэтапная миграция без остановки бизнеса
Одним из главных решений было отказаться от единовременного переключения всей системы. Вместо этого новый Django-backend некоторое время работал параллельно со старым PHP. Перед Nginx находились обе системы, а переключение выполнялось постепенно на уровне маршрутов.
Сначала реализовывался отдельный API, тестировался на реальных данных, проверялась корректность поведения — и только потом соответствующий маршрут в Nginx переключался на новый backend:
каталог товаров → поиск → фильтрация → оформление заявок → остальные функции
Python Python Python Python
(оставшаяся часть системы продолжает работать через PHP)
Такой подход позволял выпускать изменения без остановки бизнеса, быстро откатывать отдельные части системы, минимизировать риски и локализовывать проблемы на конкретном участке функциональности. По мере завершения миграции количество запросов, обслуживаемых старым backend, постепенно уменьшалось, пока PHP не был полностью выведен из эксплуатации.
Что именно было переписано
Каталог товаров, карточки товаров, поиск, фильтрация, оформление заявок, интеграция с CRM, сортировка товаров, посадочные страницы, backend для работы с Elasticsearch.
Особенно сложными оказались две области:
- Система фильтрации каталога. Она не просто отбирала товары по параметрам, а динамически пересчитывала доступные варианты фильтров: если после применения набора условий по какому-либо параметру больше не оставалось подходящих товаров, такой фильтр автоматически скрывался.
- Посадочные страницы. Каждая представляла собой заранее настроенный набор условий поиска, поверх которого пользователь мог накладывать дополнительные фильтры. Из-за большого количества комбинаций условий именно здесь возникало значительное количество логических ошибок, которые приходилось выявлять и устранять.
Работа с производительностью
После переноса backend с легаси-PHP на Django появилась отдельная задача: сократить время отклика ключевых страниц примерно с 1,5–3 секунд до порядка 200 мс. Способ достижения показателей определял самостоятельно.
Для анализа использовал Django Debug Toolbar, профилирование SQL-запросов и замеры времени на отдельных участках обработки. Профилирование показало, что основное время терялось не в бизнес-логике, а в неоптимальной работе с базой данных.
Работу вёл поэтапно:
- Устранил N+1 запросы в формировании данных. Там, где раньше по каждому товару шли отдельные обращения к базе за характеристиками и связями, теперь всё нужное предзагружалось несколькими запросами и раскладывалось в структуры в памяти — число запросов перестало зависеть от количества товаров.
- Убрал выборку ненужных полей. В подсчёте вариантов фильтров вместо полных объектов со всеми полями запрашивал только необходимые колонки в облегчённом виде.
- Перестроил тяжёлые условия. Ограничение по товарам, которое разворачивалось в перечисление тысяч идентификаторов, заменил на соединение таблиц.
- Добавил кэширование промежуточных результатов в пределах одного запроса, чтобы не пересчитывать одно и то же для разных фильтров.
Общий принцип был единым — не заставлять базу и ORM делать лишнюю работу. После поэтапной оптимизации время обработки ключевых запросов удалось сократить до целевых показателей.
Надёжная обработка заявок и воркеры
Одним из важных направлений стала автоматизация обработки клиентских заявок и их интеграция с CRM. В старой системе процесс был недостаточно надёжным: при высокой нагрузке или других проблемах PHP выдавал ошибки обработки, и для продолжения работы требовалось ручное вмешательство. Цель была построить систему, гарантирующую сохранность заявок и их доставку в CRM даже при внешних сбоях.
Вынес обработку заявок в отдельные фоновые процессы. Веб-приложение отвечало только за приём запроса, валидацию данных и сохранение заявки. Вся дальнейшая обработка — интеграция с CRM и бизнес-логика — выполнялась независимо в постоянно работающих Python-воркерах. Это изолировало критический процесс от жизненного цикла веб-приложения и исключило влияние на обработку заявок со стороны:
- пиковых нагрузок на сайт;
- длительных запросов к внешним сервисам;
- временной недоступности CRM;
- ошибок в интеграциях.
Даже при подобных проблемах веб-приложение продолжало стабильно принимать новые заявки.
Очередь с повторными попытками. Взаимодействие с CRM шло через очередь с механизмом retry: если CRM была недоступна или возвращала ошибку, заявка не терялась, а оставалась в очереди и обрабатывалась повторно после восстановления внешней системы.
Контроль недоступности CRM. Реализовал мониторинг времени ожидания заявок в очереди: если заявка оставалась необработанной дольше допустимого, система автоматически слала уведомление в Telegram — это позволяло оперативно обнаружить проблему.
Гарантия корректной обработки. Ключевым требованием было исключить ситуацию, когда
пользователь получает подтверждение создания заявки, хотя фактически она не сохранена.
Создание заявки было построено как атомарная транзакция: ответ 200 OK возвращался только
после успешного завершения всех критичных операций сохранения. При ошибке на любом этапе
транзакция полностью откатывалась, а пользователь получал сообщение об ошибке. Успешный
ответ сервера гарантировал, что заявка уже надёжно сохранена и поставлена на обработку.
В результате работа веб-приложения перестала зависеть от доступности CRM и других внешних сервисов. Даже при временных сбоях интеграций заявки не терялись, а гарантированно проходили обработку после восстановления зависимых систем, — это заметно повысило надёжность одного из наиболее важных бизнес-процессов компании.
Что сделал бы сегодня. На тот момент отдельные фоновые процессы были оптимальным решением с учётом инфраструктурных ограничений и требований проекта: на том сервере была крайне ограничена оперативная память — настолько, что мы не могли развернуть на нём даже Docker. Сегодня аналогичную задачу, скорее всего, реализовал бы на специализированной системе очередей — например Celery + Redis: это упростило бы масштабирование, централизованный мониторинг, управление retry и сопровождение. При этом заложенные архитектурные принципы считаю правильными и использовал бы снова: отделение приёма заявки от её обработки, атомарность сохранения, устойчивость к отказам внешних систем, гарантированную обработку и своевременное обнаружение проблем.
Elasticsearch и генерация индекса
Система обслуживала поиск по каталогу объёмом около 440 тысяч записей. Старый backend каждые 20 минут генерировал специальную выгрузку для поискового индекса: файл формировался около пяти минут и сохранялся в формате YML.
После анализа механизм был переработан — генерация переведена на Python, формат изменён на JSON, а процесс формирования данных оптимизирован. Это ускорило подготовку поискового индекса и снизило нагрузку на систему.
Расследование регулярных падений сервера
Помимо архитектурных проблем существовала ещё одна серьёзная: примерно раз в два дня production-сервер переставал нормально работать. Первой обычно деградировала база данных, после чего становился недоступен и backend.
В компании существовало множество версий происходящего — обсуждали проблемы хостинг-провайдера, ошибки MySQL, возможные вирусы, нехватку ресурсов сервера. Но всё это были лишь предположения: объективных данных ни у кого не было.
Поэтому первым делом — не искать проблему вслепую, а построить наблюдаемость. Самостоятельно развернул стек мониторинга:
- Prometheus;
- Grafana;
- Loki;
- node-exporter;
- process-exporter.
После этого несколько дней просто наблюдал за системой. Мне было важно не поймать один конкретный сбой, а понять, существует ли закономерность.
Со временем обнаружилась корреляция: перед каждым падением практически одновременно начинали расти два графика — нагрузка от PHP-процессов и нагрузка на MySQL. Причём почти синхронно: чем сильнее нагружался PHP, тем больше запросов получала база.
Это позволило исключить версии о нестабильном провайдере или неисправности базы. Стало понятно, что MySQL не была причиной — она лишь переставала справляться с нагрузкой, которую создавало приложение. Через process-exporter было видно, что именно PHP-процессы постепенно потребляют всё больше ресурсов.
Полностью разбираться в устройстве чужого легаси-кода смысла не было — без документации это заняло бы много времени и не гарантировало результата. Вместо этого была локализована проблемная область: анализ показал, что основная нагрузка связана с реализацией «умной» фильтрации каталога. После того как этот функционал был отключён в старом PHP backend и заменён новой реализацией на Python, регулярные аварийные падения полностью прекратились — что стало дополнительным подтверждением диагноза.
Результат
После завершения миграции компания получила полностью управляемый backend, который развивался уже внутренней командой:
- ключевые API ускорены примерно до целевых показателей;
- сайт стал работать значительно быстрее по сравнению со старой реализацией;
- исчезли регулярные аварийные падения системы;
- обработка заявок стала стабильной;
- появились логирование и контроль состояния сервисов;
- развитие функциональности перестало зависеть от внешнего подрядчика.
Самым важным результатом для бизнеса стало не столько ускорение сайта, сколько появление платформы, которую компания могла самостоятельно развивать и сопровождать без привлечения аутсорс-команды.