Новый Celery worker на VPS уже запущен, в логах написано «ready», и руки тянутся включить его немедленно. Старый worker на Render при этом ещё доделывает свои задачи. Если оба контура подключены к одному брокеру и слушают одни и те же очереди, второй worker не ускорит работу. Он просто начнёт брать то, что вы ещё не решили, кому принадлежит. А если контуры смотрят в разные брокеры, вы получите две отдельные очереди и вопрос, в какой из них осталась работа.
Поэтому переезд фоновых воркеров лучше воспринимать не как копирование процесса, а как передачу ответственности за каждую задачу. В статье разберём, из каких частей состоит контур, где в каждый момент лежит задача, как остановить приток новых и дать старым закончиться, что делать с задачей, оборвавшейся после внешнего действия, и как убедиться, что всё сошлось.
Четыре части, которые легко принять за одну
В документации Render фоновый worker описан как процесс, который обычно получает задания из очереди и выполняет длительные операции вне критического пути HTTP-запроса (Render: Background workers). Это точное, но слишком сжатое описание для плана переезда. В нём спрятаны четыре самостоятельные роли, и у каждой своя судьба при миграции.
- Производители задач. Веб-приложение, планировщик, обработчики вебхуков, скрипты. Всё, что вызывает
delay()илиapply_async(). Они решают, сколько работы появится и в какую очередь она попадёт. - Брокер. Хранилище сообщений между производителем и исполнителем. Redis, RabbitMQ или другой транспорт, который у вас настроен. Брокер не принадлежит worker: его можно оставить на месте, перенести или заменить, и от этого решения зависит почти всё остальное.
- Исполняющие процессы. Сами worker с их пулом, concurrency, списком очередей и настройками подтверждений.
- Хранилище результатов. Если вы используете result backend, там лежат статусы и возвращаемые значения. Часть проектов его не включает вовсе, и тогда о судьбе задачи можно судить только по побочным эффектам в базе или во внешних системах.
Полезно прямо выписать, где каждая из четырёх частей живёт сегодня и где будет жить после переезда. Типичная ошибка: перенесли только исполнителей, а брокер остался внутри экосистемы старой платформы, и на VPS worker ходит к нему через публичный адрес. Или наоборот: брокер переехал, а один забытый производитель продолжает писать в старый.
Три схемы переезда
От расположения брокера зависит, какой из сценариев у вас получится.
| Схема | Что происходит с ожидающими задачами | Главный риск |
|---|---|---|
| Брокер остаётся на месте, переезжают только worker | Ожидающие сообщения никуда не деваются, новый worker подключается к тому же брокеру | Старые и новые worker одновременно читают одну очередь |
| Брокер переезжает вместе с worker | Сообщения в старом брокере сами не появятся в новом | Остатки очереди остаются в прежнем хранилище и тихо ждут, пока кто-то их заберёт |
| Меняется и тип брокера | Формат хранения и семантика подтверждений другие | Поведение при обрыве и повторной доставке перестаёт совпадать с привычным |
Первая схема самая мягкая, потому что очередь остаётся единой, но именно в ней проще всего получить параллельную работу двух контуров. Вторая и третья требуют явного решения о каждом сообщении старого брокера. Переезд самого приложения и базы данных мы разбирали отдельно в материале про перенос приложения, PostgreSQL и загруженных файлов; здесь предполагается, что общая картина переезда у вас уже есть, а нас интересует именно очередь.
Где на самом деле лежит задача
Вопрос «сколько задач в очереди» кажется простым, пока не выясняется, что у задачи как минимум три состояния, и они хранятся в разных местах.
- Ожидает. Сообщение лежит у брокера в очереди и никем не взято.
- Взято, но не выполнено. Worker уже получил сообщение. Он может выполнять его прямо сейчас или держать в предзагруженном буфере (prefetch), пока свободен только один из процессов пула. Для брокера это сообщение уже не «ожидает», но и не «закончено».
- Завершено. Задача отработала, подтверждение отправлено, результат (если он есть) записан.
Опасная зона посередине. То, будет ли сообщение подтверждено до выполнения или после, определяется настройками Celery, в частности режимом подтверждения задач, а то, как и когда брокер вернёт неподтверждённое сообщение в очередь, зависит от самого брокера и его параметров. Поэтому нет универсального ответа «если worker умрёт, задача вернётся». В документации Celery прямо сказано, что при принудительном завершении процесса исполняемая работа может быть потеряна, а возможность повторной доставки зависит в том числе от подтверждений задач (Celery: Workers guide). Это и есть главная оговорка всего переезда: любые рассуждения о безопасном выключении старого контура верны только для вашей конкретной связки брокера и настроек.
Что проверить в конфигурации до начала
Прежде чем планировать остановку, выпишите из проекта несколько вещей. Это не абстрактная гигиена: каждая из них влияет на поведение в момент переключения.
- Подтверждает ли worker задачу до начала выполнения или после его окончания, и менялась ли эта настройка для отдельных задач.
- Что происходит с задачей при потере соединения с worker: возвращается ли она в очередь, и через какое время.
- Есть ли у задач ограничения по времени и политика повторов, в том числе автоматические повторы внутри самой задачи.
- Сколько сообщений worker забирает заранее и сколько процессов в пуле.
- Какие именованные очереди существуют и какой worker какую слушает.
- Есть ли отложенные задачи с будущим временем запуска (countdown, eta) и периодические задачи от планировщика.
Отложенные задачи заслуживают отдельного внимания. Они могут числиться «ожидающими», хотя до их времени ещё далеко, а worker, получивший такое сообщение, может удерживать его у себя до наступления срока. При остановке такого worker вопрос «что стало с этим сообщением» зависит от брокера и настроек, и проверять его надо на стенде, а не надеяться на удачу.
Сначала закрыть кран, потом ждать
Порядок остановки определяется простой логикой: нельзя дренировать ёмкость, в которую продолжает литься вода. Сначала прекращаем появление новых задач, потом даём старым исполнителям закончить взятое.
Остановка производителей
Здесь важна полнота. Перечислите всех, кто ставит задачи:
- веб-процессы приложения (в том числе второстепенные сервисы и админка);
- планировщик периодических задач, если он запущен отдельным процессом;
- вебхуки внешних систем, которые приводят к постановке задач;
- ручные скрипты и консольные команды, запускаемые людьми;
- другие задачи Celery, которые порождают подзадачи, цепочки и группы.
Последний пункт часто забывают. Выполняющаяся задача может в конце поставить следующую. Значит, даже после остановки внешних производителей очередь может пополняться изнутри, пока работают старые исполнители. Это не значит, что дренировать нельзя; это значит, что нужно понимать, где в цепочках такие места, и смотреть на длину очереди во времени, а не один раз.
Практически остановка производителей может означать режим обслуживания для веб-части, временное отключение планировщика и отказ внешним системам принимать вебхуки или пауза на их стороне. Какой из способов у вас реален, зависит от приложения. Если полностью остановить приём нельзя, заранее решите, куда пойдут новые задачи на время окна: например, в отдельную новую очередь, которую пока слушает только новый контур. Но этот вариант требует, чтобы маршрутизация очередей была настроена одинаково и в коде производителя, и на новом worker.
Мягкая остановка старого worker
Когда новых задач нет, старым исполнителям нужно дать закончить. Согласно документации Celery, сигнал TERM запускает warm shutdown: worker перестаёт брать новые задачи и ожидает завершения текущих (Celery: Workers guide). Именно этот сигнал и есть основа управляемого drain.
Тут есть три нюанса, которые стоит держать в голове.
- Платформа может ограничить время остановки. Если процесс не завершился за отведённый срок, его остановят принудительно, независимо от того, чем он занят. Warm shutdown от этого не защищает. Поэтому длинные задачи нужно оценивать заранее: самая долгая из них должна укладываться в окно, которое вам реально дадут.
- Warm shutdown не переносит очередь. Он только позволяет закончить то, что worker уже выполняет. Всё, что осталось в брокере, останется там же и будет ждать следующего читателя.
- Он не даёт exactly-once. Задача, которую worker успел начать, будет доведена до конца, но это ничего не говорит о том, что было бы при сбое посреди выполнения.
Если вы управляете остановкой сами, например на VPS в обратную сторону или во время репетиции, схема выглядит так: послать TERM основному процессу worker, наблюдать за журналом и списком активных задач, дождаться завершения процесса. Не следует подменять ожидание принудительным убийством «чтобы быстрее»: именно оно описано в документации как путь к потере исполняемой работы.
Почему пустой список процессов ничего не доказывает
Соблазн велик: открыли панель, увидели, что процессов worker больше нет, и решили, что работа закончена. Но отсутствие процессов говорит только о том, что исполнителей нет. Оно ничего не говорит о том, что лежит в брокере. Ожидающие сообщения продолжают храниться, и если на их очередь затем подпишется любой новый worker, он их заберёт. Верно и обратное: исполнители исчезли после принудительного лимита, а задачи, которые они держали, либо вернутся в очередь, либо пропадут, смотря по настройкам.
Поэтому критерий завершения drain состоит из нескольких независимых наблюдений:
- производители остановлены, и длина каждой нужной очереди перестала расти;
- старый worker завершился штатно, а не был остановлен принудительно (это видно по журналу);
- в очередях брокера не осталось ожидающих сообщений, либо остаток осознанно переносится;
- нет сообщений, которые числятся как взятые, но не подтверждённые;
- отложенные и периодические задачи учтены отдельно.
Конкретные команды инспекции у Celery есть, но их набор и вывод зависят от версии и брокера, поэтому в рабочем плане сверяйтесь с документацией той версии, которую вы реально используете.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации Россия, США, Франция и Великобритания. Оплата картой РФ и по СБП.
Задача, оборвавшаяся после внешнего действия
Теперь самая неприятная часть. Допустим, задача отправляет клиенту письмо с чеком и после этого помечает заказ как «уведомлён». Если процесс оборвался ровно между этими двумя шагами, снаружи письмо ушло, а внутри заказ выглядит неуведомлённым. Что будет дальше, зависит от подтверждения.
Два режима и их цена
Упрощённо, у Celery есть два принципиальных подхода к моменту подтверждения.
- Подтверждение до выполнения. Сообщение считается обработанным, как только worker его принял. Если процесс умер посреди работы, брокер уже ничего не повторит: задача потеряна. Дубля не будет, но не будет и результата.
- Подтверждение после выполнения. Сообщение остаётся неподтверждённым, пока задача не закончится. Если процесс умер, брокер при определённых условиях вернёт сообщение в очередь, и другой worker выполнит задачу заново. Потери нет, но возможен повтор уже частично сделанного.
Подчеркну: «при определённых условиях». Как именно и когда брокер возвращает неподтверждённое сообщение, определяется транспортом и его настройками, а у Celery возможность повторной доставки зависит, в том числе, от подтверждений (Celery: Workers guide). Из этого следует рабочее правило для переезда: не обещайте себе безопасный drain, не выяснив, какой режим включён и как ведёт себя ваш брокер. Для одной настройки остановка со временем вернёт всё в очередь, для другой потеряет, а для третьей даст дубль.
Что значит «безопасно повторить»
При подтверждении после выполнения повтор задачи становится нормальным событием, а не аварией, и код должен его переносить. Это свойство называют идемпотентностью: повторный запуск с теми же входными данными не создаёт второго эффекта. Достигается оно не волшебным флагом, а устройством самой операции.
Вернёмся к примеру с чеком. Есть два надёжных подхода и один ненадёжный.
- Устойчивый ключ операции. Задача получает не просто «отправь письмо», а идентификатор бизнес-события, например номер заказа и тип уведомления. Перед отправкой она проверяет в своей базе, зафиксировано ли уже такое событие. Такой ключ должен происходить из предметной области, а не генерироваться заново при каждой попытке: новый идентификатор попытки сам по себе идемпотентности не даёт, ведь повтор получит уже другой ключ.
- Ключ идемпотентности у внешней системы. Если сторонний сервис принимает такой ключ, передавайте один и тот же при каждой попытке. Это зависит от возможностей конкретного сервиса, и предполагать их без проверки нельзя.
- Ненадёжное: считать, что транзакция вашей базы покрывает внешний вызов. Она не покрывает. Запись в свою БД и обращение к внешнему API атомарно не связаны без отдельного подтверждённого механизма. Всегда остаётся окно между «внешнее действие выполнено» и «факт записан у нас».
Это окно нельзя закрыть полностью, но можно сделать его последствия приемлемыми: записывать намерение до действия, фиксировать результат после, а при повторе сначала выяснять, дошло ли действие до цели. Для писем или платежей этот выбор принимает владелец предметной области, а не инфраструктура.
Как разобрать конкретные задачи до переезда
Составьте короткую таблицу по типам задач. Она занимает час, а экономит много неприятных разговоров.
| Тип задачи | Внешний эффект | Устойчивый ключ | Допустим ли повтор |
|---|---|---|---|
| Отправка чека | Письмо клиенту | Номер заказа и тип письма | Только с проверкой записи об отправке |
| Пересчёт витрины | Только запись в свою БД | Не требуется, результат перезаписывается | Да |
| Списание по подписке | Запрос к платёжному сервису | Идентификатор периода подписки | Только если сервис поддерживает ключ или есть сверка |
| Очистка временных файлов | Удаление | Путь файла | Да, если отсутствие файла не считается ошибкой |
Таблица условная, ваши типы будут другими. Смысл в том, чтобы для каждой строки заранее знать ответ на вопрос «что будет, если эта задача выполнится дважды или не выполнится ни разу» и, соответственно, какой способ остановки для неё приемлем.
Маршруты и секреты: то, что ломается тише всего
Код worker переносится легко. Обычно именно окружение вокруг него подводит. Две группы вещей заслуживают такой же тщательности, как сам образ.
Очереди и маршрутизация
Если у вас больше одной очереди, то где-то задано, какая задача в какую очередь попадает и какой worker какую очередь слушает. Эти настройки могут жить в нескольких местах: в конфигурации Celery, в аргументах запуска процесса, в параметрах самих задач, в переменных окружения. Типовые сбои при переезде такие:
- на новом worker забыли указать список очередей, он слушает только очередь по умолчанию, а остальные молча копятся;
- имя очереди в новом окружении отличается из-за префикса или другого пространства имён брокера, и производитель пишет туда, где никто не читает;
- выделенный worker для тяжёлых задач при переносе слили в общий, и тяжёлые задачи начали вытеснять быстрые.
Защита простая: выпишите соответствие «задача, очередь, worker» из старого окружения и сверьте его с новым построчно. И проверьте на стенде, что тестовая задача каждого типа действительно попадает туда, куда задумано.
Секреты и переменные окружения
Worker использует те же секреты, что и приложение: адрес и пароль брокера, строку подключения к базе, ключи внешних сервисов, настройки отправки почты. Если в Render они заданы как переменные окружения сервиса, то на VPS их нужно воссоздать явно: файл с ограниченными правами, менеджер процессов или другой способ, принятый у вас. Две ловушки:
- переменная пропущена, и worker стартует, но задача падает на первом же обращении к соответствующему сервису; в логе это выглядит как рядовая ошибка задачи, а не ошибка переезда;
- переменная скопирована со старым значением, например адрес брокера всё ещё указывает на прежний. Тогда новый worker незаметно подключится не туда, куда вы думаете.
Сравнивайте не «все ли переменные есть», а «куда указывает каждая из них». Для адресов брокера, базы и внешних API это особенно важно.
Что нужно от VPS для такого контура
Размещение worker на VPS даёт то, чего на платформе обычно не хватает: полный контроль над процессом, его сигналами и временем остановки. На сервере с root-доступом вы сами выбираете менеджер процессов, задаёте, сколько времени он ждёт завершения worker, и можете прогнать репетицию остановки на копии. Это напрямую связано с темой статьи: чем дольше самая длинная задача, тем важнее, чтобы лимит остановки задавали вы, а не платформа.
По ресурсам Celery-контур обычно складывается из нескольких процессов: worker, при необходимости брокер, планировщик и само приложение. Тарифы MAATRIX различаются по vCPU, памяти и диску: например, Ba предлагает 1 vCPU, 2 ГБ RAM и 40 ГБ NVMe, Uab — 2 vCPU, 4 ГБ и 80 ГБ, а Heka — 4 vCPU, 8 ГБ и 160 ГБ. Эти цифры не являются обещанием ёмкости или рекомендуемым минимумом для вашей нагрузки. Реальные требования определяются тем, сколько памяти потребляет одна задача и сколько процессов пула вы запускаете, и это нужно измерять на своих задачах. Если брокер будет жить на том же сервере, учтите его память отдельной строкой.
VPS MAATRIX работают на NVMe и AMD EPYC, с root-доступом; в публичном предложении перечислены локации UK, US, RU и FR, оплата картой РФ и СБП, а VPS создаётся автоматически через пару минут после оплаты. Точный набор ОС и образов лучше смотреть в форме заказа на странице аренды VPS MAATRIX. Резервные копии здесь отдельной гарантией не считаются, поэтому данные брокера и базы, которые вам важны, нужно сохранять своими средствами.
Запуск нового контура без двойной работы
Когда старая сторона остановлена, а остаток очереди описан, можно включать новый контур. Порядок диктуется тем же принципом: ответственность передаётся строго в одну сторону.
- Новый worker стартует до производителей. Сначала убедитесь, что он подключился к нужному брокеру и слушает нужные очереди. Это видно в журнале запуска и при инспекции.
- Контрольная задача. Поставьте безвредную тестовую задачу каждого типа, где это возможно, и проверьте, что она выполнена новым worker и результат оказался там, где ожидается.
- Остаток старого брокера. Если брокер менялся, остаток либо уже обработан старыми исполнителями, либо переносится осознанно: повторной постановкой задач из источника истины (например, из таблицы в вашей базе) с теми же бизнес-идентификаторами, а не копированием внутренностей брокера.
- Производители включаются по одному. Сначала самые безопасные, затем остальные. Планировщик периодических задач включается последним, и только в одном экземпляре. Два планировщика, старый и новый, дадут каждую периодическую задачу дважды.
- Старый контур не просто выключен, а лишён возможности вернуться. Снимите его с автозапуска, отзовите у него секреты брокера или смените пароль, чтобы случайный перезапуск не подключил его обратно.
Последний пункт полезнее, чем кажется. Остановленный сервис на платформе при нажатии одной кнопки или автоматическом восстановлении снова становится читателем очереди. Смена доступа превращает «мы его выключили» в «он не может работать».
Сверка по устойчивым идентификаторам
Теперь нужно доказать, что ничего не потеряно и не удвоено. Счётчики вроде «в очереди было 1200 сообщений, стало 0» для этого слишком грубы. Они не отличают выполненное от потерянного и не видят дубли. Нужна сверка по идентификаторам предметной области.
Идея такая: у каждой значимой операции есть устойчивый ключ, и для окна переезда вы можете собрать три множества.
- Что должно было произойти: ключи операций, порождённых производителями до остановки, по источнику истины в вашей базе.
- Что зафиксировано как выполненное: ключи, для которых есть запись о завершении в базе или во внешней системе.
- Что выполнялось более одного раза: ключи, у которых несколько записей о выполнении, если ваша схема их хранит.
Разность первого и второго множеств показывает недоделанное, и это кандидаты на повторную постановку. Повторы из третьего множества показывают места, где идемпотентность не сработала. Условный пример: за окно переезда создано 340 заказов, письма с чеком зафиксированы по 338 из них, а по двум нет записи. Это две задачи, судьбу которых надо выяснить, например проверить в журнале почтового сервиса, прежде чем ставить их заново. Числа вымышлены и нужны лишь для иллюстрации подхода.
Если журнал выполнения задач у вас не ведётся, на время переезда стоит хотя бы временно добавить запись «ключ, время, исполнитель, результат». Тогда в любой момент можно ответить на вопрос, какой контур отработал конкретную задачу, и сопоставить это со строкой в логе нового или старого worker.
Пара дней наблюдения
Некоторые проблемы проявляются не сразу. Периодические задачи срабатывают раз в сутки или раз в неделю, отложенные задачи с большим countdown могли остаться в старом брокере, а ошибки секретов обнаруживаются только при первом обращении к редкому сервису. Поэтому после переключения полезно некоторое время смотреть на очереди и журнал: растёт ли длина, нет ли однотипных падений, приходят ли редкие задачи на новый worker. Старый брокер и старый контур не стоит удалять, пока не пройдёт хотя бы один полный цикл самых редких периодических задач.
Когда у каждой задачи есть адрес
Хороший переезд очереди выглядит скучно. Производители остановлены, старые исполнители получили TERM и завершились сами, а не по принудительному лимиту. Остаток брокера описан и либо отработан, либо поставлен заново по устойчивым ключам. Новый worker слушает те же очереди, что и старый, с теми же секретами, и ничего не знает о прошлом контуре. Старый контур больше не получает новых задач, потому что у него отозван доступ, а не потому, что про него забыли.
Критерий готовности не «всё запущено», а более прикладной: для любой задачи из окна переезда можно назвать, где она завершилась, один раз или нет, и каким процессом. Если на этот вопрос есть ответ, дубли и потери перестают быть догадками и превращаются в конкретные записи, которые можно проверить и при необходимости исправить.