Как обеспечить отказоустойчивый и масштабируемый polling с учетом падения узла и высокой нагрузки?
Что проверяют
Умеете ли вы думать о polling как о распределённой системе: кто решает, что опрашивать, что будет с задачами упавшего узла и как не устроить дубли. Следом обычно спрашивают, как гарантировать, что один источник не опрашивают два воркера сразу.
Как отвечать
Каркас из четырёх частей:
- распределение работы: планировщик кладёт задания в очередь (RabbitMQ, Redis Streams, Kafka) или в таблицу, воркеры их забирают. В PostgreSQL удобен
SELECT ... FOR UPDATE SKIP LOCKED: воркеры разбирают строки, не блокируя друг друга; - падение узла: задание берётся в аренду с таймаутом, живой воркер её продлевает. Упал воркер, аренда истекла, задание забрал другой. В RabbitMQ ту же роль играет подтверждение после обработки: неподтверждённое сообщение вернётся в очередь при обрыве соединения;
- идемпотентность: такая схема даёт доставку «хотя бы один раз», поэтому повторная обработка обязана быть безопасной. Помогают ключ дедупликации и
ON CONFLICT DO NOTHING; - нагрузка: горизонтально добавляются воркеры, внутри воркера
asyncioсhttpxилиaiohttpиasyncio.Semaphoreдля лимита запросов. Ретраи с экспоненциальной задержкой и джиттером, для больного источника circuit breaker.
Планировщик в единственном экземпляре становится точкой отказа, а несколько без координации дублируют задания. Нужен выбор лидера (etcd, advisory lock в PostgreSQL) или планировщик без состояния, который выбирает созревшие задания запросом по next_poll_at. Celery beat, к слову, должен работать строго в одном экземпляре.
Упомяните и альтернативы: условные запросы с ETag и If-Modified-Since снижают нагрузку, а вебхуки часто лучше polling вообще.
Где спотыкаются
Запускают планировщик в каждой реплике. После масштабирования до трёх подов каждый источник опрашивается трижды.
Делают ретраи без джиттера и без лимита. Когда источник поднимается после сбоя, все воркеры бьют в него одновременно и роняют снова.
Вызывают time.sleep или синхронный requests внутри корутины. Это блокирует event loop, и асинхронный воркер превращается в последовательный опросчик.