proxy_pool: защита последнего узла бережёт бан, но не счётчик отказов — узел выбывает через 3 мс после обещания «продолжит выдаваться» #3310

Open
opened 2026-09-01 07:24:57 +00:00 by bot-backend · 0 comments
Collaborator

Найдено при разборе алерта «2 источника не собирают дольше 3× своего такта» (01.09.2026).

Что видно в логе — три строки подряд

07:20:26.409  proxy_pool: proxy id=13 — бан не записан: это последний узел, достижимый
              для source=avito; нужны новые прокси (см. #2638).
              Узел ПРОДОЛЖИТ ВЫДАВАТЬСЯ этому источнику
              (голодание хуже, чем работа через забаненный узел).

07:20:50.124  proxy_pool: released proxy id=13

07:20:50.127  BrowserFetcher: proxy_pool acquire(avito) EMPTY in production — refusing

Три миллисекунды между «узел продолжит выдаваться» и «пул пуст».

Механизм

Защита последнего узла (mark_banned) бережёт одно поле — запись в scrape_proxy_source_bans. Но дисквалифицирует узел другой, независимый механизм:

  • mark_health(ok=False) инкрементит consecutive_fails (proxy_pool.py:517);
  • acquire отфильтровывает узлы с consecutive_fails >= MAX_CONSECUTIVE_FAILS (:309, :345), а порог — 3 (:161).

Факт на проде: у узла 13 consecutive_fails = 3. Забанен он не был — и всё равно перестал выдаваться. То есть обещание в логе защиты ложно по построению: она стережёт бан, а выбывание происходит по здоровью.

Цена

Пул из 7 узлов, для avito доступен фактически один:

узел состояние
1 asocks-residential-1 жив, но provider_affinity = yandex
9 asocks-mobile-1 забанен для avito до 03.09
10, 11 asocks-mobile-2/3 выключены — 407 Proxy Authentication Required
12 mobileproxy Билайн выключен — срок аренды истёк
13 mobileproxy Tele2 Омск единственный рабочий
14 mobileproxy МегаФон забанен для avito до 02.09

Как только 13 набирает три отказа подряд, avito выключается целиком для всех источников — и avito_detail_backfill (шёл в тот момент, run 5676, BLOCKED #14/1600), и avito_full_load_exhaustive (run 5679, оборвался, не сделав ни одного запроса).

Что это НЕ

Не дефицит прокси — тот отдельно и решается владельцем (#2638). Здесь дефект в том, что при наличии живого узла пул объявляет себя пустым. Защита строилась ровно против этого сценария и не сработала.

Не тот же дефект, что #3299 (там защита не считает узлы any) — здесь она узел посчитала и защитила, но не то поле.

Как чинить

Защита последнего узла должна быть сквозной, а не привязанной к одному месту записи: если узел признан последним для источника, его не должны выбивать ни бан, ни счётчик отказов. Варианты:

  1. acquire при пустом результате повторяет выборку без фильтра consecutive_fails для узлов, помеченных «последний для source» — с явным WARNING в лог.
  2. mark_health(ok=False) не инкрементит счётчик выше MAX-1, если узел последний для какого-либо активного источника.

Оба варианта сохраняют исходный принцип («голодание хуже работы через плохой узел») — сейчас он записан в комментарии, но не выполняется.

Приёмка

  • Воспроизвести: единственный узел + 3 отказа подряд → acquire не возвращает пусто
  • Тест двусторонний: снятие правки даёт красное по значению (пул пуст при живом узле), а не по исключению
  • Строка лога «узел продолжит выдаваться» подтверждается фактом выдачи, а не декларируется
  • На проде: после правки avito_detail_backfill не падает в BLOCKED ... pool empty при enabled узле

Refs: #2638 (дефицит узлов — решение владельца), #3299 (соседний дефект той же защиты), #2616 (запрет fallback на env-прокси — он-то отработал верно).

Найдено при разборе алерта «2 источника не собирают дольше 3× своего такта» (01.09.2026). ## Что видно в логе — три строки подряд ``` 07:20:26.409 proxy_pool: proxy id=13 — бан не записан: это последний узел, достижимый для source=avito; нужны новые прокси (см. #2638). Узел ПРОДОЛЖИТ ВЫДАВАТЬСЯ этому источнику (голодание хуже, чем работа через забаненный узел). 07:20:50.124 proxy_pool: released proxy id=13 07:20:50.127 BrowserFetcher: proxy_pool acquire(avito) EMPTY in production — refusing ``` **Три миллисекунды** между «узел продолжит выдаваться» и «пул пуст». ## Механизм Защита последнего узла (`mark_banned`) бережёт **одно** поле — запись в `scrape_proxy_source_bans`. Но дисквалифицирует узел **другой, независимый** механизм: - `mark_health(ok=False)` инкрементит `consecutive_fails` (`proxy_pool.py:517`); - `acquire` отфильтровывает узлы с `consecutive_fails >= MAX_CONSECUTIVE_FAILS` (`:309`, `:345`), а порог — **3** (`:161`). Факт на проде: у узла 13 `consecutive_fails = 3`. Забанен он не был — и всё равно перестал выдаваться. То есть обещание в логе защиты **ложно по построению**: она стережёт бан, а выбывание происходит по здоровью. ## Цена Пул из 7 узлов, для `avito` доступен фактически один: | узел | состояние | |---|---| | 1 `asocks-residential-1` | жив, но `provider_affinity = yandex` | | 9 `asocks-mobile-1` | забанен для avito до 03.09 | | 10, 11 `asocks-mobile-2/3` | выключены — `407 Proxy Authentication Required` | | 12 `mobileproxy Билайн` | выключен — срок аренды истёк | | 13 `mobileproxy Tele2 Омск` | **единственный рабочий** | | 14 `mobileproxy МегаФон` | забанен для avito до 02.09 | Как только 13 набирает три отказа подряд, **avito выключается целиком для всех источников** — и `avito_detail_backfill` (шёл в тот момент, run 5676, BLOCKED #14/1600), и `avito_full_load_exhaustive` (run 5679, оборвался, не сделав ни одного запроса). ## Что это НЕ Не дефицит прокси — тот отдельно и решается владельцем (#2638). Здесь дефект в том, что **при наличии живого узла пул объявляет себя пустым**. Защита строилась ровно против этого сценария и не сработала. Не тот же дефект, что #3299 (там защита не считает узлы `any`) — здесь она узел посчитала и защитила, но не то поле. ## Как чинить Защита последнего узла должна быть **сквозной**, а не привязанной к одному месту записи: если узел признан последним для источника, его не должны выбивать ни бан, ни счётчик отказов. Варианты: 1. `acquire` при пустом результате повторяет выборку без фильтра `consecutive_fails` для узлов, помеченных «последний для source» — с явным WARNING в лог. 2. `mark_health(ok=False)` не инкрементит счётчик выше `MAX-1`, если узел последний для какого-либо активного источника. Оба варианта сохраняют исходный принцип («голодание хуже работы через плохой узел») — сейчас он записан в комментарии, но не выполняется. ## Приёмка - [ ] Воспроизвести: единственный узел + 3 отказа подряд → `acquire` **не** возвращает пусто - [ ] Тест **двусторонний**: снятие правки даёт красное по значению (пул пуст при живом узле), а не по исключению - [ ] Строка лога «узел продолжит выдаваться» подтверждается фактом выдачи, а не декларируется - [ ] На проде: после правки `avito_detail_backfill` не падает в `BLOCKED ... pool empty` при `enabled` узле Refs: #2638 (дефицит узлов — решение владельца), #3299 (соседний дефект той же защиты), #2616 (запрет fallback на env-прокси — он-то отработал верно).
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#3310
No description provided.