tradein: redis недостижим из контейнера ПО СЕТИ, а не из-за переменной — «просто задать REDIS_URL» сделает поиск вчетверо медленнее и молча #2709

Closed
opened 2026-08-06 07:59:30 +00:00 by bot-backend · 3 comments
Collaborator

Пункт эпика #2674 звучал так: «REDIS_URL не задан в прод-окружении — кэш поиска всю жизнь стучится в localhost и получает отказ, хотя redis на хосте есть». Проверил на проде — это верно по эффекту, но починка не та, которая напрашивается, и наивный вариант сделает хуже.

Что на самом деле

tradein-backend:  REDIS_URL = None  →  дефолт в коде  redis://localhost:6379/0
                  сети: gendesign_shared, tradein-net

gendesign-redis-1: порт 6379/tcp (наружу не опубликован)
                  сеть: gendesign_default          ← общей сети НЕТ

Проверка сокетом изнутри tradein-backend:

localhost:6379        → ConnectionRefusedError
redis:6379            → gaierror (имя не резолвится)
tradein-redis:6379    → gaierror
gendesign-redis-1:6379→ gaierror

То есть redis недостижим ни под каким именем: он в другой сети docker-compose. Это не «забыли переменную» — это отсутствующая связность.

Почему наивная починка опасна

app/services/cache.py:27 создаёт клиент с socket_timeout=2.0, socket_connect_timeout=2.0, а get/set глотают любое исключение в logger.warning и возвращают None. Сейчас это бесплатно: localhost:6379 отвечает мгновенным отказом соединения, штрафа по времени нет.

Если задать REDIS_URL на имя, которое резолвится, но не отвечает (или не резолвится с медленным DNS), каждый поиск начнёт платить до 2 секунд на GET плюс 2 секунды на SET, и оба раза молча — потому что ошибка проглатывается и наверх уходит обычный промах кэша.

Задать переменную, не проверив связность, — прямой путь к тому, чтобы поиск стал вчетверо медленнее, и никто не понял почему.

Что это меняет в постановке

Кэш поиска не работал ни одного дня. Значит нынешняя скорость поиска — это и есть скорость без кэша, и пользователи всё это время видели именно её.

Отсюда: включение кэша — это изменение поведения, а не восстановление сломанного. Его надо обосновать замером, а не чинить по инерции. Три честных варианта:

  1. Подключить связность — добавить redis в compose-проект trade-in либо ввести gendesign-redis-1 в общую сеть, и обязательно выделить отдельный номер БД, чтобы ключи двух продуктов не пересекались.
  2. Ничего не подключать, убрать код. Если замер покажет, что поиск и без кэша укладывается в приемлемое время, SearchCache — мёртвый код с ловушкой внутри (таймауты, которые сработают, если кто-то однажды «починит» переменную).
  3. Оставить как есть — худший вариант: код выглядит работающим, ловушка остаётся заряженной.

Что нужно замерить до решения

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

Отдельно

celery в trade-in почти не используется (только refresh_search_matview), планировщик свой, поэтому брокер тут ни при чём — речь именно про кэш.

Связано: #2674, #2665 (там же предупреждение: потолок темпа логинов нельзя строить на хранилище, доступность которого не проверена — ровно этот случай).

Пункт эпика #2674 звучал так: «`REDIS_URL` не задан в прод-окружении — кэш поиска всю жизнь стучится в localhost и получает отказ, хотя redis на хосте есть». Проверил на проде — это верно по эффекту, но **починка не та, которая напрашивается**, и наивный вариант сделает хуже. ## Что на самом деле ``` tradein-backend: REDIS_URL = None → дефолт в коде redis://localhost:6379/0 сети: gendesign_shared, tradein-net gendesign-redis-1: порт 6379/tcp (наружу не опубликован) сеть: gendesign_default ← общей сети НЕТ ``` Проверка сокетом изнутри `tradein-backend`: ``` localhost:6379 → ConnectionRefusedError redis:6379 → gaierror (имя не резолвится) tradein-redis:6379 → gaierror gendesign-redis-1:6379→ gaierror ``` То есть redis недостижим **ни под каким именем**: он в другой сети docker-compose. Это не «забыли переменную» — это отсутствующая связность. ## Почему наивная починка опасна `app/services/cache.py:27` создаёт клиент с `socket_timeout=2.0, socket_connect_timeout=2.0`, а `get`/`set` глотают любое исключение в `logger.warning` и возвращают `None`. Сейчас это **бесплатно**: `localhost:6379` отвечает мгновенным отказом соединения, штрафа по времени нет. Если задать `REDIS_URL` на имя, которое **резолвится, но не отвечает** (или не резолвится с медленным DNS), каждый поиск начнёт платить до **2 секунд на GET плюс 2 секунды на SET**, и оба раза молча — потому что ошибка проглатывается и наверх уходит обычный промах кэша. **Задать переменную, не проверив связность, — прямой путь к тому, чтобы поиск стал вчетверо медленнее, и никто не понял почему.** ## Что это меняет в постановке Кэш поиска **не работал ни одного дня**. Значит нынешняя скорость поиска — это и есть скорость без кэша, и пользователи всё это время видели именно её. Отсюда: **включение кэша — это изменение поведения, а не восстановление сломанного.** Его надо обосновать замером, а не чинить по инерции. Три честных варианта: 1. **Подключить связность** — добавить redis в compose-проект trade-in либо ввести `gendesign-redis-1` в общую сеть, и обязательно выделить отдельный номер БД, чтобы ключи двух продуктов не пересекались. 2. **Ничего не подключать, убрать код.** Если замер покажет, что поиск и без кэша укладывается в приемлемое время, `SearchCache` — мёртвый код с ловушкой внутри (таймауты, которые сработают, если кто-то однажды «починит» переменную). 3. Оставить как есть — худший вариант: код выглядит работающим, ловушка остаётся заряженной. ## Что нужно замерить до решения - Реальное время ответа поиска на проде (без кэша, как сейчас) — на типичных пользовательских запросах, а не на одном. - Долю повторяющихся запросов: TTL поиска 300 с, и если одинаковые запросы в это окно почти не повторяются, кэш не даст ничего даже подключённый. - Не полагаться на «стало быстрее» после включения без сравнения на том же коде — в этом продукте уже дважды получали зелёный, но бессмысленный замер. ## Отдельно `celery` в trade-in почти не используется (только `refresh_search_matview`), планировщик свой, поэтому брокер тут ни при чём — речь именно про кэш. Связано: #2674, #2665 (там же предупреждение: потолок темпа логинов нельзя строить на хранилище, доступность которого не проверена — ровно этот случай).
Author
Collaborator

Замер повторяемости — вариант «удалить кэш» снимается

Один из двух вопросов, которые я перечислил, можно закрыть прямо сейчас. Повторяемость запросов по trade_in_estimates (совпадение тройки адрес + площадь + комнаты):

всего оценок                         1 041
из них повтор ранее виденного          631   (60.6%)
   повтор в пределах 300 с             226   (21.7%)   ← окно кэша поиска
   повтор в пределах 24 ч              542   (52.1%)   ← окно кэша оценки

Более половины оценок за сутки — повтор идентичного запроса. Кэш оценки с TTL 86400 попадал бы на 52% обращений, кэш поиска с TTL 300 — на 22%.

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

Остаётся замерить только время ответа без кэша — чтобы понимать, что именно экономится: заметные пользователю секунды или незаметные миллисекунды. Это определит срочность, но уже не сам выбор.

Оговорка к методу: часть повторов — один и тот же пользователь, переоткрывающий свою оценку. Для кэша это ровно тот случай, который он и обслуживает, так что на вывод это не влияет.

## Замер повторяемости — вариант «удалить кэш» снимается Один из двух вопросов, которые я перечислил, можно закрыть прямо сейчас. Повторяемость запросов по `trade_in_estimates` (совпадение тройки адрес + площадь + комнаты): ``` всего оценок 1 041 из них повтор ранее виденного 631 (60.6%) повтор в пределах 300 с 226 (21.7%) ← окно кэша поиска повтор в пределах 24 ч 542 (52.1%) ← окно кэша оценки ``` **Более половины оценок за сутки — повтор идентичного запроса.** Кэш оценки с TTL 86400 попадал бы на 52% обращений, кэш поиска с TTL 300 — на 22%. Это не «попадания кэша» буквально (таблица фиксирует запросы, а не обращения к кэшу), но порядок ясен: спрос на кэш реальный, а не гипотетический. Поэтому **вариант «убрать код как мёртвый» отпадает** — остаётся выбор между «подключить связность» и «оставить заряженную ловушку», а второе не вариант. Остаётся замерить только время ответа без кэша — чтобы понимать, что именно экономится: заметные пользователю секунды или незаметные миллисекунды. Это определит срочность, но уже не сам выбор. Оговорка к методу: часть повторов — один и тот же пользователь, переоткрывающий свою оценку. Для кэша это ровно тот случай, который он и обслуживает, так что на вывод это не влияет.
Author
Collaborator

Мой замер «спрос на кэш реальный» был неверен дважды

Выше я писал, что вариант «убрать кэш как мёртвый код» отпадает, потому что 60.6% запросов — повторы. Обе половины этого вывода неверны, и разбор (PR #2763) это показал.

1. Замер относится к ДРУГОМУ эндпоинту. Повторяемость я мерил по таблице оценок, то есть по маршруту /estimate. А кэшируется только POST /api/v1/search. Более того: константы времени жизни для оценки и карточки дома — мёртвые, вызывающих у них нет. Кэша оценки, к которому относились мои 52%, в коде не существует.

2. Темп завышен примерно в 70 раз. «1041 оценка» — это не за сутки, а за 71 день:

1058 оценок · 2026-05-27 → 2026-08-06 · = 14.9 в сутки

3. И главное: кэшируемый эндпоинт мёртв. По логам обратного прокси за 16.2 суток — 25 163 запроса и ноль обращений к /api/v1/search; во фронтенде вызова этого маршрута нет вовсе. Для сравнения за тот же период: непрочитанные в поддержке 806, подсказки геокодера 261, вход 61.

То есть аргумент «спрос реальный» держался на измерении не того маршрута, с завышенным в 70 раз темпом, при фактически нулевом трафике на том, что кэшируется.

Ещё три поправки к постановке

Мир не делится на «мгновенный отказ» и «две секунды». Замер реальным клиентом: localhost 16.6 мс, несуществующее имя 123.5 мс, «резолвится и молчит» 2003.7 мс. Моя двоичная картина была неверна — промежуточный случай стоит вчетверо дороже нынешнего.

Свой redis в стеке trade-in не заработал бы вообще. Деплой поднимает жёстко заданный список сервисов с отменой зависимостей — новый сервис в него не попадает и никогда бы не стартовал, а переменная указывала бы в пустоту. Правка списка = правка замороженного файла деплоя (#2680). Это и решило выбор в пользу общего экземпляра — не эстетика.

Что в итоге сделано и чего это стоит

Связность дана, изоляция по номеру базы работает, попадание в кэш проверено фактом:

первый запрос  0.612 с  промах
второй         0.008 с  попадание   ← 78×
тяжёлый (34 407 строк): 0.226 → 0.008 с

брокер задач (2166 ключей) не тронут · наши ключи в отдельной базе, 2 шт.

Честный вывод: пользователь этого не увидит. Медиана поиска без кэша 155 мс, и на маршруте с нулевым трафиком ускорение в 78 раз ничего не меняет. Ценность правки другая — снята заряженная ловушка (наивная установка переменной стоила бы по две секунды на запрос молча) и появилось хранилище с проверенной доступностью, которое нужно потолку темпа логинов (#2665): там трафик есть, 61 запрос за 16 суток.

Так и стоит это записать: не «ускорили поиск», а «убрали мину и подготовили место».

## Мой замер «спрос на кэш реальный» был неверен дважды Выше я писал, что вариант «убрать кэш как мёртвый код» отпадает, потому что 60.6% запросов — повторы. Обе половины этого вывода неверны, и разбор (PR #2763) это показал. **1. Замер относится к ДРУГОМУ эндпоинту.** Повторяемость я мерил по таблице оценок, то есть по маршруту `/estimate`. А кэшируется **только** `POST /api/v1/search`. Более того: константы времени жизни для оценки и карточки дома — **мёртвые**, вызывающих у них нет. Кэша оценки, к которому относились мои 52%, **в коде не существует**. **2. Темп завышен примерно в 70 раз.** «1041 оценка» — это не за сутки, а за **71 день**: ``` 1058 оценок · 2026-05-27 → 2026-08-06 · = 14.9 в сутки ``` **3. И главное: кэшируемый эндпоинт мёртв.** По логам обратного прокси за 16.2 суток — **25 163 запроса и ноль обращений** к `/api/v1/search`; во фронтенде вызова этого маршрута нет вовсе. Для сравнения за тот же период: непрочитанные в поддержке 806, подсказки геокодера 261, вход 61. То есть аргумент «спрос реальный» держался на измерении не того маршрута, с завышенным в 70 раз темпом, при фактически нулевом трафике на том, что кэшируется. ## Ещё три поправки к постановке **Мир не делится на «мгновенный отказ» и «две секунды».** Замер реальным клиентом: `localhost` 16.6 мс, **несуществующее имя 123.5 мс**, «резолвится и молчит» 2003.7 мс. Моя двоичная картина была неверна — промежуточный случай стоит вчетверо дороже нынешнего. **Свой redis в стеке trade-in не заработал бы вообще.** Деплой поднимает жёстко заданный список сервисов с отменой зависимостей — новый сервис в него не попадает и никогда бы не стартовал, а переменная указывала бы в пустоту. Правка списка = правка замороженного файла деплоя (#2680). Это и решило выбор в пользу общего экземпляра — не эстетика. ## Что в итоге сделано и чего это стоит Связность дана, изоляция по номеру базы работает, попадание в кэш проверено фактом: ``` первый запрос 0.612 с промах второй 0.008 с попадание ← 78× тяжёлый (34 407 строк): 0.226 → 0.008 с брокер задач (2166 ключей) не тронут · наши ключи в отдельной базе, 2 шт. ``` **Честный вывод: пользователь этого не увидит.** Медиана поиска без кэша 155 мс, и на маршруте с нулевым трафиком ускорение в 78 раз ничего не меняет. Ценность правки другая — **снята заряженная ловушка** (наивная установка переменной стоила бы по две секунды на запрос молча) и появилось хранилище **с проверенной доступностью**, которое нужно потолку темпа логинов (#2665): там трафик есть, 61 запрос за 16 суток. Так и стоит это записать: не «ускорили поиск», а «убрали мину и подготовили место».
Author
Collaborator

ЗАКРЫТО — проверка на проде 2026-08-07 09:0x UTC

Проверял не по коду, а живым обращением из боевого контейнера.

tradein-backend: REDIS_URL = redis://gendesign-redis:6379/1
ping                        → True, 18.9 мс
GET по несуществующему ключу x20 → медиана 0.13 мс, максимум 0.21 мс
gendesign-redis-1 keyspace: db0 = 2167 ключей (брокер celery, не тронут)
                            db1 = 0 ключей (наш кэш; трафика на /api/v1/search нет)

Главная опасность постановки снята числом. Задача предупреждала: если задать переменную на имя, которое резолвится, но не отвечает, каждый поиск заплатит до 2000 мс на GET и столько же на SET, и оба раза молча. Замер даёт 0.13 мс медиану при socket_timeout=2.0 — то есть выбран не «резолвится и молчит», а рабочая связность.

Изоляция по номеру базы работает: 2167 ключей брокера лежат в db0, наша db1 отдельная. Ключей в ней ноль — и это ожидаемо, а не отказ: кэшируемый маршрут POST /api/v1/search не вызывается ни фронтендом, ни кем-либо ещё (25 163 запроса за 16.2 суток, ноль на этот путь).

Ноль имеет причину: маршрут мёртв, а не кэш сломан.

Из трёх вариантов постановки выбран первый (подключить связность), и выбор обоснован тем, что вариант «удалить код» опровергнут не спросом, а невозможностью: код всё равно нужен потолку темпа логинов (#2665), где трафик есть. Автор PR #2763 сам опроверг свой же предыдущий замер (52% повторов относились к другому эндпоинту, темп завышен в 70 раз) — это ровно то, что должно было случиться до правки, и случилось.

Честная формулировка результата остаётся авторской: не «ускорили поиск», а «убрали мину и подготовили место».

## ЗАКРЫТО — проверка на проде 2026-08-07 09:0x UTC Проверял не по коду, а живым обращением из боевого контейнера. ``` tradein-backend: REDIS_URL = redis://gendesign-redis:6379/1 ping → True, 18.9 мс GET по несуществующему ключу x20 → медиана 0.13 мс, максимум 0.21 мс gendesign-redis-1 keyspace: db0 = 2167 ключей (брокер celery, не тронут) db1 = 0 ключей (наш кэш; трафика на /api/v1/search нет) ``` **Главная опасность постановки снята числом.** Задача предупреждала: если задать переменную на имя, которое резолвится, но не отвечает, каждый поиск заплатит до 2000 мс на GET и столько же на SET, и оба раза молча. Замер даёт 0.13 мс медиану при `socket_timeout=2.0` — то есть выбран не «резолвится и молчит», а рабочая связность. Изоляция по номеру базы работает: 2167 ключей брокера лежат в db0, наша db1 отдельная. Ключей в ней ноль — и это ожидаемо, а не отказ: кэшируемый маршрут `POST /api/v1/search` не вызывается ни фронтендом, ни кем-либо ещё (25 163 запроса за 16.2 суток, ноль на этот путь). **Ноль имеет причину: маршрут мёртв, а не кэш сломан.** Из трёх вариантов постановки выбран первый (подключить связность), и выбор обоснован тем, что вариант «удалить код» опровергнут не спросом, а невозможностью: код всё равно нужен потолку темпа логинов (#2665), где трафик есть. Автор PR #2763 сам опроверг свой же предыдущий замер (52% повторов относились к другому эндпоинту, темп завышен в 70 раз) — это ровно то, что должно было случиться до правки, и случилось. Честная формулировка результата остаётся авторской: не «ускорили поиск», а «убрали мину и подготовили место».
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#2709
No description provided.