Как заставить очереди на Postgres масштабироваться: три урока DBOS
Считается, что очереди на базе Postgres не масштабируются: для серьёзных нагрузок якобы нужны специализированные системы вроде RabbitMQ с Celery или Redis с BullMQ. Основания для такого мнения есть — тысячи воркеров, одновременно опрашивающих таблицу очереди, создают конкуренцию и изнашивают индексы. Однако инженеры DBOS Цянь Ли и Питер Крафт показали, что при правильной настройке Postgres справляется, достигнув 30 тысяч выполнений рабочих процессов в секунду на тысячах серверов. Свой опыт они свели к трём урокам.
Урок первый: SKIP LOCKED
Первая проблема — конкуренция за одни и те же задачи. Если каждый воркер простым запросом выбирает N самых старых задач в очереди, все они видят одни и те же строки и пытаются захватить их одновременно. Забрать задачу может лишь один, остальные впустую повторяют попытку. Решение давно встроено в Postgres — блокирующие конструкции. Запрос с FOR UPDATE SKIP LOCKED блокирует выбранные строки и пропускает уже заблокированные другими воркерами. Так каждый берёт свою порцию задач без конфликтов. Авторы иронично замечают, что SKIP LOCKED — из тех старых трюков Postgres, которые открывают заново снова и снова. Без него потолок — около 100 задач в секунду.
Урок второй: уровни изоляции транзакций
Следующее узкое место — при более чем тысяче задач в секунду большинство операций падало с ошибками сериализации и требовало повторов. Причиной оказался уровень изоляции REPEATABLE READ, изначально выбранный ради глобальных ограничений вида «не более N процессов одновременно на всех воркерах». Такой режим требует согласованного снимка базы, но при высокой конкуренции становится дорогим: пересекающиеся изменения приводят к отмене транзакций. Ключевое наблюдение — крупнейшие очереди редко пользуются глобальным контролем, предпочитая локальные лимиты («не более 10 процессов на воркер»), которым согласование между воркерами не нужно. Поэтому уровень изоляции сделали условным: очереди с глобальным контролем остаются на REPEATABLE READ, остальные переходят на READ COMMITTED, полностью устраняющий ошибки сериализации.
Урок третий: индексы не бесплатны
После этого конкуренция практически исчезла, но при нагрузке свыше 8 тысяч задач в секунду выросла нагрузка на процессор — из-за неэффективных индексов. Индекс для выборки задач не возвращал их в нужном порядке, поэтому Postgres приходилось дополнительно сортировать по времени. К тому же каждое изменение статуса требовало обновления всех индексов, а следом autovacuum вычищал устаревшие записи. Решением стала выборочность: главный индекс переделали так, чтобы он сразу сортировал задачи по приоритету и времени, и превратили его в частичный — поддерживаемый лишь для процессов в статусе ENQUEUED. Тот же приём применили к индексам для наблюдаемости, например к индексу по идентификатору родительского процесса.
В сумме эти оптимизации резко снизили нагрузку на процессор и позволили довести очереди до более чем 30 тысяч задач в секунду — около 80 миллиардов в месяц.
Источник: Hacker News
Комментарии
Войдите, чтобы комментировать.