[HIGH] tradein/proxy: проверка здоровья прокси ходит обычным HTTP, а работа — браузером — 480 зелёных проб за 10 суток, когда сбор падал по 4-5 раз в день #2723

Closed
opened 2026-08-06 11:02:12 +00:00 by bot-backend · 4 comments
Collaborator

Найдено при разборе того, почему avito_full_load не завершался месяц. Проверка здоровья прокси не трогает тот путь, который отказывает, и поэтому месяцами отвечала «всё в порядке» ровно тогда, когда всё было не в порядке.

Что проверяет проба и что делает работа

run_proxy_healthcheck (app/services/proxy_pool.py:790) гоняет через каждый узел обычный HTTP-запрос к https://api.ipify.org — эндпоинту, возвращающему полтора десятка байт с адресом выхода.

Реальный сбор идёт через полноценный браузер в сайдкаре: TLS-отпечаток, навигация по странице, сотни килобайт, долгоживущие соединения. Прокси может пропустить крошечный GET и не пропустить Page.goto — именно это мы сегодня и наблюдали живьём: NS_ERROR_PROXY_BAD_GATEWAY при том, что узел «здоров».

Контраст по дням — сопоставление, а не рассуждение

Слева реальные отказы прогонов с текстом «proxy may be down», справа — что в те же сутки говорила проба:

дата         реальных отказов   проб за сутки   проб с отказом
2026-08-03          1                48               2
2026-08-02          2                48               9
2026-08-01          1                48              12
2026-07-31          4                48               5
2026-07-30          4                48               0
2026-07-29          4                48               0
2026-07-28          4                48               0
2026-07-27          4                47               0
2026-07-26          5                48               0
2026-07-25          4                48               0
2026-07-24          4                48               0
2026-07-23          4                48               0
2026-07-22          4                48               0
2026-07-21          4                48               1

Десять суток подряд (21–30 июля) реальная работа падала по 4–5 раз в день, а проба — 480 запусков за эти дни — не заметила почти ничего. Первые заметные отказы у пробы появились только 31 июля, то есть когда узел, судя по всему, отвалился уже и на уровне обычного HTTP.

Накопительно за период: 90 прогонов оборвались с «proxy may be down», плюс 1 326 домов несут отказ того же класса от сайдкара. Проба за то же время: 1 656 прогонов, отказы в 134 из них — и, как видно из таблицы, приходятся они не на те дни.

Почему это важнее, чем кажется

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

Прямые последствия, каждое уже разобрано отдельно:

  • 92 прогона из 115 помечены «нас забанила площадка», хотя это был отказ сайдкара через неисправный прокси (#2686) — и на этом основании сбор замедлили вдвое;
  • домовая оценка Авито мертва 34 дня (#2698);
  • полный обход Авито не завершался с 3 июля (#2687).

Во всех трёх случаях пул рапортовал, что узлы здоровы.

Что предлагается

Проба должна ходить тем же трактом, что и работа: через сайдкар, браузером, на страницу, сопоставимую по весу с рабочей. Дешёвый вариант — тот же /fetch-json с безобидным адресом (например robots.txt целевой площадки), который мы уже используем для ручных проверок.

Два условия, без которых починка станет своей противоположностью:

  1. Проба не должна сама создавать нагрузку на площадку — отсюда robots.txt, а не выдача.
  2. Старую пробу не убирать, а различать исходы: «узел мёртв целиком» и «узел жив для HTTP, но не для браузера» — это разные диагнозы и разные действия (вывести из пула против пометить непригодным для браузерных источников). Схлопывать их в один флаг — повторить ошибку #2686 в другом месте.

Связано: #2674, #2686, #2698, #2687, #2638.

Найдено при разборе того, почему `avito_full_load` не завершался месяц. Проверка здоровья прокси **не трогает тот путь, который отказывает**, и поэтому месяцами отвечала «всё в порядке» ровно тогда, когда всё было не в порядке. ## Что проверяет проба и что делает работа `run_proxy_healthcheck` (`app/services/proxy_pool.py:790`) гоняет через каждый узел обычный HTTP-запрос к `https://api.ipify.org` — эндпоинту, возвращающему полтора десятка байт с адресом выхода. Реальный сбор идёт **через полноценный браузер в сайдкаре**: TLS-отпечаток, навигация по странице, сотни килобайт, долгоживущие соединения. Прокси может пропустить крошечный GET и не пропустить `Page.goto` — именно это мы сегодня и наблюдали живьём: `NS_ERROR_PROXY_BAD_GATEWAY` при том, что узел «здоров». ## Контраст по дням — сопоставление, а не рассуждение Слева реальные отказы прогонов с текстом «proxy may be down», справа — что в те же сутки говорила проба: ``` дата реальных отказов проб за сутки проб с отказом 2026-08-03 1 48 2 2026-08-02 2 48 9 2026-08-01 1 48 12 2026-07-31 4 48 5 2026-07-30 4 48 0 2026-07-29 4 48 0 2026-07-28 4 48 0 2026-07-27 4 47 0 2026-07-26 5 48 0 2026-07-25 4 48 0 2026-07-24 4 48 0 2026-07-23 4 48 0 2026-07-22 4 48 0 2026-07-21 4 48 1 ``` **Десять суток подряд (21–30 июля) реальная работа падала по 4–5 раз в день, а проба — 480 запусков за эти дни — не заметила почти ничего.** Первые заметные отказы у пробы появились только 31 июля, то есть когда узел, судя по всему, отвалился уже и на уровне обычного HTTP. Накопительно за период: **90 прогонов** оборвались с «proxy may be down», плюс **1 326 домов** несут отказ того же класса от сайдкара. Проба за то же время: 1 656 прогонов, отказы в 134 из них — и, как видно из таблицы, приходятся они не на те дни. ## Почему это важнее, чем кажется Это не «проба неточна». Это **проба, отвечающая на другой вопрос**, чем тот, который задают. «Доступен ли узел для простого запроса» и «пройдёт ли через него браузерная навигация» — разные свойства, и система принимает решения (оставить узел в пуле, считать его живым, не поднимать тревогу) по первому, а страдает от второго. Прямые последствия, каждое уже разобрано отдельно: - 92 прогона из 115 помечены «нас забанила площадка», хотя это был отказ сайдкара через неисправный прокси (#2686) — и на этом основании сбор замедлили вдвое; - домовая оценка Авито мертва 34 дня (#2698); - полный обход Авито не завершался с 3 июля (#2687). Во всех трёх случаях пул рапортовал, что узлы здоровы. ## Что предлагается Проба должна ходить **тем же трактом, что и работа**: через сайдкар, браузером, на страницу, сопоставимую по весу с рабочей. Дешёвый вариант — тот же `/fetch-json` с безобидным адресом (например `robots.txt` целевой площадки), который мы уже используем для ручных проверок. Два условия, без которых починка станет своей противоположностью: 1. **Проба не должна сама создавать нагрузку на площадку** — отсюда `robots.txt`, а не выдача. 2. **Старую пробу не убирать**, а различать исходы: «узел мёртв целиком» и «узел жив для HTTP, но не для браузера» — это разные диагнозы и разные действия (вывести из пула против пометить непригодным для браузерных источников). Схлопывать их в один флаг — повторить ошибку #2686 в другом месте. Связано: #2674, #2686, #2698, #2687, #2638.
Author
Collaborator

Прод-верификация PR #2736 — сделана мной, потому что агент оборвался сразу после мержа

Правка смержена, но её автор упал по инфраструктурной причине, не дождавшись прогона. Проверил вместо него.

Код и схема доехали:

tradein-scraper: browser_health в proxy_pool.py            → 7 вхождений
tradein-scraper: browser_probe в browser_fetcher.py        → 6
_schema_migrations: 228_scrape_proxies_browser_health.sql  → применена
scrape_proxies: browser_fail_streak · browser_unfit_since · browser_check_at

Главное условие соблюдено: вердикт браузерной пробы живёт в отдельных колонках, а не смешан с полями HTTP-пробы. Два диагноза — «узел мёртв целиком» и «узел жив для HTTP, но не для браузера» — не схлопнуты, чего я и требовал в постановке.

Проба реально отработала:

12:59:35  done  {"ok": 4, "failed": 0, "checked": 4, "browser_checked": 4, "browser_ok": 4, ...}
13:29:37  done  {"ok": 3, "failed": 1, "checked": 4, "browser_checked": 0, "browser_ok": 0, ...}

Первый прогон после деплоя прогнал браузерную пробу по всем четырём узлам, все прошли. Второй браузерную часть не запускал — такты разведены, и это правильное решение: браузерная проба дороже HTTP на порядки, гонять её 48 раз в сутки на узел незачем.

Состояние узлов сейчас: у всех четырёх browser_fail_streak = 0, browser_unfit_since пусто, browser_check_at — 12:59–13:00.

Чего проверка НЕ доказывает

Что механизм ловит непригодный узел — не доказано. Для этого нужен узел, который проходит HTTP и не проходит браузером; сейчас такого нет — все четыре здоровы по обоим трактам. То есть проверено, что проба исполняется и не даёт ложных отказов, но не то, что она срабатывает по назначению.

Показательно, что в прогоне 13:29 HTTP-проба дала один отказ, а браузерная в этот тик не шла — то есть даже отрицательный пример пока не сопоставлен.

Настоящее подтверждение придёт одним из двух путей: либо узел деградирует естественным образом (по истории — это случалось регулярно), либо кто-то воспроизведёт условие намеренно. До тех пор статус честнее держать как «работает, но по назначению ещё не срабатывало».

## Прод-верификация PR #2736 — сделана мной, потому что агент оборвался сразу после мержа Правка смержена, но её автор упал по инфраструктурной причине, не дождавшись прогона. Проверил вместо него. **Код и схема доехали:** ``` tradein-scraper: browser_health в proxy_pool.py → 7 вхождений tradein-scraper: browser_probe в browser_fetcher.py → 6 _schema_migrations: 228_scrape_proxies_browser_health.sql → применена scrape_proxies: browser_fail_streak · browser_unfit_since · browser_check_at ``` **Главное условие соблюдено:** вердикт браузерной пробы живёт в **отдельных колонках**, а не смешан с полями HTTP-пробы. Два диагноза — «узел мёртв целиком» и «узел жив для HTTP, но не для браузера» — не схлопнуты, чего я и требовал в постановке. **Проба реально отработала:** ``` 12:59:35 done {"ok": 4, "failed": 0, "checked": 4, "browser_checked": 4, "browser_ok": 4, ...} 13:29:37 done {"ok": 3, "failed": 1, "checked": 4, "browser_checked": 0, "browser_ok": 0, ...} ``` Первый прогон после деплоя прогнал браузерную пробу по всем четырём узлам, все прошли. Второй браузерную часть не запускал — такты разведены, и это правильное решение: браузерная проба дороже HTTP на порядки, гонять её 48 раз в сутки на узел незачем. Состояние узлов сейчас: у всех четырёх `browser_fail_streak = 0`, `browser_unfit_since` пусто, `browser_check_at` — 12:59–13:00. ## Чего проверка НЕ доказывает **Что механизм ловит непригодный узел — не доказано.** Для этого нужен узел, который проходит HTTP и не проходит браузером; сейчас такого нет — все четыре здоровы по обоим трактам. То есть проверено, что проба **исполняется** и **не даёт ложных отказов**, но не то, что она срабатывает по назначению. Показательно, что в прогоне 13:29 HTTP-проба дала один отказ, а браузерная в этот тик не шла — то есть даже отрицательный пример пока не сопоставлен. Настоящее подтверждение придёт одним из двух путей: либо узел деградирует естественным образом (по истории — это случалось регулярно), либо кто-то воспроизведёт условие намеренно. До тех пор статус честнее держать как «работает, но по назначению ещё не срабатывало».
Author
Collaborator

Статус на 2026-08-07 09:0x UTC: проба исполняется, но по назначению ещё ни разу не срабатывала. Задача остаётся открытой.

Формулировка выбрана намеренно. «Работает» тут было бы тем же классом ошибки, против которого задача и заводилась: утверждением о свойстве, которое не проверялось.

Что подтверждено числом

Прогоны proxy_healthcheck с момента мержа #2736 (2026-08-06 12:59) по 2026-08-07 08:36:

прогонов всего                       40
из них с браузерной пробой            7
браузерных проверок узлов            13
   из них ok                         13
   из них unfit                       0
   из них refit                       0

Такты разведены сознательно (браузерная проба дороже HTTP на порядки), и это видно в данных: 7 прогонов из 40. Состояние всех четырёх узлов: browser_fail_streak = 0, browser_unfit_since пусто.

Вердикт браузерной пробы по-прежнему живёт в отдельных колонках — два диагноза не схлопнуты, второе условие постановки соблюдено.

Чего это НЕ доказывает

Механизм без расписания: новая проба даст сигнал только на узле, который проходит обычный HTTP и не проходит браузером. Такого узла за сутки не появилось — 13 браузерных проверок, 13 успехов. Ближайший к отрицательному примеру случай тоже мимо: asocks-mobile-3 выключен по 11 подряд HTTP-отказам, то есть его поймала СТАРАЯ проба, а не новая.

Отрицательный пример пока не сопоставлен ни разу.

Критерий приёмки — записан ДО факта

Задача закрывается, когда в данных появится любая из двух строк:

  1. counters->>'browser_unfit' > 0 хотя бы в одном прогоне proxy_healthcheck, ИЛИ
  2. scrape_proxies.browser_fail_streak > 0 / browser_unfit_since IS NOT NULL хотя бы у одного узла,

и при этом узел в тот же момент проходит HTTP-пробу (consecutive_fails = 0) — то есть подтверждается именно то расхождение трактов, ради которого правка делалась.

Срок проверки: 2026-08-14. Выбран не произвольно: по истории из тела задачи реальные отказы случались 4–5 раз в сутки десять суток подряд, так что неделя без единого срабатывания — сама по себе информация. Если к этой дате условие не наступило, честных исходов два: воспроизвести деградацию намеренно (узел с живым HTTP и мёртвым браузерным трактом) или записать, что механизм не подтверждён, и не делать вид, что подтверждён.

Проверка выполнена. Проверено ровно то, что можно было проверить.

## Статус на 2026-08-07 09:0x UTC: **проба исполняется, но по назначению ещё ни разу не срабатывала**. Задача остаётся открытой. Формулировка выбрана намеренно. «Работает» тут было бы тем же классом ошибки, против которого задача и заводилась: утверждением о свойстве, которое не проверялось. ### Что подтверждено числом Прогоны `proxy_healthcheck` с момента мержа #2736 (2026-08-06 12:59) по 2026-08-07 08:36: ``` прогонов всего 40 из них с браузерной пробой 7 браузерных проверок узлов 13 из них ok 13 из них unfit 0 из них refit 0 ``` Такты разведены сознательно (браузерная проба дороже HTTP на порядки), и это видно в данных: 7 прогонов из 40. Состояние всех четырёх узлов: `browser_fail_streak = 0`, `browser_unfit_since` пусто. Вердикт браузерной пробы по-прежнему живёт в отдельных колонках — два диагноза не схлопнуты, второе условие постановки соблюдено. ### Чего это НЕ доказывает Механизм **без расписания**: новая проба даст сигнал только на узле, который проходит обычный HTTP и не проходит браузером. Такого узла за сутки не появилось — 13 браузерных проверок, 13 успехов. Ближайший к отрицательному примеру случай тоже мимо: `asocks-mobile-3` выключен по 11 подряд HTTP-отказам, то есть его поймала СТАРАЯ проба, а не новая. Отрицательный пример пока не сопоставлен ни разу. ### Критерий приёмки — записан ДО факта Задача закрывается, когда в данных появится **любая** из двух строк: 1. `counters->>'browser_unfit' > 0` хотя бы в одном прогоне `proxy_healthcheck`, ИЛИ 2. `scrape_proxies.browser_fail_streak > 0` / `browser_unfit_since IS NOT NULL` хотя бы у одного узла, и при этом узел в тот же момент проходит HTTP-пробу (`consecutive_fails = 0`) — то есть подтверждается именно то расхождение трактов, ради которого правка делалась. Срок проверки: **2026-08-14**. Выбран не произвольно: по истории из тела задачи реальные отказы случались 4–5 раз в сутки десять суток подряд, так что неделя без единого срабатывания — сама по себе информация. Если к этой дате условие не наступило, честных исходов два: воспроизвести деградацию намеренно (узел с живым HTTP и мёртвым браузерным трактом) или записать, что механизм не подтверждён, и не делать вид, что подтверждён. Проверка выполнена. Проверено ровно то, что можно было проверить.
lekss361 added the
bug
observability
scrapers
tradein
labels 2026-08-16 10:25:18 +00:00
Author
Collaborator

Статус на 21.08 (две недели после #2736): проба исполняется, событие, ради которого она заведена, не наступило

Числа с 06.08 12:59:

прогонов proxy_healthcheck              711
  из них с браузерной пробой             184
браузерных проверок узлов               221   ok 221 · unfit 0 · refit 0
HTTP-проба: failed                      110 · pair_banned 40
узлы: browser_fail_streak = 0 у всех четырёх, browser_unfit_since пусто, последняя проверка сегодня

Реальные отказы сбора за 14 суток — по 2–3 в день, и все до единого — баны площадок: QRATOR Домклика, «Avito SERP firewall … IP banned», backfill-honest-status … остановлен блоками источника. Ни одного «proxy may be down» / NS_ERROR_PROXY_BAD_GATEWAY — того класса, который в июле-августе давал 4–5 отказов в день и ради которого проба заводилась.

То есть «сработала ли браузерная проба по назначению» по-прежнему неизвестно — не потому, что она не работает, а потому, что отказа прокси, который она должна поймать, за две недели не было. Это другое утверждение, и я не подменяю его словом «работает».

Задача остаётся открытой. Следующая проверка — 04.09. Критерий закрытия тот же: первый реальный отказ узла для браузера (browser_unfit > 0 при NS_ERROR_PROXY* в прогоне) или, если за месяц класс отказа так и не вернулся, — закрыть как «механизм стоит, повод не повторился», с этой оговоркой в заголовке.

## Статус на 21.08 (две недели после #2736): проба исполняется, событие, ради которого она заведена, не наступило Числа с 06.08 12:59: ``` прогонов proxy_healthcheck 711 из них с браузерной пробой 184 браузерных проверок узлов 221 ok 221 · unfit 0 · refit 0 HTTP-проба: failed 110 · pair_banned 40 узлы: browser_fail_streak = 0 у всех четырёх, browser_unfit_since пусто, последняя проверка сегодня ``` Реальные отказы сбора за 14 суток — по 2–3 в день, и **все до единого — баны площадок**: QRATOR Домклика, «Avito SERP firewall … IP banned», `backfill-honest-status … остановлен блоками источника`. Ни одного «proxy may be down» / `NS_ERROR_PROXY_BAD_GATEWAY` — того класса, который в июле-августе давал 4–5 отказов в день и ради которого проба заводилась. То есть «сработала ли браузерная проба по назначению» по-прежнему **неизвестно** — не потому, что она не работает, а потому, что отказа прокси, который она должна поймать, за две недели не было. Это другое утверждение, и я не подменяю его словом «работает». **Задача остаётся открытой. Следующая проверка — 04.09.** Критерий закрытия тот же: первый реальный отказ узла для браузера (`browser_unfit > 0` при `NS_ERROR_PROXY*` в прогоне) или, если за месяц класс отказа так и не вернулся, — закрыть как «механизм стоит, повод не повторился», с этой оговоркой в заголовке.
Owner

Ревизия открытых задач 2026-08-30. Проверено в коде на forgejo/main — сделано, закрываю.

Проба узла ходит тем же браузерным трактом, что и работа. tests/test_2723_browser_probe.py — каждый тест падает на коде до фикса.

Ревизия открытых задач 2026-08-30. Проверено в коде на forgejo/main — сделано, закрываю. Проба узла ходит тем же браузерным трактом, что и работа. tests/test_2723_browser_probe.py — каждый тест падает на коде до фикса.
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
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#2723
No description provided.