Skip to content

Compare versions

From:To:
+95
11¶ **graphile-worker, очередь задач на Postgres. Автор и основной разработчик — Бенджи Гиллам (Benjie Gillam), тот же человек, что делает PostGraphile.**
22
33 Очередь задач в базе устроена просто ровно до второго воркера. Дальше встаёт вопрос, который решают все и по-разному: как двум процессам взять из одной таблицы разные задачи и не подраться за одну.
44
55 Postgres даёт готовый ответ — `FOR UPDATE SKIP LOCKED`: «возьми строки, пропуская те, что уже кем-то заняты». Но есть ещё именованные очереди — когда задачи с одинаковым именем обязаны выполняться строго по одной. И вот тут одного `SKIP LOCKED` уже мало.
66¶ ### Кадр первый: 29 июня 2022
77
88 Приезжает коммит с названием, которое ничего не обещает:
99
1010 > `Add a few strategies`
1111
12 Сто строк в файле выбора задачи. Описания у коммита нет — вся документация внутри, комментарием над настройкой. И это лучший вид документации, потому что она не про то, как работает код, а про то, что показали замеры:
12+ Сто строк в файле выбора задачи — [`src/sql/getJob.ts`, строки 28–52 в том самом коммите](https://github.com/graphile/worker/blob/ad21f2a17bc5/src/sql/getJob.ts#L28-L52). Описания у коммита нет — вся документация внутри, комментарием над настройкой. И это лучший вид документации, потому что она не про то, как работает код, а про то, что показали замеры:
1313
1414 > **0** — we're not using named queues; skip them! …it's the absolute fastest strategy.
1515 >
1616 > **1** — for each matched job, go lock its job queue if you can. …what Worker traditionally used… **but these days its a terrible strategy unless you're still randomly generating queue names (don't do that!)**. Performance is abysmal if you have a large jobs table with many higher priority but stuck jobs.
1717 >
1818 > **2** — lock the job queues up front, then find a job to do. …seems to be the fastest strategy for jobs that aren't in a queue…
1919 >
2020 > **3** — explicitly avoid locked job queues, but risk multiple jobs in same queue running at same time. **Strategy 3 is probably unsafe. Don't use it.**
2121
22 Четыре варианта, из них один — «скорее всего небезопасен, не пользуйтесь». Оставлен в коде и снабжён предупреждением, потому что кому-то он всả-таки может понадобиться.
22+ Четыре варианта, из них один — «скорее всего небезопасен, не пользуйтесь» ([строка 48](https://github.com/graphile/worker/blob/ad21f2a17bc5/src/sql/getJob.ts#L48)). Оставлен в коде и снабжён предупреждением, потому что кому-то он всё-таки может понадобиться.
2323¶ ### Кадр второй: пятый вариант, которого нет
2424
25 Чуть ниже в том же файле лежит закомментированный кусок SQL с эпитафией:
25+ Чуть ниже в том же файле лежит закомментированный кусок SQL с эпитафией — [строки 98–101](https://github.com/graphile/worker/blob/ad21f2a17bc5/src/sql/getJob.ts#L98-L101):
2626
2727 > This strategy causes incredibly bad performance, presumably due to the lack of lock/skip locked
2828
2929 *Эта стратегия даёт чудовищную производительность, предположительно из-за отсутствия lock/skip locked.*
3030
3131 Его не удалили. Его положили рядом с надписью «пробовали, плохо» — чтобы следующий, кому эта идея покажется хорошей, сначала прочитал.
3232
3333 Слово `presumably` здесь важнее остального: автор не выдумывает объяснение задним числом, а честно помечает, что замер есть, а причина — догадка.
3434¶ ### Кадр третий: 14 ноября 2023
3535
36 Через год и четыре месяца приходит коммит с названием, которое просится в рамку:
36+ Через год и четыре месяца приходит [коммит `e56613e9`](https://github.com/graphile/worker/commit/e56613e94283) с названием, которое просится в рамку:
3737
3838 > `Go back to the old way of doing it`
3939
4040 Минус 267 строк, плюс 73. Файл выбора задачи худеет на 177 строк. Настраиваемость выбрасывают целиком.
4141
4242 Четыре стратегии прожили полтора года и свернулись обратно в одну. Замеры при этом никуда не делись — описания остались в коде. Перестал существовать сам выбор.
4343
4444 Почему именно так, в коммите не объяснено: он идёт в серии из семи коммитов одного дня с названиями вроде `Checkout basics` и `More backporting` — то есть в середине крупной перекладки, а не отдельным решением. Так что от домыслов воздержимся: известно, что выбор из четырёх стратегий не пережил полутора лет.
45+
46+ Ссылки выше ведут в коммит `ad21f2a1`, а не на ветку, и это здесь не формальность: на `main` этого файла больше нет — его и снёс тот самый откат. Ссылка на ветку показала бы «страница не найдена» ровно там, где читатель пошёл смотреть, о чём речь.
4547¶ ### Цена решения, описанная ими самими
4648
4749 У этой конструкции есть цена, и найти её можно не в коде, а в документации для пользователей — в разделе про обработку ошибок:
4850
4951 > If the worker is terminated in a way that cannot be handled (e.g. `process.exit()`, segfault, `SIGKILL`, **someone pulled the power cord**, etc) then the jobs that that worker was executing **remain locked for at least 4 hours**. Every 8-10 minutes a worker will sweep for jobs that have been locked for more than 4 hours and will make them available to be processed again automatically.
5052
5153 *Если воркер завершился так, что обработать это нельзя — …кто-то выдернул шнур… — то задачи, которые он выполнял, остаются заблокированными не менее четырẻх часов.*
5254
5355 Вот она, вся арифметика в одном абзаце. Задача помечается взятой (`locked_at`) на время работы. Воркер умер — пометка осталась. Значит нужен ещё один механизм, который ходит и разблокирует; в проекте под него отдельный файл, `resetLockedAt.ts`.
5456
5557 Выдернули шнур — пользователь ждёт четыре часа. Не потому что кто-то недодумал, а потому что это честная цена за «пометить взятой».
5658## Что с этим делать у себя
57591. Спросить, кто снимет пометку «взято» после SIGKILL
5860 Прежде чем помечать запись взятой на время работы — найдите того, кто снимет пометку, если процесс умрёт между двумя строками.
5961 why: Если ответа нет — нужен второй механизм уборки, а это отдельная работа, которую надо написать, проверить и не сломать. Цена её отсутствия измерена авторами: четыре часа ожидания.
6062 - [ ] Либо найден механизм сброса, либо показано, что помечать взятым нечего
61graphile-workerhttps://github.com/graphile/worker
63+Четыре стратегии с замерами: getJob.ts#L28-L52 (коммит ad21f2a1) https://github.com/graphile/worker/blob/ad21f2a17bc5/src/sql/getJob.ts#L28-L52
64+ → Откат: «Go back to the old way of doing it» — https://github.com/graphile/worker/commit/e56613e94283
62652. Помечать попытку тем же запросом, которым выбирается запись [recommended]
6366 Альтернатива блокировке на время работы: одним UPDATE … FROM (SELECT … FOR UPDATE SKIP LOCKED) сразу поднять счётчик попыток и время следующей.
6467 why: Убитый процесс просто не сделает записи об успехе — запись созреет снова сама, по расписанию повторов. Сбрасывать нечего, потому что ничего не висит взятым.
6568 - [ ] Проверено на двух параллельных воркерах: одна запись не достаётся двоим
66693. Проверить, что разросшаяся таблица не замедляет опрос [recommended]
6770 Авторы прямо предупреждают про «large jobs table with many higher priority but stuck jobs». Частичный индекс, исключающий доставленные и брошенные записи, снимает вопрос.
6871 why: Очередь растёт монотонно, а опрос идёт постоянно. Замедление приходит не сразу и потому выглядит как «что-то с базой».
6972 - [ ] Есть частичный индекс по «созревшим» записям, а не по всей таблице
73+ → Предупреждение про большую таблицу: getJob.ts#L41 — https://github.com/graphile/worker/blob/ad21f2a17bc5/src/sql/getJob.ts#L41
70744. Не выносить выбор наружу, пока нечем выбирать [optional]
7175 Чтобы выбрать между стратегиями 1 и 2, надо знать про свои именованные очереди, застрявшие задачи и размер таблицы. Проще выбрать за него.
7276 why: Четыре стратегии с настройкой прожили полтора года и свернулись в одну — у автора, который знает про свою очередь всё.
7377 - [ ] Для каждой настройки назван, кто и по каким данным её выберет