В 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. Замена СУБД не сократит долгую транзакцию захвата и не удалит лишний индекс.

Что проверить в ближайшее окно обслуживания

Не переносите очередь после первого скачка нагрузки. Пройдите пять проверок по порядку:

  1. Убедитесь, что работники захватывают строки через FOR UPDATE SKIP LOCKED.
  2. Зафиксируйте транзакцию до выполнения внешнего действия.
  3. Найдите правила, которым нужен согласованный снимок всей очереди.
  4. Сверьте частичный индекс с фильтром и сортировкой запроса выдачи.
  5. Проверьте CPU, блокировки, соединения, размер индексов и работу autovacuum вместе с нагрузкой 1С.

Если предел скрывается в захвате, изоляции или индексе, укрепляйте PostgreSQL. Если очередь уже отбирает ресурсы у 1С либо требует fan-out, истории, групп потребителей и отдельного масштабирования, подключайте брокер с outbox.

После изменений нужны четыре признака. Работники не ждут одну строку. Транзакция захвата завершается до обработки. План использует частичный индекс. Рост очереди не замедляет запросы 1С.

Вот ваш порог перехода: его видно на собственной системе. Чужой результат в 100, тысячу, восемь тысяч или 30 тысяч выполнений за секунду этого порога не заменит.

postgresql skip locked брокеры сообщений индексы очереди задач