tradein-backend: пул коннектов SQLAlchemy 15 против пикового спроса до 28 на /estimate; sync db.execute/commit в estimator и cian_price_history на публичном event loop #3408

Open
opened 2026-09-06 11:30:33 +00:00 by bot-backend · 1 comment
Collaborator

Хвосты deep-ревью PR #3407 (#3398 п.1). Не блокеры, но потолки, о которые упрёмся первыми:

1. Пул коннектов. app/core/db.pypool_size=5 + max_overflow=10 = 15, pool_timeout дефолт 30 с. Пиковый спрос на tradein-backend: 4 /estimate (get_db) + 4 _suggest_slots + 8 фоновых догрузок (_MAX_DEFERRED_REFRESH_TASKS, своя SessionLocal, транзакция живёт через HTTP-фетч) + до 12 коротких сессий пул-операций из потоков (после #3407 они больше не сериализуются заблокированным loop'ом) = до 28 из 15. Сервер (max_connections=100, живых 9) не узкое место — упрёмся в QueuePool limit … timed out 30. Комментарий trade_in.py:78-84 считает только 8 и про 8 фоновых сессий не знает. Решить: поднять pool_size/max_overflow под замер, или ограничить фоновые догрузки/пул-операции семафором.

2. Sync БД на loop'е рядом с правкой #3407: app/services/estimator.py:973, :1062, :1093:728 для IMV) — db.execute/db.commit() прямо на loop'е внутри тех же функций, чьи пул-операции унесены в поток; при исчерпанном пуле — тот же 30-секундный checkout на loop'е. Утверждение «весь инстанс не отвечал» закрыто частично (#3083).

3. Ещё один sync-вход на публичном loop'е: app/services/cian_price_history.py:140fetch_detail(url, config=_PoolCurlConfig(), proxy_provider=RealProxyProvider()) → own-session ветка providers/cian/detail.py:233 с синхронным curl_proxy_url; дёргается админ-ручкой admin.py:2281 в tradein-backend — по блокирующей паре acquire/release на каждый листинг батча. Перевести на acurl_proxy_url (#3407) или гонять в scraper.

4. Бюджет _with_budget на ветке acquire не соблюдается (wait_for(0.05) вернулся через 0.31 с — ждём весь acquire в потоке): 3 источника × до 30 с checkout'а сверх бюджетов = симптом #654 (502/504 на шлюзе). Записано потолком в докстринге acurl_proxy_url; закрыть либо statement_timeout/pool_timeout короче бюджета, либо не ждать поток при отмене (тогда lease освобождает поток сам).

Refs #3398, PR #3407, #3083, #654, #3405.

Хвосты deep-ревью PR #3407 (#3398 п.1). Не блокеры, но потолки, о которые упрёмся первыми: **1. Пул коннектов.** `app/core/db.py` — `pool_size=5 + max_overflow=10 = 15`, `pool_timeout` дефолт 30 с. Пиковый спрос на `tradein-backend`: 4 `/estimate` (`get_db`) + 4 `_suggest_slots` + 8 фоновых догрузок (`_MAX_DEFERRED_REFRESH_TASKS`, своя `SessionLocal`, транзакция живёт через HTTP-фетч) + до 12 коротких сессий пул-операций из потоков (после #3407 они больше не сериализуются заблокированным loop'ом) = **до 28 из 15**. Сервер (`max_connections=100`, живых 9) не узкое место — упрёмся в `QueuePool limit … timed out 30`. Комментарий `trade_in.py:78-84` считает только 8 и про 8 фоновых сессий не знает. Решить: поднять `pool_size`/`max_overflow` под замер, или ограничить фоновые догрузки/пул-операции семафором. **2. Sync БД на loop'е рядом с правкой #3407:** `app/services/estimator.py:973, :1062, :1093` (и `:728` для IMV) — `db.execute`/`db.commit()` прямо на loop'е внутри тех же функций, чьи пул-операции унесены в поток; при исчерпанном пуле — тот же 30-секундный checkout на loop'е. Утверждение «весь инстанс не отвечал» закрыто частично (#3083). **3. Ещё один sync-вход на публичном loop'е:** `app/services/cian_price_history.py:140` → `fetch_detail(url, config=_PoolCurlConfig(), proxy_provider=RealProxyProvider())` → own-session ветка `providers/cian/detail.py:233` с **синхронным** `curl_proxy_url`; дёргается админ-ручкой `admin.py:2281` в `tradein-backend` — по блокирующей паре acquire/release на каждый листинг батча. Перевести на `acurl_proxy_url` (#3407) или гонять в scraper. **4. Бюджет `_with_budget` на ветке acquire не соблюдается** (`wait_for(0.05)` вернулся через 0.31 с — ждём весь `acquire` в потоке): 3 источника × до 30 с checkout'а сверх бюджетов = симптом #654 (502/504 на шлюзе). Записано потолком в докстринге `acurl_proxy_url`; закрыть либо `statement_timeout`/`pool_timeout` короче бюджета, либо не ждать поток при отмене (тогда lease освобождает поток сам). Refs #3398, PR #3407, #3083, #654, #3405.
Author
Collaborator

Приёмка PR #3444 на проде (12.09, деплой 4efcb712).

Маркеры конфигурации (признак ОТСУТСТВУЕТ в старой версии):

BUILD_SHA=4efcb71
asyncio.shield в app/services/estimator.py — 2 вхождения   (в старой версии 0)
pool: size=5  max_overflow=15  timeout=5                    (было 5 / 10 / 30)
QueuePool-ошибок за 30 мин — 0

Поведенческая проба (6 параллельных /estimate на новых адресах, под служебным аккаунтом admin, с сэмплированием pg_stat_activity каждые 0,5 с):

5 × 200 за 0.68–1.09 с, 1 × 429 (лимитер оценок — ожидаемо)
пик idle in transaction: 2
состояние после: idle 7, active 1, idle in transaction 1

Замер «до» из разбора давал 6–8 коннектов idle in transaction при восьми параллельных оценках — то есть коннект действительно перестал жить через внешний HTTP.

Оговорка честности: проба «после» шла при N=6, а «до» мерилось при N=8 и на другом наборе адресов, так что это сравнение порядка величины, а не строгий A/B. Строгий — только повтор той же пробы на том же наборе; если понадобится, сниму.

Отдельно: pool_timeout 30 → 5 лежит отдельным коммитом в конце ветки (ae6d28d5) — откатывается одной командой, не задевая остальное. Триггер отката записан прямо у значения в app/core/db.py: любое QueuePool limit … timed out в логах бэкенда либо рост failed+zombie в scrape_runs. За первые 30 минут после деплоя — ноль и того, и другого; наблюдение продолжается сутки.

Известный остаток, названный в PR: у Циана транзакция по-прежнему живёт через весь фетч (providers/cian/valuation.py:163,176,595) — доделать можно только опт-ин параметром в общей kit-функции, у которой второй живой вызывающий (cian_history_backfill.py:458) на коммите в середине сменил бы семантику батча. На потолок пула это не влияет (задача держит один коннект независимо от длительности), только на среднюю занятость.

Соседняя дыра того же класса найдена и вынесена в #3449: geocoder.py (9 сайтов) при отмене по бюджету тоже оставляет поток работать с сессией запроса; там это на main и раньше.

**Приёмка PR #3444 на проде (12.09, деплой `4efcb712`).** Маркеры конфигурации (признак ОТСУТСТВУЕТ в старой версии): ``` BUILD_SHA=4efcb71 asyncio.shield в app/services/estimator.py — 2 вхождения (в старой версии 0) pool: size=5 max_overflow=15 timeout=5 (было 5 / 10 / 30) QueuePool-ошибок за 30 мин — 0 ``` Поведенческая проба (6 параллельных `/estimate` на новых адресах, под **служебным** аккаунтом `admin`, с сэмплированием `pg_stat_activity` каждые 0,5 с): ``` 5 × 200 за 0.68–1.09 с, 1 × 429 (лимитер оценок — ожидаемо) пик idle in transaction: 2 состояние после: idle 7, active 1, idle in transaction 1 ``` Замер «до» из разбора давал **6–8** коннектов `idle in transaction` при восьми параллельных оценках — то есть коннект действительно перестал жить через внешний HTTP. Оговорка честности: проба «после» шла при N=6, а «до» мерилось при N=8 и на другом наборе адресов, так что это сравнение порядка величины, а не строгий A/B. Строгий — только повтор той же пробы на том же наборе; если понадобится, сниму. Отдельно: `pool_timeout 30 → 5` лежит **отдельным коммитом в конце ветки** (`ae6d28d5`) — откатывается одной командой, не задевая остальное. Триггер отката записан прямо у значения в `app/core/db.py`: любое `QueuePool limit … timed out` в логах бэкенда либо рост `failed+zombie` в `scrape_runs`. За первые 30 минут после деплоя — ноль и того, и другого; наблюдение продолжается сутки. Известный остаток, названный в PR: у Циана транзакция по-прежнему живёт через весь фетч (`providers/cian/valuation.py:163,176,595`) — доделать можно только опт-ин параметром в общей kit-функции, у которой второй живой вызывающий (`cian_history_backfill.py:458`) на коммите в середине сменил бы семантику батча. На потолок пула это не влияет (задача держит один коннект независимо от длительности), только на среднюю занятость. Соседняя дыра того же класса найдена и вынесена в #3449: `geocoder.py` (9 сайтов) при отмене по бюджету тоже оставляет поток работать с сессией запроса; там это на `main` и раньше.
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#3408
No description provided.