Аудит · научное исследование · программа решений · tazzz.ru

Ёмкость без закупок

Глубокая проверка отчёта «Ёмкость живого поиска» на коде, проде и живых пробах. Вывод: отчёт верен, но щадящ — главные потолки рукотворные. 50–65% времени детального слота сервер тратит сам на себя, до половины машин парсится повторно зря, а пул прокси меньше заявленного и наполовину слеп. Программа из пяти чисто кодовых решений возвращает ёмкость ×2,5–4 без единой покупки.

Снято 27 августа 2026, прод live Метод код + Loki 7 дн + PG 60 дн + живые пробы прокси + эксперименты База отчёт «Ёмкость живого поиска» от 27.08 Покупки не требуются ни на одной ступени
Итог

Пять чисел, которые меняют картину

Каждое снято в этом аудите с прода — не из конфига и не из симуляции.

Слот занят «собой»

50–65%

Доля времени детального слота, уходящая не на площадку: запись в БД, инлайновый LLM, перевод.

Машины уже в БД

39–54%

Доля машин живых поисков, которые уже лежали в базе — и всё равно парсятся заново (no_cache).

Прокси живы

3из 4

Адрес .165 мёртв прямо сейчас (таймауты 12–20 с) — и ни один компонент системы этого не знает.

Уникальных платных IP

5

Не 9: dongchedi и encar делят одни и те же 4 адреса. Пятый — единственный у mobile.de.

Жёсткий потолок после программы

600+/час

Вместо ~240. Та же машина, те же прокси, те же 24 слота — другой код.

Отчёт «Ёмкость живого поиска» ставил диагноз «первыми сгорят прокси» и предлагал лестницу из ручного потолка, закупки прокси и железа. Аудит подтверждает диагноз, но меняет вывод: до закупок есть целый этаж бесплатной ёмкости. Прокси горят не потому, что их четыре, а потому, что каждый поиск тратит их бюджет на уже известные машины, каждый детальный слот на две трети занят внутренней работой, а мёртвый адрес неделями остаётся в ротации и кормит предохранитель ложными сбоями.

01 · Аудит

Что подтвердилось и что пришлось поправить

Все флаги, капы и механизмы отчёта проверены на живом проде. Подтверждено: env-флаги (fan-out включён), ручная пятёрка, предохранитель 8 сбоев/120 с → пауза 30 мин, che168 без прокси. Ниже — пять поправок.

К1

Пулы прокси меньше заявленных: 5 уникальных IP, а не 9

поправка

Отчёт считал «4 dongchedi + 4 encar + 1 mobile.de». Таблица proxies показывает: dongchedi и encar записаны на одни и те же четыре адреса 79.172.218.{214, 230, 247, 165}. Для банов это не страшно (баны — по паре «IP × целевой сайт»), но потеря одного адреса — это −25% ёмкости обеих площадок сразу, а не одной.

К2

Один из четырёх прокси мёртв прямо сейчас — и система слепа

живой замер

Проба всех адресов против api.encar.com: три отвечают за 0,7–1,2 с, .165 висит до таймаута 12–20 с на любой цели. При PROXY_SELECTION=random это значит: каждый четвёртый фетч dongchedi и encar сгорает в таймаут, раздувает медианы и кормит окно предохранителя ложными сбоями — тот самый механизм, что ставит площадку на паузу 30 минут для всех.

Почему слепа: счётчики success_count/failure_count в таблице proxies — нули, last_used_at пуст; статистика ProxyRotator живёт в памяти каждого из 24 процессов отдельно и умирает вместе с ними; метка proxy:dead ставится только в части путей и живёт 120 с. Ни health-проб, ни общего реестра нет.

К3

«24 слота деталей» — потолок не слотов, а раздутого времени слота

главное

Поэтапный тайминг из Loki за 7 дней (n=289) показывает: у encar фетч с площадки — 5,6 с из 13,9 с задачи, у dongchedi — 2,5 с из 18,6 с. Остальное — наша собственная работа: запись в Postgres на HDD с синхронным коммитом (1,7–4,8 с), инлайновый LLM-подбор мощности в расчёте цены (p90 — 31 секунда внутри слота), последовательные под-запросы. Жёсткий потолок «240 поисков/час» — это 24 слота × раздутое время; лечится диетой слота, а не железом. Разбор — в §02.

К4

Книга весов памяти устарела: dongchedi давно не браузерный

поправка

Вес dongchedi в admission — 250 МБ с комментарием «Playwright». Фактически листинг и детали dongchedi с июня идут через JSON-API (curl_cffi); за 7 дней Loki слово «playwright» встречается в логах воркеров 6 раз. Честный вес — ~120 МБ, как у che168/encar. Одна строка конфига удваивает допуск dongchedi-тяжёлых миксов: 11 → ~23 одновременных.

К5

«Свежие» покупают уже купленное

архитектурное

Классический живой поиск ставит no_cache=True: каждая машина листинга уходит в полный детальный фетч, даже если она в базе с полной карточкой. По 60 дням прода уже в базе были: encar — 45%, mobile.de — 54%, dongchedi — 40%, che168 — 28% машин живых сессий. При этом листинг уже несёт живую цену и пробег — то есть свежесть проверяема без детального фетча. С ростом трафика доля повторов только растёт: это главный расход прокси-бюджета впустую.

02 · Замеры

Куда уходит секунда детального слота

Поэтапные тайминги задач process_single_car_detail: Loki, 7 дней, 289 задач, средние (аддитивны по этапам). Синим — единственный этап, где мы разговариваем с площадкой.

фетч с площадки перевод расчёт цены + LLM запись в БД эмит и прочее
Средние по 7 дням (секунды); ширина — доля от самой длинной задачи (dongchedi). Точные значения — в таблице ниже. У dongchedi «расчёт» раздут хвостом: p50 этапа — 2,3 с, но при промахе кэша мощности внутри слота выполняется LLM-запрос (p90 — 31 с). У encar фетч — это 2–4 последовательных HTTP-запроса (карточка → HTML-страница → отчёт осмотра), причём каждый четвёртый заход через мёртвый прокси добавляет до 20 с.
ПлощадкаФетчПереводРасчёт+LLMЗапись БДПрочееИтого (ср.)p50 задачи
dongchedi2,5 с2,3 с11,2 с2,2 с0,4 с18,6 с9,9 с
encar5,6 с1,2 с1,8 с4,8 с0,5 с13,9 с13,8 с
che1683,1 с0,1 с0,1 с1,8 с0,2 с5,3 с4,9 с

Сходимость с отчётом: 24 слота ÷ взвешенное среднее ~12 с × 3600 ≈ 7100 деталей/час ≈ 240 сессий/час при ~30 машинах на сессию — ровно жёсткий потолок отчёта. То есть потолок воспроизводится из этих цифр, и каждая срезанная секунда слота двигает его напрямую.

Почему запись в БД стоит секунды

parsed_cars — 143 колонки, 25 текстовых (отчёты осмотра на корейском И русском, raw_json, фото-списки), 20 индексов, 3,4 ГБ. Каждый upsert на вращающемся диске (замер: rotational=1) при synchronous_commit=on ждёт fsync WAL, тащит TOAST больших JSON и обновление всех индексов — плюс в той же транзакции пишется body-class и alert-outbox. Отсюда 1,8–4,8 с на машину.

Повторяемость: сколько уже куплено

ПлощадкаМашин в живых сессиях (60 дн)Уже были в БДДоля повторов
mobile.de1628753,7%
encar59926944,9%
dongchedi3282129639,5%
che168113031828,1%

SQL по search_session_cars × parsed_cars, только result_source='live'. Плюс 15% живых сессий — точные дубли фильтров других сессий. Это при сегодняшнем трафике 8 поисков/час; с ростом аудитории пересечение растёт (популярные модели ищут чаще), то есть выигрыш кэш-переиспользования масштабируется вместе с нагрузкой.

Живая проба прокси

Адресapi.encar.comapiuscdt.che168.comВердикт
79.172.218.214200 · 0,7 с—жив
79.172.218.230200 · 1,2 с—жив
79.172.218.247200 · 1,2 с200 · 1,8 сжив, che168 доступен
79.172.218.165таймаут 12 стаймаут 20 смёртв, в ротации

Ключевой побочный результат: che168 через существующий прокси отвечает 200 за 1,8 с — значит, единая точка отказа «che168 живёт с IP сервера» снимается конфигурацией, без закупки. Баны считаются по паре «IP × целевой сайт», поэтому переиспользование адресов между площадками не увеличивает риск ни одной из них.

Эксперимент: curl_cffi отпускает GIL

Для решения Р4 (пул потоков вместо процессов) проверена ключевая гипотеза: 8 потоков Python одновременно тянут URL с задержкой 2 с через curl_cffi — общее время 2,23 с, а не 16. Сетевое ожидание в curl_cffi не держит GIL, значит детальные задачи (сеть + ожидание БД) параллелятся в потоках без потери, а слой БД уже на scoped_session (потокобезопасен по построению).

03 · Диагноз

Три архитектурные болезни

Всё измеренное сводится к трём решениям, принятым давно и по другим причинам. Ни одно не требует железа для исправления.

Б1

Свежесть куплена повторной покупкой всего

Режим «Свежие» доказывает актуальность машины полным перепарсом детали — хотя сам факт присутствия в листинге с живой ценой и пробегом уже доказывает всё, что меняется у машины со временем. ТТХ, комплектация, отчёт осмотра — неизменяемы. Платим за это дефицитнейшим ресурсом: запросами с четырёх прокси-адресов.

Б2

Детальный слот — конвейер, склеенный в одну задачу

Фетч (внешний, дефицитный) и обработка (внутренняя, дешёвая при правильном исполнении) занимают один слот последовательно. Пока слот пишет 4,8 с в Postgres на HDD или 31 с ждёт LLM, он не фетчит — а именно числом одновременных фетчей меряется анти-бан-кап площадки. Слот-время = потолок, и две трети слот-времени не относятся к площадке.

Б3

Прокси — расходник без системы управления

Выбор случайный, здоровье не проверяется, статистика не переживает процесс, предохранитель считает сбои по площадке целиком — поэтому один мёртвый адрес и таймауты от него неотличимы от бана площадки и способны погасить её для всех пользователей на 30 минут. Фиксированные капы (12/4/16) — догадки, а не измеренные пределы: реальный безопасный темп каждого IP никто не ищет.

04 · Решения

Программа из пяти решений

В порядке «эффект ÷ трудоёмкость». Для каждого: что делаем, что это даст (с формулой), чем рискуем и как проверяем.

Р1

«Дельта-Свежие»: листинг подтверждает, деталь — только новым

2–3 дня · эффект ×1,5–1,7

Что делаем. В on_page_parsed вместо безусловного no_cache — трёхходовая развилка: машина есть в БД с полной карточкой и цена/пробег листинга совпадают с сохранёнными → отдать карточку из БД как свежую (листинг только что подтвердил, что она живая и цена та же; data_freshness = сейчас). Цена изменилась → пересчитать «под ключ» от листинговой цены локально (расчёт — наш код, 0 внешних запросов) и поставить фоновый refresh детали вне слота. Новая машина → полный фетч, как сегодня.

страница листинга живая цена + пробег машина известна и карточка полная? цена совпала → из БД 0 запросов к площадке цена изменилась → локальный пересчёт + фоновый refresh новая → детальный фетч 1–4 запроса, как сегодня ~40–54% машин ~46–60% машин
Сегодня все три ветки идут по нижней. Семантика «Свежих» сохраняется: листинг — это и есть живое подтверждение площадки, что машина продаётся сейчас и по этой цене; деталь не добавляет свежести — только неизменяемые ТТХ.

Что даст. Минус 40–50% детальных фетчей уже при сегодняшнем трафике (замер §02) → прокси-бюджет и слот-время сессии падают почти вдвое, потолки площадок в сессиях/час растут в ~1,5–1,7 раза, «время до полной выдачи» на повторных машинах — секунды вместо минут. Выигрыш растёт с трафиком. Сюда же — коалесценция точных дублей (15% сессий): одинаковый (площадка × фильтры) в пределах пары минут присоединяется к уже идущей сессии.

Риск и проверка. Риск продуктовый: карточка могла стать полнее с момента сохранения (гейт pipeline_complete уже это ловит). Канарейка: метрика «доля деталей, отданных дельтой» + жалобы на устаревшие данные; кнопка «Обновить» на карточке остаётся как ручной форс.

Р2

Диета слота: убрать из него всё, что не площадка

2–3 дня · потолок 240→~600/ч

Что делаем. Три независимых правки.

  • Запись в БД. SET LOCAL synchronous_commit=off на транзакции upsert'а parsed_cars (данные переспарсиваемы; по документации PG это не риск целостности — лишь окно ~0,6 с при падении сервера) + wal_compression=on + вынести raw_json из горячего пути. Ожидание: 1,8–4,8 с → 0,2–0,5 с.
  • LLM вне слота. Промах кэша мощности не блокирует слот 30 секунд: карточка эмитится без «под ключ», отдельная задача в control-очереди добирает л.с. и дорассчитывает цену, фронт получает обновление существующим путём update_car_to_full. Убивает p90=31 с этапа «расчёт» dongchedi.
  • Диета запросов encar. Карточка и отчёт осмотра — параллельно, а не последовательно; HTML-страницу fem.encar.com не тянуть, когда inspection-ID извлекается из URL фото (этот путь уже есть — сделать его основным); одна keep-alive HTTP/2-сессия. Фетч 5,6 с → ~3–3,5 с и вдвое меньше запросов на машину — то есть вдвое больше сессий на тот же анти-бан-кап.

Что даст. Среднее слот-время: dongchedi 18,6→~5 с, encar 13,9→~5,5 с, che168 5,3→~3,5 с. По формуле потолка (24 слота × 3600 ÷ слот-время ÷ машин на сессию): жёсткий потолок ~240/ч → ~600/ч на тех же 24 слотах. Вместе с Р1 внутренний потолок уходит за 1000/ч — ограничителем становятся площадки, как и должно быть.

Риск и проверка. synchronous_commit — канон PG (см. методику); канарейка по метрике save_ms в Loki (она уже пишется). LLM-вне-слота меняет порядок появления цены на карточке — фронт уже умеет жить с поздним total_cost (механизм существует).

Р3

Флот прокси: реестр здоровья, per-IP предохранитель, адаптивный темп

3–4 дня · паузы площадок исчезают
СЕГОДНЯ ПОСЛЕ Р3 24 воркера random-выбор вслепую 4 прокси-IP .214 · .230 · .247 .165 — мёртв, но в ротации: 25% фетчей → таймаут 12–20 с никакого учёта здоровья IP сервера SPOF che168 dongchedi · encar кап-догадки 12 / 16 che168 · direct предохранитель на ВСЮ площадку воркеры просят маршрут реестр здоровья (Redis) проба каждого IP раз в 1 мин мёртвый — вон мгновенно, для всех темп AIMD на пару IP×площадка предохранитель per-IP×площадка счётчики в БД — переживают рестарт 5 маршрутов 4 IP + IP сервера, общие для всех площадок che168: 1 → 5 маршрутов dongchedi encar · che168 mobile.de пауза площадки — только если легли ВСЕ маршруты
Разница — средний блок: сегодня между воркерами и адресами нет ничего, после — общий реестр, который знает здоровье и безопасный темп каждой пары «IP × площадка». Правый край: пауза-предохранитель сужается с площадки до маршрута.

Что делаем. (1) Реестр здоровья в Redis + beat-проба раз в минуту лёгким запросом; мёртвый адрес исключается для всех процессов сразу, счётчики пишутся в таблицу proxies (она уже есть — просто не используется). (2) Предохранитель считает сбои по паре «площадка × egress-IP»; вся площадка встаёт на паузу, только если легли все её маршруты. (3) Вместо фиксированных капов — токен-бакет на IP с адаптивным темпом AIMD: успех длится — темп медленно растёт, 429/капча — темп мгновенно вдвое вниз. Система сама находит реальный безопасный предел каждого адреса, вместо того чтобы вечно жить на догадке. (4) che168 и mobile.de получают общий пул как дополнительные маршруты (проба §02 подтвердила доступность).

Что даст. Сегодняшние 25% фетчей-в-таймаут исчезают (минус ложные сбои → минус ложные 30-минутные паузы). che168: 1 маршрут → 5, потолок площадки ~80 → ~300+ сессий/час, SPOF снят. Реальные потолки площадок перестают быть константами в коде и начинают измеряться автоматически.

Риск и проверка. AIMD стартует с сегодняшних консервативных капов и растёт только на длительном успехе — хуже сегодняшнего не станет по построению. Метрика канарейки: частота срабатываний предохранителя по маршрутам (уже есть колокольчик админки).

Р4

Детали на пуле потоков: та же работа, минус 3 ГБ памяти

~неделя с канарейкой · допуск ×2

Что делаем. Воркер деталей переводится с 24 prefork-процессов на --pool=threads с 32–64 потоками. Детальная задача — чистое ожидание (сеть площадки, БД, LLM): эксперимент §02 подтвердил, что curl_cffi отпускает GIL, слой БД — на потокобезопасном scoped_session, а переводчик уже носит внутренние локи (он живёт в многопоточном gunicorn). Канарейка: второй контейнер деталей на потоках забирает часть очереди, неделя наблюдения, потом полный переход.

Что даст. Книга весов admission растёт с 2800 МБ до ~5500 МБ: одновременных поисков 11–15 → 25–35 (вместе с честным весом dongchedi из К4). Слоты деталей перестают быть дефицитом: 24 → 48–64 без роста памяти — жёсткий потолок из «24 слота» превращается в «сколько разрешают площадки». Исчезает и постоянная замена раздутых процессов (max-memory-per-child).

Риск и проверка. Главный риск — неявно непотокобезопасные углы в 5000-строчном переводчике и скраперах; поэтому канарейка, а не выкатка. Утечки памяти в одном процессе копятся — лечится плановым рестартом контейнера раз в сутки (ночное окно уже есть у кампаний).

Р5

Однострочники: снять рукотворные потолки

часы · первый шаг
  • Вес dongchedi в книге весов 250 → 120 МБ (К4): допуск dongchedi-миксов ×2 одной строкой.
  • MAX_CONCURRENT_SEARCHES 5 → 8 → 12 по плану Т2.2 — теперь с уверенностью, что fan-out в проде уже живёт.
  • Прокси-строки che168 в таблицу proxies (существующие 4 адреса) — SPOF серверного IP снят до всякого реестра.
  • Убрать мёртвый .165 из ротации руками — четверть фетчей dongchedi/encar мгновенно перестаёт гореть в таймаутах (до появления реестра Р3).
  • wal_compression=on в Postgres — дешевле WAL на HDD.
05 · Лестница

Что получается по ступеням

Оценки на медианах §02 и формуле потолка; проверяются канарейкой на каждой ступени. Все ступени — только код и конфиг.

СтупеньСоставТрудоёмкостьОдновременноЖёсткий потолокЧто снимает
Сегодня——5 (ручной)~240/ч—
A · однострочникиР5 целикомчасы11–20~300/чручную пятёрку, SPOF che168, мёртвый прокси, стёртый вес
B · архитектурные правкиР1 + Р2 + Р3~2 недели суммарно15–24~600/чповторные покупки, раздутый слот, слепой флот, паузы 30 мин
C · смена пулаР4 + AIMD в полную силу~неделя + канарейка25–35700+/ч*память как ограничитель, полку «24 слота»

* На ступени C внутренний потолок перестаёт быть ограничителем: предел задают сами площадки, и AIMD измеряет его автоматически вместо констант в коде. Дальше ёмкость растёт уже закупками (прокси, SSD/RAM из Э4, S3 для фото) — но каждая купленная единица ляжет на архитектуру, которая выжимает её полностью, а не на четверть.

Порядок внедрения и канарейка

  1. Ступень A (Р5) — сразу, наблюдая метрики отчёта Т2.2: частота предохранителя, MemAvailable в пиках, глубина очереди, время до первой карточки.
  2. Р2 (диета слота) — по одной правке: сначала synchronous_commit (метрика save_ms уже в Loki), затем LLM-вне-слота, затем encar-диета.
  3. Р3 (флот) — реестр здоровья и per-IP предохранитель; AIMD включать со стартом от текущих капов.
  4. Р1 (дельта-свежие) — за фиче-флагом (паттерн ADMISSION_CONTROL уже в кодовой базе), метрика «доля дельта-отдач».
  5. Р4 (threads) — вторым контейнером-канарейкой, неделя, потом переключение.
06 · Методика

Границы оценок и источники

Честные оговорки. Числа потолков — производные от средних 7-дневной выборки (289 задач; малый трафик = широкие доверительные интервалы). Банные пороги площадок публично не документированы — потому решение Р3 строится так, чтобы система измеряла их сама, а не так, чтобы верить нашей оценке. Выигрыш Р1 (39–54%) снят на сегодняшнем трафике и с ростом аудитории меняется в большую сторону, но точная кривая неизвестна. Threads-пул (Р4) — единственное решение с реальным техническим риском, поэтому только через канарейку.

Данные. Конфиг и код: ветка full-update + живые env прода. Тайминги: Loki, поля *_ms задач деталей за 7 дней. Повторяемость: PG, search_sessions × search_session_cars × parsed_cars за 60 дней. Прокси: таблица proxies + живые пробы curl через каждый адрес (encar/che168, 27.08). Диск/память: /sys/block (rotational), docker stats, pg_settings. Эксперимент GIL: 8 потоков × httpbin /delay/2 через curl_cffi, wall 2,23 с.

Внешние источники. Пул потоков для IO-bound задач — документация Celery (Concurrency) и разбор пулов воркеров; асинхронный коммит и его гарантии (потеря ≤ ~0,6 с без риска целостности) — PostgreSQL: Asynchronous Commit и Percona о synchronous_commit; per-транзакционное применение — selective async commits. Схема AIMD — тот же принцип, которым TCP ищет пропускную способность канала: медленный рост, мгновенный сброс при сигнале перегрузки.