01 февр. 2026 г.·7 мин чтения

Миграция с Python на Rust для одного узкого места в сервисе

Планируете миграцию с Python на Rust для одного узкого места? Начните с профилирования, перенесите только медленный путь и не усложняйте деплой для команды.

Миграция с Python на Rust для одного узкого места в сервисе

Как выглядит эта проблема

Узкое место в сервисе редко выглядит сломанным с первого взгляда. Большую часть дня все работает. Запросы проходят, пользователи получают результаты, а команда спокойно выпускает новые функции на Python. Но потом резко вырастает трафик, один job становится больше обычного или клиент запускает более тяжелый запрос, и один участок сервиса начинает тормозить все остальное.

Обычно это заметно сначала по мелким, раздражающим признакам, прежде чем все превращается в инцидент. Страницы загружаются медленнее. Фоновые задачи копятся. На одном инстансе скачет загрузка CPU, пока остальная система ждет. Деплой, который должен быть обычным, превращается в стресс, потому что один endpoint или worker уже не справляется.

В повседневной работе картина обычно такая:

  • один API-роут внезапно начинает отвечать за 2 секунды вместо 200 мс
  • одна задача отчета блокирует очередь на минуты
  • одна функция парсинга или скоринга съедает большую часть CPU
  • одно действие клиента вызывает таймауты у всех, кто идет за ним

Вот почему одна медленная функция так важна. Даже если 95 процентов сервиса работают нормально, медленный участок задает темп всей системе. Он забивает пулы воркеров, задерживает повторные попытки и создает ощущение, что autoscaling все исправит, хотя настоящая проблема сидит в нескольких строках кода.

Python по-прежнему может быть правильным выбором для большей части сервиса. Он быстро меняется, легко читается и хорошо подходит для бизнес-логики, API и связующего кода. Команды начинают попадать в неприятности, когда считают, что медленная производительность означает, будто весь стек выбран неправильно. Чаще всего это значит, что один цикл, один сериализатор, один алгоритм сопоставления или одно преобразование данных требует другого инструмента.

Именно здесь миграция с Python на Rust имеет смысл. Не как полный переписанный проект и не как ставка на то, что новый язык решит все проблемы. Более удачный вариант гораздо меньше и спокойнее: оставить Python-сервис, найти медленный путь и перенести только эту часть на Rust.

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

Сначала найдите медленный участок

Миграция с Python на Rust обычно начинается с догадки. И нередко эта догадка ошибочна. Медленный запрос может выглядеть как проблема Python, но настоящая задержка может приходить из запроса к базе, стороннего API, disk I/O или очереди, которая забивается под нагрузкой.

Начните с цифр из реального трафика или с воспроизведения недавних production-запросов. Измерьте общее время ответа, загрузку CPU и использование памяти сервиса в текущем состоянии. Если CPU остается низким, а задержка растет, Python-код может быть не главной причиной. Если один worker упирается в ядро, и вместе с этим растет время ответа, это уже более сильный сигнал.

Отделяйте время вычислений от времени ожидания. Запрос, который тратит 40 мс на математику и 300 мс на ожидание Postgres, в первую очередь не нуждается в Rust. А запрос, который проводит 250 мс внутри одного шага парсинга, скоринга или преобразования, возможно, нуждается.

Полезен короткий чек-лист:

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

Игрушечные бенчмарки только тратят время. Маленький JSON, чистый кэш или фальшивый датасет из десяти строк могут сделать Python вполне нормальным. А потом production присылает payload на 12 МБ, странный файл клиента или десять тысяч записей — и медленный путь сразу вылезает наружу. Используйте примеры, похожие на те грязные случаи, с которыми ваш сервис уже работает.

Прежде чем трогать Rust, зафиксируйте одну цель. Выберите что-то конкретное, например: «снизить p95 latency с 420 мс до 220 мс» или «уменьшить загрузку CPU на этом endpoint на 35%». Одна цель делает работу честной. И она же помогает понять, когда нужно остановиться.

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

Выберите небольшой участок для Rust

Когда профилирование показало, куда уходит время, не усложняйте первую замену на Rust. Выберите одну функцию, которая сильно нагружает CPU, принимает понятный вход и возвращает понятный результат. Так у команды будет маленькая зона, которую можно изучить, протестировать и объяснить.

Оставьте обработку запросов на Python, если она и так достаточно быстрая. То же относится к валидации, маршрутизации и большей части бизнес-логики. Эти части часто меняются чаще, чем горячий цикл, и Python обычно удобнее, когда продуктовая логика постоянно движется.

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

Плохой первый кандидат — все, что завязано на внешние системы. Вызовы к базе, consumers очередей, проверки auth, повторные попытки и сетевые запросы часто больше ждут, чем считают. Перенос этих частей на Rust в первом заходе добавляет работы, но редко убирает реальное узкое место.

Граница должна укладываться в одно простое предложение. Например: «Python отправляет пачку событий в Rust, а Rust возвращает результаты с оценкой». Если команда не может так же просто описать передачу данных, значит, цель, скорее всего, слишком большая.

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

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

Такой формат делает смешанный сервис Python Rust проще для понимания. Python остается главным для запроса, логирования, auth и работы с базой. Rust берет на себя один дорогой участок, который действительно сжигает CPU.

Небольшой пример делает это понятнее. Допустим, сервис принимает запрос, загружает данные клиента, применяет десять бизнес-правил, а затем считает fraud score для 50 000 транзакций. Оставьте поток запроса и правила в Python. Переносите только цикл fraud scoring, если именно там уходит время CPU.

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

Не усложняйте деплой

Миграция проходит спокойнее, когда production по-прежнему выглядит знакомо. Если ваша команда уже обслуживает один Python-сервис, старайтесь как можно дольше сохранять эту схему. Самый безопасный первый шаг — это часто Rust-библиотека, к которой Python обращается ради одной медленной функции, а не совершенно новый Rust-сервис со своим контейнером, конфигурацией и on-call-нагрузкой.

Такой выбор снижает риск, пусть и в скучных, но важных деталях. У вас остается одна network boundary, один deployable unit и одно место, которое нужно проверить, если что-то сломается. Команда может учить Rust, не одновременно осваивая новый релизный процесс.

Для первой версии по возможности не трогайте:

  • публичный API
  • payload запросов и ответов
  • формат и поля логов
  • алерты, дашборды и коды ошибок

Если клиенты отправляют тот же JSON и получают тот же JSON в ответ, rollout становится намного проще. Support-командам не нужны новые runbook. Старые дашборды по-прежнему помогают, потому что люди могут искать по тем же полям и сравнивать поведение до и после изменения.

Смешанный сервис Python Rust часто работает лучше, чем люди ожидают. Python может оставить за собой web-слой, валидацию, auth и весь связующий код. Rust может взять на себя плотный цикл, который сжигает CPU, или парсер, который обрабатывает слишком много данных. Такое разделение проще отлаживать, чем два сервиса, которые разговаривают по HTTP.

Сохраните и привычный процесс сборки и релиза. Если ваша команда уже собирает один Docker-образ в CI и публикует его одинаковым способом, продолжайте так же. Добавьте один шаг сборки Rust-расширения, а потом публикуйте тот же образ в то же место. Простой деплой Rust обычно означает меньше движущихся частей, а не больше.

Соберите Rust-часть так, чтобы Python импортировал ее как обычный модуль. Если что-то ломается, возвращайте те же status codes и ту же форму ошибки, что и раньше. Такая дисциплина очень помогает на rollout, потому что любое изменение поведения выглядит как реальный баг, а не как побочный эффект новой архитектуры.

Один новый язык — это уже достаточно. Новый язык плюс новый сервис плюс новая операционная нагрузка — вот где команды теряют недели.

Пошаговый план миграции

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

Большинство команд слишком рано переносит слишком много кода. Безопасная миграция с Python на Rust начинается с доказательств, а не с надежды, и держит первое изменение настолько маленьким, чтобы его можно было откатить за минуты.

Сначала зафиксируйте текущее поведение. Напишите тесты вокруг медленного кода, но на этом не останавливайтесь. Сохраните и реальные примеры входных данных с production вместе с ожидаемыми результатами, особенно для грязных случаев: пустые значения, странный Unicode, большие payload и пограничные числа. Эти файлы поймают баги, которые аккуратные unit-тесты пропускают.

Затем сделайте Python-часть скучной. Поместите медленную функцию за один понятный интерфейс со стабильными типами входа и выхода. Если остальной сервис может вызывать этот интерфейс, не заботясь о том, происходит работа в Python или в Rust, миграция станет намного проще.

Обычно хорошо работает такой план:

  1. Зафиксируйте поведение тестами и примерами из реального трафика.
  2. Оберните горячую функцию тонкой границей, чтобы один вызов управлял переключением.
  3. Перенесите только эту функцию на Rust, а остальной сервис оставьте на Python.
  4. Запустите обе версии на одной и той же нагрузке и сравните каждый результат, включая ошибки и крайние случаи.
  5. Включите Rust-путь за флагом или на небольшом срезе трафика, оставив Python-путь как fallback.

Когда сравниваете результаты, делайте это аккуратно. Проверьте порядок, округление, обработку null, кодировку текста и поведение при timeout. «Почти одинаково» часто превращается в production-баг. Проверки построчно кажутся медленными, но потом экономят дни.

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

Пока команда учится, не усложняйте деплой. Один скомпилированный модуль внутри текущего сервиса обычно проще, чем новый Rust-microservice со своей сборкой, логами, алертами и процессом релиза. Олег часто подталкивает команды именно к таким небольшим и обратимым изменениям в первую очередь, потому что это снижает риск и учит команду быстрее, чем большой переписанный проект.

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

Простой пример

Хороший пример такого подхода — worker для импорта файлов. Пользователи загружают большие CSV-файлы, ждут завершения импорта и хотят понятные статусы, если что-то пошло не так. Python-worker уже многое делает хорошо: принимает файл, проверяет колонки, хранит прогресс задачи и превращает ошибки в сообщения, которые люди понимают.

Проблемы начинаются, когда файл становится огромным. Представьте импорт клиента с 1,5 миллиона строк, где каждую строку нужно очистить и сопоставить с существующими записями. Большая часть общего времени уходит не на upload и не на запись в базу. Она уходит на один плотный цикл, который нормализует текст, сравнивает поля и снова и снова оценивает возможные совпадения.

Профилировщик указывает прямо на этот цикл. Он может занимать 70–80 процентов времени CPU, пока остальная часть worker остается вполне обычной. Это сильный кандидат для миграции с Python на Rust, потому что команда может перенести только медленный путь и оставить все остальное как есть.

Python по-прежнему обрабатывает то, что замечает пользователь:

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

Rust заменяет одну функцию: по уже распарсенным строкам и данным для поиска вернуть лучшее совпадение для каждой строки. В итоге worker становится смешанным сервисом Python Rust, но только в одном небольшом месте. Python подготавливает данные, вызывает Rust-код и продолжает работать как раньше.

Деплой тоже остается простым. Команда выпускает тот же worker-образ, только внутри него находится один скомпилированный Rust-extension. Не нужно добавлять новый network service, новую очередь или второй шаг деплоя, пока все еще учатся. Если вызов Rust падает, можно какое-то время откатываться на старый Python-путь.

Со стороны пользователя ничего не меняется. Он по-прежнему загружает тот же файл на том же экране. Он по-прежнему видит те же статусы прогресса. Единственное заметное отличие — скорость. Задача, которая занимала 12 минут, может теперь завершаться за 3, и команда получила этот результат без переписывания всего worker.

Ошибки, которые тратят время

Бенчмарк на реальных запросах
Тестируйте на тех же запросах, крайних случаях и размерах данных, которые уже присылают пользователи.

Одна частая ошибка — слишком доверять профилям на ноутбуке. Сервис, который кажется медленным на машине разработчика, может вести себя совсем иначе при реальном трафике, реальном объеме данных и реальных сетевых условиях. Если профилировать на тихом ноутбуке и предположить, что production будет таким же, можно неделю ускорять не ту функцию.

Команды также теряют время, когда переписывают больше, чем нужно. Если команда только начинает учить Rust, перенос целого модуля может казаться чище, но часто превращает маленькую задачу по производительности в долгий учебный проект. Оставляйте миграцию с Python на Rust узкой. Перенесите горячий цикл, парсер или CPU-heavy worker, затем измерьте еще раз.

Еще одна ловушка — менять поведение одновременно со сменой языка. Если один pull request переписывает бизнес-логику и переносит ее на Rust, отлаживать все становится очень тяжело. Никто не поймет, пришла ли ошибка из новых правил или из новой реализации. Сначала оставьте логику той же. Когда Rust-код начнет совпадать с выводом Python, уже потом улучшайте дизайн.

Многие команды слишком рано дробят один сервис на несколько сервисов. Это добавляет RPC-вызовы, новые шаги деплоя и больше мест, где могут прятаться сбои. Для смешанного сервиса Python Rust shared library или один контейнер часто оказываются спокойнее, пока команда учится. Простой деплой Rust не выглядит эффектно, но экономит часы.

Последняя трата времени появляется уже после релиза. Некоторые команды выпускают Rust-путь, видят один удачный бенчмарк и перестают следить за ним. Это рискованно. Вам все равно нужен мониторинг задержки, частоты ошибок, использования памяти и глубины очереди, особенно в первые дни.

Несколько тревожных признаков подсказывают, что вы уходите не туда:

  • production-цифры не меняются, а переписывание продолжает расти
  • деплои стали сложнее, чем исходное узкое место
  • команда чинит баги переноса вместо улучшения медленного пути
  • память растет, хотя время CPU снизилось

Если избегать этих ошибок, обычно удается и быстрее освоить Rust, и сохранить стабильность сервиса.

Быстрые проверки перед rollout

Задайте понятную границу
Четко задайте входы и выходы, прежде чем команда напишет первый модуль на Rust.

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

Начните с проверки выходных данных. Возьмите выборку реальных запросов, прогоните их через Python-путь и Rust-путь и сравните результаты построчно. Если сервис работает с float, timestamp или сортировкой, заранее определите, что считать «одинаковым». Небольшие различия в формате могут скрывать реальные баги.

Проверка скорости требует двух чисел, а не одного. Median показывает, стали ли обычные запросы быстрее. Worst-case показывает, продолжают ли редкие медленные запросы мешать пользователям. Если median latency падает с 120 мс до 40 мс, но некоторые запросы все еще взлетают до 3 секунд, rollout еще не закончен.

Помогает короткий список перед релизом:

  • прогоните сохраненные запросы и сравните ответы
  • сравните median latency и tail latency на одной и той же нагрузке
  • убедитесь, что сборка, деплой и rollback идут по тому же рабочему процессу команды
  • проверьте, что логи, ошибки и алерты остаются понятными в 2 часа ночи

Деплой должен оставаться скучным. Если команда уже выпускает один контейнер, старайтесь и дальше выпускать один контейнер. Если вы уже используете тот же CI pipeline, оставьте его без изменений. Сейчас не время добавлять новый package manager, новый release tool и новый dashboard.

Вид on-call важнее, чем кажется. Если Rust-код меняет поля логов, текст ошибок или названия метрик, команда может потерять время во время инцидента. По возможности оставляйте старые названия. Если не получается, обновите дашборды и правила алертов до rollout. Команды, которые используют Sentry, Grafana или Prometheus, должны видеть новый путь в привычных местах и с уже понятными им метками.

Еще одна важная проверка — ownership. Назначьте одного человека, который отвечает за первую неделю после релиза, и одного человека, который отвечает за следующий раунд настройки. Rust часто убирает одно узкое место и вскрывает следующее. Если за последующую работу никто не отвечает, миграция остановится сразу после самой трудной части.

Что делать дальше

Начните с самого маленького теста, который вы можете обосновать. Выберите один endpoint или один background worker, который достаточно часто тормозит, чтобы это можно было измерить, но при этом достаточно изолирован, чтобы rollback был скучным. Так первый шаг миграции с Python на Rust останется небольшим и понятным для команды.

Прежде чем кто-то напишет Rust-код, договоритесь об одной цифре, по которой будет понятно, сработал ли пилот. Это может быть p95 latency, jobs per minute, время CPU на запрос или cloud cost для этого worker. Выберите одну метрику, запишите текущий baseline и проверьте ее снова после релиза. Если цифра не сдвинулась в полезную сторону, остановитесь и разберитесь почему.

Простой первый проход выглядит так:

  • Оставьте API или контракт worker на Python без изменений.
  • Перенесите только горячий цикл, парсер, transform или другой вычислительно тяжелый шаг.
  • Выпустите Rust-часть самым простым способом, который поддержит ваша команда.
  • Добавьте переключатель для отката до того, как туда попадет реальный трафик.

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

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

Если вам нужен внешний взгляд, прежде чем вкладывать инженерное время, Олег Sotnikov может помочь оценить небольшой, низкорисковый план миграции в рамках Fractional CTO или архитектурной консультации. Это особенно полезно, когда вы не уверены, нужен ли Rust этому узкому месту вообще, или проблему быстрее решит более удачный дизайн на Python.