fix(db): /dev/shm боевого postgres — 64 МБ умолчания Docker → 1 ГБ #2819
No reviewers
Labels
No labels
Fable 5 ревью
GG-форсайт
admin
analytics
auth
automation
bug
business
chore
ci
compliance
data
data-moat
docs
duplicate
dx
enhancement
feedback/max
generative
needs-discussion
needs-human
observability
pause-bots
performance
priority/p0
priority/p1
priority/p2
priority/p3
scope/backend
scope/db
scope/devops
scope/frontend
scope/qa
scrapers
security
site-finder
stage/1
stage/2
status/blocked
status/done
status/needs-analysis
status/needs-fix
status/qa
status/ready
status/review
status/wip
tech-debt
tradein
ux
week ревью 1
wontfix
ИРД
вторичка
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: lekss361/gendesign#2819
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "fix/2812-postgres-shm"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Что меняется
Одна строка у сервиса
postgresвdocker-compose.prod.yml:Больше в сервисе не тронуто ничего (
ports,volumes,restart, healthcheck, сети — как были).Почему это простой, а не «просто деплой»
docker-compose.prod.yml— триггерdeploy.yml, аshm_sizeэто свойство создания контейнера.Шаг
docker compose -p gendesign -f docker-compose.prod.yml up -d(deploy.yml:421) захватывает иpostgres,поэтому первый же деплой после merge пересоздаст контейнер БД, когда бы этот деплой ни случился.
restartтут не поможет — свойство читается только при создании.postgres_dataпереживает пересоздание,down -vне выполняется.Окно
По
beat_schedule.py: плотная батч-полоса 03:00–07:00 МСК (гуще всего пн/вт/ср), ежедневная задачав 09:00 МСК. Постоянно тикают только zombie-cleanup'ы (раз в минуту и раз в 2 минуты) — пропущенный тик
безвреден, следующий подберёт. Пользовательская нагрузка — рабочий день (сам отказ случился в 11:46 МСК).
Наименее плохое окно: 01:00–02:30 МСК (после людей, до батчей) либо 21:00–23:00 МСК.
Худшее — 03:00–07:00 МСК.
Обоснование 1 ГиБ — по замерам, не по рекомендации
dynamic_shared_memory_type = posix(прод, изpostgresql.conf) → сегменты DSM параллельных плановживут в
/dev/shm. У контейнера там 64 МБ (умолчание Docker;ShmSize=67108864в inspect).Замеры на проде 2026-08-10:
Арифметика сходится с инцидентом: два backend-процесса упёрлись, пока остальные держали память.
Нижняя граница. 128 МиБ хватает лишь на ~7–8 одновременных. Потолок конкурентности —
celery
--concurrency=8плюс request-path, то есть ~12 тяжёлых запросов → минимум 256 МиБ.Локальный A/B это подтвердил: при 6 одновременных запросах реальный спрос 113 МиБ, то есть 128 МиБ
заполнены на 88% — впритык.
Верхняя граница.
/dev/shm— tmpfs, страницы выделяются по факту: значение это потолок, а не резерв(свежий контейнер с
1g:1.0G 1.1M 1023M, занято 0). Платить приходится только за то, что реальновзято. На хосте 11 ГиБ RAM, свободно ~6 ГиБ, swap уже занят целиком (3/3 ГиБ) — то есть при росте
tmpfs вытеснять некуда, он пойдёт прямо из RAM. Поэтому выше 2 ГиБ на этом хосте я бы не шёл.
1 ГиБ = ~65 таких запросов, ~5× запаса над реалистичным пиком в 12, и место под медленно растущий
базовый расход pgstat. Абсолютный теоретический максимум им не покрыт (
max_connections=100× 15.4 МиБ≈ 1.5 ГиБ), но 100 одновременных тяжёлых аналитических запросов — это уже другая авария.
Проверка без прода (зелёный CI тут ничего не доказывает)
postgres:16, GUC как на проде (work_mem=4MB,max_parallel_workers_per_gather=2,dynamic_shared_memory_type=posix,shared_buffers=128MB), 600k строк шириной ~2 КБ.Между прогонами различается только
--shm-size. 6 одновременных запросов:64mcould not resize shared memory segment ... No space left on device128m256m1gПик при
1g(113 МиБ) — это реальный спрос: 64 МиБ его резали почти вдвое.План проверяется на параллельность в самом скрипте (
plan_gather_nodes), cost-GUC не трогаются.Что сейчас видит пользователь
Отказ ловится graceful-обёртками, но по-разному, и не везде видно:
special_indices§25 artificial_demand_unavailable(reason="ошибка расчёта (см. логи)")market_metrics._query_stockn_lots/n_sold/obj_count)n_lots > 0, confidence падает в low — это единственный намёкmarket_metrics._query_sales_windowrows = []→ продажи окна 0sales_seriesисточник Breturn {}То есть худший случай из названных в issue подтверждается: тихая пустота, а не видимая ошибка.
Срочность от этого выше, чем кажется по «6 ошибок в мониторинге».
Альтернатива без простоя (если окно не сейчас)
Контекст GUC —
user, рестарт не нужен. Убирает параллельные планы → DSM запросами не берётся вовсе.Цена (замерено на проде, прогоны чередованием, прогретый кэш — холодный первый прогон даёт
обратную картину и обманывает):
v_objective_lots_latest)Такие district-вызовы идут в отчёте не по одному (
market_metricsдёргается из 9 forecast/scoring-путей —это по комментариям в коде, не мой замер), так что порядок эффекта — единицы–десяток секунд на отчёт.
⚠️ Оговорка:
ALTER DATABASEдействует на новые соединения. Пул SQLAlchemy держит открытые —эффект наступит не мгновенно, а по мере их переоткрытия.
Выбор
shm_size: 1gb(этот PR)ALTER DATABASE ... = 0B обратима одной командой (
RESET) и годится как временная мера до окна. Это не взаимоисключающиеварианты: можно поставить B сегодня и снять её после того, как A уедет.
Откат A
Убрать строку и пересоздать контейнер (снова 64 МиБ). Том с данными не затрагивается.
Про trade-in
Дыра структурно та же и в
tradein-postgres:/dev/shmтоже 64 МБ (ShmSize=67108864), тот жеdynamic_shared_memory_type=posix, тот жеmax_parallel_workers_per_gather=2, и планировщик на егоданных реально выбирает Parallel Hash (проверил
EXPLAINнаlistings ⋈ listing_sources).Но по данным она не стреляет: за 9 минут живого трафика
/dev/shmне сдвинулся с 1.03 МиБ ни набайт, и в логах ноль таких ошибок. Оговорка честная: журнал глубиной всего 4 суток (journald для обоих
стеков появился 2026-08-06), так что «ноль» — это ноль за 4 дня, а не за всё время.
В этот PR я его не тащу: это отдельный контейнер, отдельный простой и отдельное решение, а весь
смысл этого PR — одна строка и одно решение.
deploy-tradein.ymlподнимает захардкоженный списоксервисов без
postgres, так что сам он никогда не пересоздаётся — понадобится такое же ручное окно.Готовая строка для будущего PR —
shm_size: 1gbуpostgresвtradein-mvp/docker-compose.prod.yml.Опровергнутое по ходу
/PostgreSQL.3812907982)попадает в заголовок, а он у GlitchTip участвует в фингерпринте → каждое событие завело свою группу,
у всех
count=1. Это один инцидент длиной 1.2 с (08:46:09.65–08:46:10.83) внутри одного разбораучастка
66:41:0702017:131. На срочность влияет в обе стороны: проблем меньше, но и «6 в сутки» какчастота — неверно, за 4 суток логов это единственный случай.
/dev/shm» подтверждён, но не тем доводом. Решает не то, что на диске свободно:temp_file_limit = -1(то есть лимита на temp-файлы нет вовсе, упереться в него нельзя), а сортировкив этих планах и правда спиллят на диск — и спокойно, по 5.7 МБ на воркера. Отсекает именно связка
dynamic_shared_memory_type=posix+ имя сегмента/PostgreSQL.<handle>(так POSIX-DSM именует своиобъекты в
/dev/shm) +ERRCODE_DISK_FULLотftruncate. Ни WAL, ниpg_tempтаких имён не дают.показал parallel=0 БЫСТРЕЕ (1515 мс против 3802 мс) — потому что первый прогон грел кэш для второго.
С чередованием картина перевернулась. Если бы я остановился на первом замере, отключение параллелизма
выглядело бы бесплатным улучшением.
вариант дал
plan_gather_nodes=0(плана-параллели нет) и 8 из 8 «успехов» — зелёный результат,который не проверял ничего. Второй дал параллельный план, но спрос 0.5 МиБ на запрос: 64 МиБ таким
не исчерпать. Рабочим стал третий. Санити-проверка на параллельность плана оставлена в скрипте
именно поэтому.
_query_artificial_demandдокументирует graceful-поведение, которого в нём нет. Docstring обещает«Сбой → {n_sold:0,n_mortgage:0} (НЕ crash)», но
try/exceptв функции отсутствует — ловит внешний_runвcompute_special_indices, и результат другой: не нули, а_unavailable. На поведение сейчасне влияет (обёртка есть), но docstring врёт про контракт.
Refs #2812
Параллельные планы Постгреса размещают DSM-сегменты в /dev/shm (dynamic_shared_memory_type=posix). Контейнеру БД никто не задавал shm_size, поэтому там держалось умолчание Docker — 64 МБ, и запросы Объектива падали с psycopg.errors.DiskFull «could not resize shared memory segment». Текст ошибки называет НЕ тот ресурс: на разделе 28 ГБ свободно, кончался именно /dev/shm. Замеры на проде 2026-08-10 (не рекомендация из интернета): постоянный расход ~9.8 МиБ (DSA кумулятивной статистики pgstat) один параллельный ~15.4 МиБ (3 одновременных → 55.9 МиБ из 64) → 4-й одновременный запрос не влезает; ровно это и наблюдалось (6 отказов за 1.2 с из двух backend-процессов) Нижняя граница: 128 МиБ покрывают лишь ~7-8 одновременных, а потолок celery (--concurrency=8) плюс request-path даёт ~12 → минимум 256 МиБ. Верхняя: /dev/shm это tmpfs, страницы выделяются по факту, поэтому значение — потолок, а не резерв; на хосте 11 ГиБ RAM (свободно ~6), 2 ГиБ я бы не переходил. 1 ГиБ = ~65 таких запросов, ~5x запаса. Локальная проверка A/B (postgres:16, GUC как на проде, различается только --shm-size): 6 одновременных параллельных запросов 64m → 4 отказа, пик 57.3 МиБ (упёрлись в лимит) 1g → 0 отказов, пик 113.3 МиБ (реальный спрос вдвое выше 64 МиБ) Refs #2812WIP: fix(db): /dev/shm боевого postgres — 64 МБ умолчания Docker → 1 ГиБ (#2812)to fix(db): /dev/shm боевого postgres — 64 МБ умолчания Docker → 1 ГБ