[HIGH] tradein: 6 из 7 запасных прокси в окружении мертвы — подписка mobileproxy не отвечает с начала июля #2613

Closed
opened 2026-08-01 19:05:09 +00:00 by lekss361 · 2 comments
Owner

Найдено 2026-08-01 при проверке эффекта от PR #2609 (самовосстановление пула). Прямая проба каждого прокси из контейнера tradein-scraper.

Что измерено

Запасные прокси из переменных окружения — то, куда код падает, когда пул не может выдать узел:

переменная адрес результат
AVITO_PROXY_URL ard.mobileproxy.space:1055 407 Proxy Authentication Required
CIAN_PROXY_URL ha.mobileproxy.space:2014 Connection refused
YANDEX_PROXY_URL aup.mobileproxy.space:1080 Invalid username/password
BROWSER_PROXY_AVITO ard.mobileproxy.space:1055 407
BROWSER_PROXY_CIAN ha.mobileproxy.space:2013 Connection refused
BROWSER_PROXY_YANDEX aup.mobileproxy.space:1079 407
SCRAPER_PROXY_URL 212.8.249.134:10423 (ASocks) OK, exit 45.140.53.192

То же и в пуле — четыре записи mobileproxy (scrape_proxies id 2, 3, 4, 5) выключены с consecutive_fails=5, last_check_at у них с 4-9 июля. Прямая проба сейчас: id 2 — connection refused, id 3/4/5 — 407.

Диагноз: учётные данные mobileproxy больше не принимаются. Похоже, подписка кончилась или ключи сменились в начале июля.

Почему это важно

  1. Запасной путь ведёт в никуда. Логика проста: пул не выдал узел → берём прокси из окружения. Для Авито, Циана и Яндекса этот прокси мёртв, то есть «запасной вариант» гарантированно проваливает запрос вместо того, чтобы деградировать.
  2. Существующая ручная ротация ротирует мертвецов. POST /scraper/{source}/rotate-ip дёргает *_PROXY_ROTATE_URL — ссылки changeip тех же мёртвых аккаунтов mobileproxy.
  3. Ложная уверенность. Переменные заданы, ручка отвечает, в конфигурации всё выглядит настроенным. Никакой проверки, что за этими адресами кто-то есть, нигде нет.

Сейчас сбор работает, потому что путь через пул отрабатывает: в логах за сутки ноль срабатываний запасного пути (grep 'fallback to env proxy'). То есть проблема латентная — стреляет ровно тогда, когда пул исчерпан, а это худший момент.

Смягчающее обстоятельство

PR #2609 (уже в проде) уменьшил зависимость от запасного пути: acquire() теперь при отсутствии свободных своих берёт узел чужой affinity, а проверка здоровья возвращает в строй ошибочно выключенные. Живых узлов сейчас 4 (все ASocks). Но при их исчерпании код по-прежнему упрётся в мёртвый запасной.

Что делать

Требует решения владельца: подписка mobileproxy жива или её не продлевали? От этого зависит выбор.

  • Если не нужна — выпилить мёртвые переменные (AVITO_PROXY_URL, CIAN_PROXY_URL, YANDEX_PROXY_URL, BROWSER_PROXY_*, *_PROXY_ROTATE_URL) из .env.runtime и удалить/пометить четыре записи scrape_proxies id 2-5. Заодно снимет вторую проблему: в rotate_url этих строк лежат ссылки changeip с секретом mobileproxy в query — из-за них глубокое ревью PR #2611 нашло блокер (наш токен ASocks уходил бы на чужой хост при вызове ротации для такой строки).
  • Если нужна — обновить учётные данные, после чего пул сам вернёт узлы в строй (проверка здоровья теперь опрашивает и выключенные, PR #2609).

Независимо от решения: запасной путь должен отличать «прокси нет» от «прокси есть, но мёртвый». Сейчас оба случая выглядят одинаково — запрос просто падает. Минимум — предупреждение в лог при обращении к env-прокси, который не проходил проверку здоровья.

Связано: #2600 (архитектура пула), #2610 (ручное выключение), PR #2609, PR #2611.

Найдено 2026-08-01 при проверке эффекта от PR #2609 (самовосстановление пула). Прямая проба каждого прокси из контейнера `tradein-scraper`. ## Что измерено Запасные прокси из переменных окружения — то, куда код падает, когда пул не может выдать узел: | переменная | адрес | результат | |---|---|---| | `AVITO_PROXY_URL` | `ard.mobileproxy.space:1055` | ❌ 407 Proxy Authentication Required | | `CIAN_PROXY_URL` | `ha.mobileproxy.space:2014` | ❌ Connection refused | | `YANDEX_PROXY_URL` | `aup.mobileproxy.space:1080` | ❌ Invalid username/password | | `BROWSER_PROXY_AVITO` | `ard.mobileproxy.space:1055` | ❌ 407 | | `BROWSER_PROXY_CIAN` | `ha.mobileproxy.space:2013` | ❌ Connection refused | | `BROWSER_PROXY_YANDEX` | `aup.mobileproxy.space:1079` | ❌ 407 | | `SCRAPER_PROXY_URL` | `212.8.249.134:10423` (ASocks) | ✅ OK, exit 45.140.53.192 | То же и в пуле — четыре записи mobileproxy (`scrape_proxies` id 2, 3, 4, 5) выключены с `consecutive_fails=5`, `last_check_at` у них с **4-9 июля**. Прямая проба сейчас: id 2 — connection refused, id 3/4/5 — 407. Диагноз: учётные данные mobileproxy больше не принимаются. Похоже, подписка кончилась или ключи сменились в начале июля. ## Почему это важно 1. **Запасной путь ведёт в никуда.** Логика проста: пул не выдал узел → берём прокси из окружения. Для Авито, Циана и Яндекса этот прокси мёртв, то есть «запасной вариант» гарантированно проваливает запрос вместо того, чтобы деградировать. 2. **Существующая ручная ротация ротирует мертвецов.** `POST /scraper/{source}/rotate-ip` дёргает `*_PROXY_ROTATE_URL` — ссылки changeip тех же мёртвых аккаунтов mobileproxy. 3. **Ложная уверенность.** Переменные заданы, ручка отвечает, в конфигурации всё выглядит настроенным. Никакой проверки, что за этими адресами кто-то есть, нигде нет. Сейчас сбор работает, потому что путь через пул отрабатывает: в логах за сутки **ноль** срабатываний запасного пути (`grep 'fallback to env proxy'`). То есть проблема латентная — стреляет ровно тогда, когда пул исчерпан, а это худший момент. ## Смягчающее обстоятельство PR #2609 (уже в проде) уменьшил зависимость от запасного пути: `acquire()` теперь при отсутствии свободных своих берёт узел чужой affinity, а проверка здоровья возвращает в строй ошибочно выключенные. Живых узлов сейчас 4 (все ASocks). Но при их исчерпании код по-прежнему упрётся в мёртвый запасной. ## Что делать **Требует решения владельца:** подписка mobileproxy жива или её не продлевали? От этого зависит выбор. - **Если не нужна** — выпилить мёртвые переменные (`AVITO_PROXY_URL`, `CIAN_PROXY_URL`, `YANDEX_PROXY_URL`, `BROWSER_PROXY_*`, `*_PROXY_ROTATE_URL`) из `.env.runtime` и удалить/пометить четыре записи `scrape_proxies` id 2-5. Заодно снимет вторую проблему: в `rotate_url` этих строк лежат ссылки changeip **с секретом mobileproxy в query** — из-за них глубокое ревью PR #2611 нашло блокер (наш токен ASocks уходил бы на чужой хост при вызове ротации для такой строки). - **Если нужна** — обновить учётные данные, после чего пул сам вернёт узлы в строй (проверка здоровья теперь опрашивает и выключенные, PR #2609). **Независимо от решения:** запасной путь должен отличать «прокси нет» от «прокси есть, но мёртвый». Сейчас оба случая выглядят одинаково — запрос просто падает. Минимум — предупреждение в лог при обращении к env-прокси, который не проходил проверку здоровья. Связано: #2600 (архитектура пула), #2610 (ручное выключение), PR #2609, PR #2611.
Author
Owner

Владелец подтвердил: подписку mobileproxy закрыли, продлевать не будут. Значит вариант «обновить учётные данные» отпадает, идём по пути выпиливания.

⚠️ Но удалять переменные окружения НЕЛЬЗЯ первым шагом

Проверил, что произойдёт при пустом прокси. providers/_proxy.py:62-65:

if lease is None:
    # off / пул пуст / ошибка acquire → env как сейчас (сбор не ломаем).
    yield env_fallback_url
    return

env_fallback_url = None означает не отказ, а прямое подключение с IP сервера. То же в браузерном контейнере: browser/server.py:213-224_provider_proxy() при отсутствии BROWSER_PROXY_* и legacy-переменной возвращает None, и camoufox идёт напрямую.

То есть сейчас мёртвая переменная случайно работает предохранителем: запрос падает с ошибкой прокси вместо того, чтобы уйти с нашего адреса. Удали её — и скрапер начнёт ходить на Авито и Циан с IP сервера, светя его.

Правильный порядок:

  1. Сначала fail-closed в коде. Отсутствие прокси в проде должно означать «не ходить», а не «идти напрямую». Аккуратно: в dev прямое подключение легитимно (так и написано в комментарии cian/detail.py:118), поэтому нужен признак окружения или явный флаг, а не безусловный запрет.
  2. Потом чистка переменных из .env.runtime: AVITO_PROXY_URL, CIAN_PROXY_URL, YANDEX_PROXY_URL, BROWSER_PROXY_AVITO, BROWSER_PROXY_CIAN, BROWSER_PROXY_YANDEX, AVITO_PROXY_ROTATE_URL, CIAN_PROXY_ROTATE_URL, YANDEX_PROXY_ROTATE_URL.

Промежуточный безопасный вариант, если хочется закрыть быстро и без правки горячего пути: не удалять переменные, а переставить их на живой прокси — тот же SCRAPER_PROXY_URL (резидентный ASocks, 212.8.249.134:10423, проверен, работает). Одна правка значений, кода не касается, прямого подключения не возникает.

Что уже делается

PR на вычистку пула — удаление четырёх мёртвых записей (id 2, 3, 4, 5) вместе с rotate_url, где у трёх лежит чужой API-ключ открытым текстом. Именно неоднородность этой колонки породила блокер в PR #2611 (наш токен ASocks ушёл бы на changeip.mobileproxy.space). Пин хоста уже в проде и это перекрывает, но держать чужой секрет в базе незачем.

Побочное следствие

POST /api/v1/admin/scraper/{source}/rotate-ip после чистки станет мёртвым кодом: он дёргает *_PROXY_ROTATE_URL, то есть changeip-ссылки закрытых аккаунтов. Ротация живых узлов теперь идёт через новый POST /api/v1/admin/proxies/{proxy_id}/rotate (PR #2611, в проде). Старую ручку стоит удалить вместе с переменными на шаге 2.

Пул после чистки

Останутся четыре узла ASocks — asocks-residential-1 (affinity domclick) и три мобильных (any). Все проверены живой пробой, у всех заполнен rotate_url на api.asocks.com, секретов в нём нет. Самовосстановление (PR #2609) вернёт узел в строй, если он выпадет из-за транзиентного сбоя.

Владелец подтвердил: **подписку mobileproxy закрыли, продлевать не будут.** Значит вариант «обновить учётные данные» отпадает, идём по пути выпиливания. ## ⚠️ Но удалять переменные окружения НЕЛЬЗЯ первым шагом Проверил, что произойдёт при пустом прокси. `providers/_proxy.py:62-65`: ```python if lease is None: # off / пул пуст / ошибка acquire → env как сейчас (сбор не ломаем). yield env_fallback_url return ``` `env_fallback_url = None` означает не отказ, а **прямое подключение с IP сервера**. То же в браузерном контейнере: `browser/server.py:213-224` — `_provider_proxy()` при отсутствии `BROWSER_PROXY_*` и legacy-переменной возвращает `None`, и camoufox идёт напрямую. То есть сейчас мёртвая переменная случайно работает предохранителем: запрос падает с ошибкой прокси вместо того, чтобы уйти с нашего адреса. Удали её — и скрапер начнёт ходить на Авито и Циан с IP сервера, светя его. **Правильный порядок:** 1. **Сначала fail-closed в коде.** Отсутствие прокси в проде должно означать «не ходить», а не «идти напрямую». Аккуратно: в dev прямое подключение легитимно (так и написано в комментарии `cian/detail.py:118`), поэтому нужен признак окружения или явный флаг, а не безусловный запрет. 2. **Потом чистка переменных** из `.env.runtime`: `AVITO_PROXY_URL`, `CIAN_PROXY_URL`, `YANDEX_PROXY_URL`, `BROWSER_PROXY_AVITO`, `BROWSER_PROXY_CIAN`, `BROWSER_PROXY_YANDEX`, `AVITO_PROXY_ROTATE_URL`, `CIAN_PROXY_ROTATE_URL`, `YANDEX_PROXY_ROTATE_URL`. Промежуточный безопасный вариант, если хочется закрыть быстро и без правки горячего пути: **не удалять переменные, а переставить их на живой прокси** — тот же `SCRAPER_PROXY_URL` (резидентный ASocks, `212.8.249.134:10423`, проверен, работает). Одна правка значений, кода не касается, прямого подключения не возникает. ## Что уже делается PR на вычистку пула — удаление четырёх мёртвых записей (`id 2, 3, 4, 5`) вместе с `rotate_url`, где у трёх лежит **чужой API-ключ открытым текстом**. Именно неоднородность этой колонки породила блокер в PR #2611 (наш токен ASocks ушёл бы на `changeip.mobileproxy.space`). Пин хоста уже в проде и это перекрывает, но держать чужой секрет в базе незачем. ## Побочное следствие `POST /api/v1/admin/scraper/{source}/rotate-ip` после чистки станет мёртвым кодом: он дёргает `*_PROXY_ROTATE_URL`, то есть changeip-ссылки закрытых аккаунтов. Ротация живых узлов теперь идёт через новый `POST /api/v1/admin/proxies/{proxy_id}/rotate` (PR #2611, в проде). Старую ручку стоит удалить вместе с переменными на шаге 2. ## Пул после чистки Останутся четыре узла ASocks — `asocks-residential-1` (affinity `domclick`) и три мобильных (`any`). Все проверены живой пробой, у всех заполнен `rotate_url` на `api.asocks.com`, секретов в нём нет. Самовосстановление (PR #2609) вернёт узел в строй, если он выпадет из-за транзиентного сбоя.
Collaborator

Устранено. Замер на проде 2026-08-05 12:21 UTC — все три утверждения перестали быть верны.

Пул

id label affinity enabled fails exit_ip disabled_reason
1 asocks-residential-1 domclick t 0 46.8.110.75
9 asocks-mobile-1 any t 0 5.227.31.243
10 asocks-mobile-2 any t 0 109.105.94.120
11 asocks-mobile-3 any t 0 92.100.84.113

Мёртвых записей mobileproxy (id 2-5) больше нет — вычищены миграцией 201. Выключенных ноль. rotate_url у всех четырёх ведёт на api.asocks.com, ни одной ссылки на mobileproxy не осталось — то есть чужой секрет в query-строке из базы ушёл. Health-проба раз в 30 минут, последний результат {"ok":4,"failed":0,"checked":4,"revived":0}.

Окружение

Из прокси-переменных в контейнерах остались только SCRAPER_PROXY_URL (живой ASocks residential), USE_PROXY_POOL_CURL/BROWSER=true и AVITO_PROXY_MAX_ROTATIONS. Переменных AVITO_PROXY_URL, CIAN_PROXY_URL, YANDEX_PROXY_URL, BROWSER_PROXY_*, *_PROXY_ROTATE_URL нет ни одной — снесены в #2650.

Опасение из твоего комментария закрыто

Ты писал: «удалишь переменную — пойдём напрямую с IP сервера». Порядок соблюдён обратный — сначала отказ, потом чистка. В providers/_proxy.py при пустом пуле в проде запрос не отправляется вообще (refusing (no HTTP request)), а ENVIRONMENT=production подтверждён во всех трёх контейнерах. То есть даже если пул опустеет, прямого выхода с IP сервера не будет.

Что осталось (не по этой задаче)

Ротация exit-IP мертва. ASOCKS_API_TOKEN в контейнере пуст, таблица scrape_proxy_rotations пуста за всю историю — эндпоинт ротации (#2611) не срабатывал ни разу. Это единственный реальный рычаг против бана Авито, и он ждёт токен от тебя.

Пул тонкий. Три узла affinity any на avito+cian+yandex при эксклюзивной аренде, плюс один закреплён за domclick. Лучше, чем два узла в момент заведения #2600, но запаса нет — это #2638.

Устранено. Замер на проде 2026-08-05 12:21 UTC — все три утверждения перестали быть верны. ## Пул | id | label | affinity | enabled | fails | exit_ip | disabled_reason | |---|---|---|---|---|---|---| | 1 | asocks-residential-1 | domclick | t | 0 | 46.8.110.75 | — | | 9 | asocks-mobile-1 | any | t | 0 | 5.227.31.243 | — | | 10 | asocks-mobile-2 | any | t | 0 | 109.105.94.120 | — | | 11 | asocks-mobile-3 | any | t | 0 | 92.100.84.113 | — | Мёртвых записей mobileproxy (id 2-5) больше нет — вычищены миграцией 201. Выключенных ноль. `rotate_url` у всех четырёх ведёт на `api.asocks.com`, ни одной ссылки на mobileproxy не осталось — то есть **чужой секрет в query-строке из базы ушёл**. Health-проба раз в 30 минут, последний результат `{"ok":4,"failed":0,"checked":4,"revived":0}`. ## Окружение Из прокси-переменных в контейнерах остались только `SCRAPER_PROXY_URL` (живой ASocks residential), `USE_PROXY_POOL_CURL/BROWSER=true` и `AVITO_PROXY_MAX_ROTATIONS`. Переменных `AVITO_PROXY_URL`, `CIAN_PROXY_URL`, `YANDEX_PROXY_URL`, `BROWSER_PROXY_*`, `*_PROXY_ROTATE_URL` **нет ни одной** — снесены в #2650. ## Опасение из твоего комментария закрыто Ты писал: «удалишь переменную — пойдём напрямую с IP сервера». Порядок соблюдён обратный — сначала отказ, потом чистка. В `providers/_proxy.py` при пустом пуле в проде запрос не отправляется вообще (`refusing (no HTTP request)`), а `ENVIRONMENT=production` подтверждён во всех трёх контейнерах. То есть даже если пул опустеет, прямого выхода с IP сервера не будет. ## Что осталось (не по этой задаче) **Ротация exit-IP мертва.** `ASOCKS_API_TOKEN` в контейнере пуст, таблица `scrape_proxy_rotations` пуста за всю историю — эндпоинт ротации (#2611) не срабатывал ни разу. Это единственный реальный рычаг против бана Авито, и он ждёт токен от тебя. **Пул тонкий.** Три узла affinity `any` на avito+cian+yandex при эксклюзивной аренде, плюс один закреплён за domclick. Лучше, чем два узла в момент заведения #2600, но запаса нет — это #2638.
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#2613
No description provided.