PHP → Python Migration

Постепенный перенос backend сайта Maxim Demidov с legacy-PHP (аутсорс, без тестов и понятной архитектуры) на Django — без остановки бизнеса, через маршрутизацию на Nginx. Попутно расследовал и устранил регулярные падения прода.

Ускорил время отклика на 90% (1,5–3 с → 200 мс)Устранил регулярные падения сервераНаладил доставку заявок в CRM без потерь

Контекст

Когда я пришёл в компанию, 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 с legacy-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, регулярные аварийные падения полностью прекратились — что стало дополнительным подтверждением диагноза.

График нагрузки сервера до и после исправления: слева резкие скачки CPU до 100%, справа — стабильная нагрузка ниже 50%
Нагрузка сервера по времени. Слева — резкие скачки до 100%, после которых сервер отключался. После замены PHP-фильтрации на реализацию на Python нагрузка перестала подниматься выше ~50% — проблема устранена окончательно.

Результат

После завершения миграции компания получила полностью управляемый backend, который развивался уже внутренней командой:

  • ключевые API ускорены примерно до целевых показателей;
  • сайт стал работать значительно быстрее по сравнению со старой реализацией;
  • исчезли регулярные аварийные падения системы;
  • обработка заявок стала стабильной;
  • появились логирование и контроль состояния сервисов;
  • развитие функциональности перестало зависеть от внешнего подрядчика.

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

← все кейсы