В DBOS обычная очередь на PostgreSQL упёрлась в конкуренцию работников примерно при 100 выполнениях в секунду. Три изменения подняли результат выше 30 тысяч. Но это цифры конкретной архитектуры DBOS, а не предел PostgreSQL или вашего сервера.
Очередь фоновых обменов часто хранится рядом с базой 1С. При небольшой нагрузке такое соседство упрощает транзакции и сопровождение. Когда задач становится больше, очередь начинает отбирать у 1С процессорное время, соединения и дисковые операции.
Само число задач ещё не повод переезжать на RabbitMQ, Redis или SQS. Сначала найдите ограничение: захват строк, уровень изоляции или обслуживание индексов. Брокер нужен при новых требованиях к доставке и масштабированию, а не после условной тысячи сообщений в секунду.
Таблица задач должна переживать сбои работников
Простейшая очередь хранит задачу и её состояние в строке PostgreSQL. Приложение добавляет запись со статусом ready. Работник меняет его на running, а после выполнения — на done или dead.
Статусов недостаточно. Работник может завершиться после захвата строки, но до смены состояния. Сеть может оборваться после внешнего действия, а повторный запрос поставит ту же задачу ещё раз.
Пранав Кулкарни в руководстве по очередям с SKIP LOCKED называет пять обязательных механизмов: конкурентный захват, идемпотентную постановку, повторные попытки, аренду задачи и отдельное состояние для окончательно неудачных запусков.
| Механизм | От какого сбоя защищает | Чего не гарантирует |
|---|---|---|
FOR UPDATE SKIP LOCKED | Два работника не заберут одну строку одновременно | Обработчик не запустится повторно после другого сбоя |
| Уникальный ключ идемпотентности | Повторный запрос не создаст вторую одинаковую задачу | Внешняя система не выполнит действие дважды |
| Аренда задачи с отметкой времени | Задача вернётся в очередь после остановки работника | Короткая аренда не отберёт долгую задачу у живого работника |
Состояние dead | Исчерпанные попытки не будут повторяться бесконечно | Оператор сам не узнает и не устранит причину ошибки |
Из этой таблицы следует главное: очередь защищает захват задачи, но не побочные действия обработчика.
Такая схема даёт выполнение не менее одного раза. Значит, обработчик должен выдерживать повторный запуск. Письмо не должно уйти дважды, платёж — списаться повторно.
Ключ идемпотентности описывает действие, а не HTTP-запрос. Для отправки квитанции подойдёт invoice:1842:send-receipt. Повторная постановка с тем же ключом завершится через INSERT... ON CONFLICT DO NOTHING.
Аренда закрывает другой разрыв. Работник сохраняет владельца задачи и время захвата. Периодическая процедура возвращает просроченные записи в ready, если работник не продлил аренду.
Срок аренды берите длиннее нормального времени выполнения. Для долгих операций добавьте heartbeat: работник обновляет время блокировки, пока задача жива. Проверка locked_by не даст прежнему работнику завершить задачу, которую уже перехватил другой процесс.
Когда очередь выдерживает эти сбои, ограничение обычно смещается к началу таблицы. Несколько работников одновременно пытаются забрать одну и ту же строку.
SKIP LOCKED убирает очередь работников за одной строкой
Без блокировки запрос сначала находит самую старую задачу. Несколько работников видят её одновременно и спорят за одну строку. В опыте DBOS такая схема перестала расти примерно после 100 выполнений в секунду.
SELECT... FOR UPDATE SKIP LOCKED меняет сам захват. Первый работник блокирует выбранные строки. Остальные пропускают их и сразу забирают следующие, не ожидая освобождения общей головы очереди.
Транзакцию захвата держите короткой:
WITH picked AS (
SELECT id
FROM jobs
WHERE status = 'ready'
AND run_at <= now()
ORDER BY run_at, id
FOR UPDATE SKIP LOCKED
LIMIT 1
)
UPDATE jobs
SET status = 'running',
locked_at = now(),
locked_by = $1
FROM picked
WHERE jobs.id = picked.id
RETURNING jobs.*;
Сразу после COMMIT работник выполняет задачу уже без блокировки строки. Не оставляйте транзакцию открытой на время обращения к медленному API или большого обмена. Иначе конкуренция вернётся, хотя в запросе останется SKIP LOCKED.
У такого захвата есть цена. Документация PostgreSQL предупреждает, что SKIP LOCKED возвращает несогласованный срез данных. Для очереди конкурирующих работников это подходит. Для строгого общего FIFO — нет.
Сортировка run_at, id задаёт устойчивый порядок среди доступных строк. Но она не заставляет работников завершать задачи в той же последовательности. Ранняя задача может выполняться дольше поздней.
Уровень изоляции зависит от глобальных ограничений
После устранения блокировок DBOS получила новый предел. При нагрузке свыше примерно тысячи выполнений в секунду PostgreSQL стал возвращать ошибки сериализации. Причина — REPEATABLE READ: каждая транзакция видела фиксированный снимок, а параллельные изменения пересекались.
DBOS выбрала этот уровень ради глобальных ограничений. Правило «во всей системе одновременно работают не больше N задач» требует согласованного представления о состоянии всей очереди.
Локальное правило не требует такой координации. Если один работник берёт не больше десяти задач, ему не нужен общий снимок состояния остальных процессов. Здесь можно использовать READ COMMITTED.
В DBOS переход на READ COMMITTED убрал ошибки сериализации и поднял пропускную способность. Но менять изоляцию вслепую нельзя. Если бизнес-правило зависит от общего снимка очереди, после перехода оно начнёт работать иначе.
Сформулируйте решение, которое транзакция принимает по данным всей очереди. Если такого решения нет, REPEATABLE READ, вероятно, не нужен. Если есть, сначала вынесите ограничение в отдельный счётчик, секцию координации или другой механизм.
| Признак | Вероятное ограничение | Что изменить | Какой риск проверить |
|---|---|---|---|
| Работники ждут блокировки одной строки | Конкурирующий захват начала очереди | Применить FOR UPDATE SKIP LOCKED | Строгий глобальный порядок исчезнет |
| PostgreSQL возвращает ошибки сериализации | Транзакции пересекаются на REPEATABLE READ | Проверить возможность перехода на READ COMMITTED | Глобальный лимит увидит несогласованное состояние |
| В плане выдачи есть отдельная сортировка | Индекс не совпадает с фильтром и порядком | Создать частичный индекс готовых задач | Новый индекс займёт место и потребует обслуживания |
| Индексы растут, autovacuum забирает CPU | Частые смены статуса обновляют лишние индексы | Сократить число индексов и их область | Диагностические запросы могут замедлиться |
Таблица задаёт порядок диагностики: сначала определите вид ожидания, затем меняйте конкретный механизм.
Добавляйте работников лишь после устранения текущего ограничения. Иначе новые процессы увеличат ожидания блокировок, займут соединения и добавят работы индексам.
Частичный индекс сокращает работу при каждой выдаче
Следующий предел DBOS увидела примерно при восьми тысячах выполнений в секунду. Процессор загружали запрос выдачи и autovacuum. Механика SKIP LOCKED работала штатно — проблема оказалась в индексах.
Индекс по имени очереди и статусу находил готовые задачи, но PostgreSQL отдельно сортировал их по времени. При каждой смене статуса СУБД также обновляла вторичные индексы. Затем autovacuum убирал устаревшие записи.
Частичный индекс хранит только строки, доступные для захвата:
CREATE INDEX jobs_ready_idx
ON jobs (queue, run_at, id)
WHERE status = 'ready';
Поля индекса повторяют фильтр и порядок запроса выдачи. Когда работник меняет ready на running, PostgreSQL удаляет запись из небольшого индекса. Строки done и dead в него не входят.
Результат проверяйте по плану запроса, размеру индекса и работе autovacuum. Общий маршрут описан в материале о проверке нагрузки от Linux до PostgreSQL и процессов 1С. Одного графика CPU СУБД мало: очередь может занять рабочие соединения 1С раньше, чем процессор упрётся в предел.
Не добавляйте индексы на каждый столбец ради будущей диагностики. Таблица очереди меняется постоянно, поэтому любой вторичный индекс накапливает старые версии записей. Оставляйте индекс, только если под него есть конкретный проверяемый запрос.
LISTEN/NOTIFY будит работника, но не хранит задания
Постоянный опрос таблицы создаёт запросы даже тогда, когда заданий нет. LISTEN/NOTIFY сокращает задержку пробуждения. После фиксации транзакции PostgreSQL отправляет уведомление подключённым сеансам, и работник читает таблицу.
Уведомление не заменяет строку задачи. ArmorDB в сравнении вариантов очередей описывает NOTIFY как сигнал пробуждения, а не долговечное хранилище. Здесь нет повторного чтения истории, тайм-аута видимости, очереди ошибок и групп потребителей.
После уведомления работник всё равно захватывает строку через SKIP LOCKED. Если соединение оборвалось и уведомление потерялось, периодический опрос найдёт сохранённую задачу. Таблица хранит состояние. Уведомление лишь сокращает время ожидания.
Advisory lock решает ещё более узкую задачу. Он подходит для одной фоновой операции или блокировки по клиенту. Транзакционная advisory-блокировка снимается при COMMIT или ROLLBACK, но не хранит полезную нагрузку, число попыток и состояние выполнения.
| Механизм | Что делает | Подходящая задача | Где заканчиваются возможности |
|---|---|---|---|
SKIP LOCKED и таблица | Конкурентно выдаёт сохранённые задания | Несколько работников обрабатывают долговечные задачи | Нет строгого глобального FIFO и функций брокера |
LISTEN/NOTIFY и таблица | Будит работника после появления строки | Нужна быстрая реакция без частого опроса | Уведомление нельзя перечитать как журнал |
| Advisory lock | Запрещает параллельный запуск по выбранному ключу | Одна задача на систему или клиента | Нет очереди, повторов и состояния задания |
| Брокер и outbox | Доставляет события отдельным потребителям | Fan-out, группы потребителей, межсервисные события | Появляется ещё один сервис и новый режим отказа |
Вывод простой: LISTEN/NOTIFY сокращает задержку, но долговечность по-прежнему даёт таблица.
Добавляйте LISTEN/NOTIFY, когда работник должен быстрее просыпаться. Не стройте на уведомлениях долговечную очередь: для этого уже есть строка в таблице.
Порог перехода задают требования к доставке
Универсальной границы в задачах за секунду нет. Неоптимизированная схема DBOS остановилась около 100 выполнений. После изменения захвата, изоляции и индексов она превысила 30 тысяч. Это результат одной системы с известной последовательностью доработок, а не норматив PostgreSQL.
Оставляйте очередь в базе, когда задача связана с транзакцией PostgreSQL. Например, запись документа и постановка обмена должны подтвердиться вместе. Общая таблица решает это без распределённой транзакции.
Ещё одно условие — допустимый повторный запуск. Таблица с арендой даёт выполнение не менее одного раза. Обработчик поэтому должен распознавать уже совершённое внешнее действие.
Проверьте и требования к порядку. SKIP LOCKED раздаёт доступные строки нескольким работникам, но не сохраняет общий FIFO при параллельной обработке.
Брокер нужен, когда доставка превращается в отдельную подсистему. ArmorDB относит к таким признакам высокий fan-out, долгую отложенную доставку, группы потребителей, очередь ошибок, межсервисный поток событий и независимое масштабирование.
| Требование | Таблица PostgreSQL | Внешний брокер | Решение |
|---|---|---|---|
| Постановка задачи в одной транзакции с данными | Прямая запись в общей транзакции | Нужен outbox или распределённая транзакция | Оставить таблицу либо применить outbox |
| Несколько конкурирующих работников | Работает через SKIP LOCKED | Поддерживается средствами брокера | Сначала проверить блокировки и индекс |
| Строгий порядок при параллельной обработке | SKIP LOCKED его нарушает | Зависит от секций и модели потребления | Проектировать порядок отдельно |
| Много независимых потребителей одного события | Нужны копии состояния и собственная маршрутизация | Fan-out входит в модель брокера | Выносить доставку |
| Повторное чтение истории | Нужны отдельная таблица и политика хранения | Поддерживается не каждым брокером, но проектируется явно | Выбирать систему по сроку хранения |
| Независимое масштабирование доставки | Нагрузка остаётся на общей СУБД | Брокер масштабируется отдельно | Выносить при конкуренции с запросами 1С |
Эта развилка не про моду на технологии. Таблица выигрывает там, где нужна общая транзакция. Брокер — там, где доставку приходится развивать отдельно от данных.
Брокер не обязан хранить первичное событие. Таблица outbox записывается вместе с бизнес-данными, а отдельный процесс публикует новые записи. PostgreSQL сохраняет транзакционную связь, брокер отвечает за доставку.
У выноса есть цена и без замеров производительности. Вы получите ещё один сервис, учётные данные, мониторинг, сценарии отказа и зависимость локальной среды. Эти расходы оправданы, если покупают нужную семантику доставки, а не новую технологию ради неё самой.
Выбор СУБД для 1С лежит в другой плоскости. Если очередь подтолкнула вас к пересмотру всего контура, отделите её требования от критериев выбора PostgreSQL и SQL Server. Замена СУБД не сократит долгую транзакцию захвата и не удалит лишний индекс.
Что проверить в ближайшее окно обслуживания
Не переносите очередь после первого скачка нагрузки. Пройдите пять проверок по порядку:
- Убедитесь, что работники захватывают строки через
FOR UPDATE SKIP LOCKED. - Зафиксируйте транзакцию до выполнения внешнего действия.
- Найдите правила, которым нужен согласованный снимок всей очереди.
- Сверьте частичный индекс с фильтром и сортировкой запроса выдачи.
- Проверьте CPU, блокировки, соединения, размер индексов и работу autovacuum вместе с нагрузкой 1С.
Если предел скрывается в захвате, изоляции или индексе, укрепляйте PostgreSQL. Если очередь уже отбирает ресурсы у 1С либо требует fan-out, истории, групп потребителей и отдельного масштабирования, подключайте брокер с outbox.
После изменений нужны четыре признака. Работники не ждут одну строку. Транзакция захвата завершается до обработки. План использует частичный индекс. Рост очереди не замедляет запросы 1С.
Вот ваш порог перехода: его видно на собственной системе. Чужой результат в 100, тысячу, восемь тысяч или 30 тысяч выполнений за секунду этого порога не заменит.