[HIGH] ptica/db: параллельные запросы Объектива падают на /dev/shm = 64 МБ (умолчание Docker) — 6 отказов за сутки #2812

Closed
opened 2026-08-10 09:19:27 +00:00 by bot-backend · 3 comments
Collaborator

Найдено сегодня при проверке накопленных ошибок в мониторинге (#2673). Живой отказ на проде, не гипотеза.

Симптом

За сутки 10.08 — 6 групп ошибок одного вида, все в 08:46:

OperationalError: (psycopg.errors.DiskFull)
could not resize shared memory segment "/PostgreSQL.3812907982"
to 6311936 bytes: No space left on device

Запрос — тот самый, что считает актуальные лоты Объектива:

WITH latest AS (
    SELECT DISTINCT ON (project_name, corpus_name, section, floor, lot_number)
        objective_lot_id, is_sold, contract_date, ...

Причина: /dev/shm у контейнера — 64 МБ, умолчание Docker

хост                        tmpfs   5.9G   свободно 5.9G
gendesign-postgres-1        shm      64M   ← умолчание Docker

shm_size не задан ни в одном compose-файле репозитория — проверено grep. Postgres размещает в разделяемой памяти рабочие сегменты параллельных воркеров; запрос попросил 6.3 МБ, и на 64 мегабайтах при нескольких параллельных ветках это упирается.

Диск тут ни при чём, несмотря на текст ошибки. На разделе 28 ГБ свободно (82% занято). «No space left on device» относится к /dev/shm, а не к /. Я сам сначала прочитал это как «диск кончился» — и это стоит отметить: сообщение называет не тот ресурс, который кончился.

Почему это не мелочь

  1. Отказ тихий для пользователя: запрос падает, а что показывает экран вместо чисел Объектива — надо проверить отдельно (может быть пусто, может быть старый кэш).
  2. Он перемежающийся: зависит от того, сколько параллельных воркеров планировщик выберет. Значит воспроизводится не всегда и легко списывается на случайность.
  3. Он накапливался незамеченным ровно потому, что в мониторинге нет получателя (#2673). Шесть отказов за сутки лежали в ящике, который никто не открывал.

Что предлагается

Одна строка в docker-compose.prod.yml у сервиса postgres:

shm_size: 1gb

Обосновать значение, а не взять наугад: посмотреть max_parallel_workers_per_gather, work_mem и типичный размер сегмента (здесь 6.3 МБ). 1 ГБ — обычная рекомендация для Postgres в Docker, но проверить по своим числам.

Требует пересоздания контейнера БД — то есть короткого простоя, и это решение владельца, а не побочный эффект деплоя.

Альтернатива без перезапуска: снизить параллелизм (max_parallel_workers_per_gather = 0 для этого запроса или глобально) — но это лечит симптом ценой скорости, а не причину.

Проверить отдельно

  • Что видит пользователь, когда этот запрос падает — пустой блок, ошибку или устаревшие данные.
  • Не тот же ли корень у других перемежающихся отказов Объектива.

Связано: #2673 (нет получателя — поэтому копилось незамеченным), #2761 (диск и журналы — сюда же смотрели, но /dev/shm не проверяли).

Найдено сегодня при проверке накопленных ошибок в мониторинге (#2673). **Живой отказ на проде**, не гипотеза. ## Симптом За сутки 10.08 — **6 групп ошибок** одного вида, все в 08:46: ``` OperationalError: (psycopg.errors.DiskFull) could not resize shared memory segment "/PostgreSQL.3812907982" to 6311936 bytes: No space left on device ``` Запрос — тот самый, что считает актуальные лоты Объектива: ```sql WITH latest AS ( SELECT DISTINCT ON (project_name, corpus_name, section, floor, lot_number) objective_lot_id, is_sold, contract_date, ... ``` ## Причина: `/dev/shm` у контейнера — 64 МБ, умолчание Docker ``` хост tmpfs 5.9G свободно 5.9G gendesign-postgres-1 shm 64M ← умолчание Docker ``` `shm_size` **не задан ни в одном compose-файле** репозитория — проверено `grep`. Postgres размещает в разделяемой памяти рабочие сегменты параллельных воркеров; запрос попросил 6.3 МБ, и на 64 мегабайтах при нескольких параллельных ветках это упирается. **Диск тут ни при чём, несмотря на текст ошибки.** На разделе 28 ГБ свободно (82% занято). «No space left on device» относится к `/dev/shm`, а не к `/`. Я сам сначала прочитал это как «диск кончился» — и это стоит отметить: сообщение называет не тот ресурс, который кончился. ## Почему это не мелочь 1. Отказ **тихий для пользователя**: запрос падает, а что показывает экран вместо чисел Объектива — надо проверить отдельно (может быть пусто, может быть старый кэш). 2. Он **перемежающийся**: зависит от того, сколько параллельных воркеров планировщик выберет. Значит воспроизводится не всегда и легко списывается на случайность. 3. Он **накапливался незамеченным** ровно потому, что в мониторинге нет получателя (#2673). Шесть отказов за сутки лежали в ящике, который никто не открывал. ## Что предлагается Одна строка в `docker-compose.prod.yml` у сервиса `postgres`: ```yaml shm_size: 1gb ``` Обосновать значение, а не взять наугад: посмотреть `max_parallel_workers_per_gather`, `work_mem` и типичный размер сегмента (здесь 6.3 МБ). 1 ГБ — обычная рекомендация для Postgres в Docker, но проверить по своим числам. **Требует пересоздания контейнера БД** — то есть короткого простоя, и это решение владельца, а не побочный эффект деплоя. Альтернатива без перезапуска: снизить параллелизм (`max_parallel_workers_per_gather = 0` для этого запроса или глобально) — но это лечит симптом ценой скорости, а не причину. ## Проверить отдельно - Что видит пользователь, когда этот запрос падает — пустой блок, ошибку или устаревшие данные. - Не тот же ли корень у других перемежающихся отказов Объектива. Связано: #2673 (нет получателя — поэтому копилось незамеченным), #2761 (диск и журналы — сюда же смотрели, но `/dev/shm` не проверяли).
Author
Collaborator

PR #2819 готов, CI зелёный. Намеренно оставлен под замком (WIP) — мержить владельцу.

Почему не мержится автоматически: docker-compose.prod.yml триггерит deploy.yml, а shm_size — свойство создания контейнера, поэтому первый же деплой после merge пересоздаст контейнер боевой БД (~10 с, все соединения рвутся; данные в томе целы). Окно и цена — в описании PR.

Коротко по проверкам из issue:

  • диагноз /dev/shm подтверждён (не temp-файлы: temp_file_limit = -1; отсекает связка dynamic_shared_memory_type=posix + имя /PostgreSQL.<handle> + ERRCODE_DISK_FULL от ftruncate);
  • значение обосновано замером: постоянный расход 9.75 МиБ + ~15.4 МиБ на параллельный запрос → 4-й не влезает в 64 МиБ; нижняя граница 256 МиБ, выше 2 ГиБ на этом хосте нельзя (swap уже 3/3);
  • отказ воспроизведён локально (64m → 4 из 6 падают) и исчезает на 1g при прочих равных;
  • у tradein-postgres дыра структурно та же, но не стреляет — в этот PR не тащу, обоснование в описании;
  • «6 групп» оказались одним инцидентом на 1.2 с: имя сегмента попадает в фингерпринт GlitchTip.
PR #2819 готов, CI зелёный. **Намеренно оставлен под замком (WIP) — мержить владельцу.** Почему не мержится автоматически: `docker-compose.prod.yml` триггерит `deploy.yml`, а `shm_size` — свойство создания контейнера, поэтому первый же деплой после merge пересоздаст контейнер боевой БД (~10 с, все соединения рвутся; данные в томе целы). Окно и цена — в описании PR. Коротко по проверкам из issue: - диагноз `/dev/shm` подтверждён (не temp-файлы: `temp_file_limit = -1`; отсекает связка `dynamic_shared_memory_type=posix` + имя `/PostgreSQL.<handle>` + ERRCODE_DISK_FULL от ftruncate); - значение обосновано замером: постоянный расход 9.75 МиБ + ~15.4 МиБ на параллельный запрос → 4-й не влезает в 64 МиБ; нижняя граница 256 МиБ, выше 2 ГиБ на этом хосте нельзя (swap уже 3/3); - отказ воспроизведён локально (64m → 4 из 6 падают) и исчезает на 1g при прочих равных; - у `tradein-postgres` дыра структурно та же, но не стреляет — в этот PR не тащу, обоснование в описании; - «6 групп» оказались одним инцидентом на 1.2 с: имя сегмента попадает в фингерпринт GlitchTip.
Author
Collaborator

Поправка к моей же шапке: «6 отказов за сутки» — это ОДИН инцидент длиной 1.2 секунды

Я написал «6 групп ошибок за сутки», и это читается как частота. Неверно. Проверил:

08:46:09.658   1 событие   сегмент PostgreSQL.294
08:46:09.771   1 событие   сегмент PostgreSQL.902
08:46:09.931   1 событие   сегмент PostgreSQL.622
08:46:10.245   1 событие   сегмент PostgreSQL.260
08:46:10.329   1 событие   сегмент PostgreSQL.606
08:46:10.827   1 событие   сегмент PostgreSQL.381

Все шесть — внутри 1.2 секунды, у каждой count = 1, все просили ровно 6 311 936 байт, все внутри одного разбора участка. Групп шесть потому, что имя сегмента попадает в заголовок, а заголовок участвует в отпечатке — шесть разных имён дали шесть «разных» ошибок.

За все 4 суток, что есть журналы, это единственный случай. Срочность ниже, чем следовало из моей формулировки.

Но диагноз подтверждён — и не тем доводом, которым я его обосновал

Мой довод «на диске 28 ГБ свободно, значит дело не в диске» ничего не отсекает. Сортировки в этих планах реально спиллят на диск (external merge Disk: 5800kB на воркера), так что «что-то про диск» в тексте — не пустой звук.

Отсекает связка трёх фактов: dynamic_shared_memory_type = posix + имя вида /PostgreSQL.<handle> (так POSIX-DSM именует объекты в /dev/shm, ни WAL, ни временные файлы таких имён не дают) + код ошибки приходит от расширения сегмента. temp_file_limit при этом -1 — упереться в него нельзя в принципе.

Что видит пользователь — хуже, чем я предполагал

место поведение видно ли
§25 «искусственный спрос» «ошибка расчёта» да
счётчики предложения 0 нет — ноль неотличим от «в районе правда нет лотов»
окно продаж пустой список нет
ряд продаж, источник B return {} нет вообще — ряд молча строится по одному источнику из двух

Три из четырёх — тихая пустота.

Два варианта с ценой, решение за вами

A. shm_size: 1gb — PR #2819, открыт, CI зелёный. Простой ~10 секунд (пересоздание контейнера БД), скорость не меняется, чинит причину. Данные целы — том переживает.

B. Отключить параллелизм для этой базы — простоя нет, чинит симптом. Цена замерена чередованием на прогретом кэше: районная форма запроса ×2.2 (622 → 1390 мс, +0.77 с на вызов), полнотабличная ×1.05–1.15.

Не взаимоисключающие: B годится как временная мера до окна.

Значение 1 ГиБ обосновано с обеих сторон. Снизу: постоянный расход 9.75 МиБ + один запрос ~15.4 МиБ → четвёртый одновременный упирается в 64 МиБ (арифметика сходится с инцидентом — в журнале упёрлись два процесса). Сверху: на хосте 11 ГиБ памяти, свободно ~6, и подкачка занята целиком (3 из 3) — вытеснять некуда, рост пойдёт прямо из памяти.

Важно про момент применения: shm_size — свойство создания контейнера, а шаг деплоя захватывает postgres. Значит первый же деплой после мержа пересоздаст БД, когда бы он ни случился. Момент выбирается моментом мержа. Наименее плохое окно по расписанию задач — 01:00–02:30 МСК либо 21:00–23:00.

Та же дыра у trade-in — есть структурно, не стреляет по данным

ShmSize тот же, тип памяти тот же, планировщик на его данных реально выбирает параллельный план. По симметрии я бы её закрыл — но за 9 минут живого трафика /dev/shm не сдвинулся ни на байт, ошибок ноль. Оговорка честная: журналу 4 суток, так что это «ноль за 4 дня», а не за всё время. В PR не тащится — отдельный контейнер, отдельный простой, отдельное решение.

Отказ воспроизведён локально

postgres:16, настройки как на проде, различается только размер разделяемой памяти, 6 одновременных запросов: при 64 МиБ — 4 из 6 падают с той же ошибкой; при 1 ГиБ — ноль отказов, пик 113 МиБ. То есть 64 МиБ резали реальный спрос почти вдвое.

Отдельно стоит знать: локальное воспроизведение дважды «проходило», ничего не проверяя — сначала план оказался непараллельным (8 успехов из 8), потом параллельным, но с ничтожным спросом. Проверка на параллельность плана осталась в скрипте именно поэтому.

## Поправка к моей же шапке: «6 отказов за сутки» — это ОДИН инцидент длиной 1.2 секунды Я написал «6 групп ошибок за сутки», и это читается как частота. Неверно. Проверил: ``` 08:46:09.658 1 событие сегмент PostgreSQL.294 08:46:09.771 1 событие сегмент PostgreSQL.902 08:46:09.931 1 событие сегмент PostgreSQL.622 08:46:10.245 1 событие сегмент PostgreSQL.260 08:46:10.329 1 событие сегмент PostgreSQL.606 08:46:10.827 1 событие сегмент PostgreSQL.381 ``` Все шесть — внутри **1.2 секунды**, у каждой `count = 1`, все просили ровно 6 311 936 байт, все внутри одного разбора участка. Групп шесть потому, что **имя сегмента попадает в заголовок**, а заголовок участвует в отпечатке — шесть разных имён дали шесть «разных» ошибок. За все 4 суток, что есть журналы, это **единственный случай**. Срочность ниже, чем следовало из моей формулировки. ## Но диагноз подтверждён — и не тем доводом, которым я его обосновал Мой довод «на диске 28 ГБ свободно, значит дело не в диске» **ничего не отсекает**. Сортировки в этих планах реально спиллят на диск (`external merge Disk: 5800kB` на воркера), так что «что-то про диск» в тексте — не пустой звук. Отсекает связка трёх фактов: `dynamic_shared_memory_type = posix` + имя вида `/PostgreSQL.<handle>` (так POSIX-DSM именует объекты в `/dev/shm`, ни WAL, ни временные файлы таких имён не дают) + код ошибки приходит от расширения сегмента. `temp_file_limit` при этом `-1` — упереться в него нельзя в принципе. ## Что видит пользователь — хуже, чем я предполагал | место | поведение | видно ли | |---|---|---| | §25 «искусственный спрос» | «ошибка расчёта» | **да** | | счётчики предложения | → **0** | **нет** — ноль неотличим от «в районе правда нет лотов» | | окно продаж | пустой список | **нет** | | ряд продаж, источник B | `return {}` | **нет вообще** — ряд молча строится по одному источнику из двух | Три из четырёх — тихая пустота. ## Два варианта с ценой, решение за вами **A. `shm_size: 1gb`** — PR #2819, открыт, CI зелёный. Простой **~10 секунд** (пересоздание контейнера БД), скорость не меняется, чинит причину. Данные целы — том переживает. **B. Отключить параллелизм** для этой базы — простоя нет, чинит симптом. Цена замерена чередованием на прогретом кэше: районная форма запроса **×2.2** (622 → 1390 мс, +0.77 с на вызов), полнотабличная ×1.05–1.15. Не взаимоисключающие: B годится как временная мера до окна. **Значение 1 ГиБ обосновано с обеих сторон.** Снизу: постоянный расход 9.75 МиБ + один запрос ~15.4 МиБ → четвёртый одновременный упирается в 64 МиБ (арифметика сходится с инцидентом — в журнале упёрлись два процесса). Сверху: на хосте 11 ГиБ памяти, свободно ~6, **и подкачка занята целиком (3 из 3)** — вытеснять некуда, рост пойдёт прямо из памяти. **Важно про момент применения:** `shm_size` — свойство создания контейнера, а шаг деплоя захватывает `postgres`. Значит **первый же деплой после мержа пересоздаст БД**, когда бы он ни случился. Момент выбирается моментом мержа. Наименее плохое окно по расписанию задач — 01:00–02:30 МСК либо 21:00–23:00. ## Та же дыра у trade-in — есть структурно, не стреляет по данным `ShmSize` тот же, тип памяти тот же, планировщик на его данных **реально выбирает параллельный план**. По симметрии я бы её закрыл — но за 9 минут живого трафика `/dev/shm` не сдвинулся ни на байт, ошибок ноль. Оговорка честная: журналу 4 суток, так что это «ноль за 4 дня», а не за всё время. В PR не тащится — отдельный контейнер, отдельный простой, отдельное решение. ## Отказ воспроизведён локально `postgres:16`, настройки как на проде, различается **только** размер разделяемой памяти, 6 одновременных запросов: при 64 МиБ — **4 из 6 падают** с той же ошибкой; при 1 ГиБ — ноль отказов, пик 113 МиБ. То есть 64 МиБ резали реальный спрос почти вдвое. Отдельно стоит знать: локальное воспроизведение **дважды «проходило», ничего не проверяя** — сначала план оказался непараллельным (8 успехов из 8), потом параллельным, но с ничтожным спросом. Проверка на параллельность плана осталась в скрипте именно поэтому.
lekss361 added the
bug
scope/db
scope/devops
site-finder
labels 2026-08-16 10:25:21 +00:00
Owner

Закрываю по итогам разбора трекера 16.08.2026

Вердикт: сделано кодом.

Ровно этот дефект закрыт #2819; ошибок DiskFull на shared memory после подъёма shm_size быть не должно.

Доказательство: PR #2819 — shm_size боевого postgres 64МБ→1ГБ, смержен и проверен на проде 15-16.08

Независимая проверка. Вердикт отдельно проверялся вторым проходом, задачей которого было именно опровергнуть закрытие, а не подтвердить его:

Проверил на проде напрямую: docker inspect gendesign-postgres-1 → ShmSize=1073741824, внутри контейнера df -h /dev/shm → 1.0G (использовано 3.3M). Коммит 6ab359a1 в forgejo/main, docker-compose.prod.yml содержит shm_size: 1gb у боевого postgres; контейнер пересоздан (Up 15 часов, т.е. после мержа #2819 15-16.08). Рецидивов нет: в текущем списке issues GlitchTip ни одного DiskFull / 'could not resize shared memory segment'. Единственная оговорка — tradein-postgres по-прежнему 64 МБ (/dev/shm 64M) — но это явно и обоснованно вынесено за рамки задачи самим автором в комментарии ('отдельный контейнер, отдельный простой, отдельное решение'; за 9 минут живого трафика /dev/shm не сдвинулся, ошибок ноль), так что закрытие #2812 этим не блокируется. Побочный вопрос 'что видит пользователь при отказе' в задаче отвечен в комментарии (три из четырёх мест — тихая пустота); если это надо чинить, это отдельный предмет, а не невыполненный пункт #2812.

Если что-то из перечисленного всё же живо — переоткройте задачу, разбор мог упустить частный случай.

## Закрываю по итогам разбора трекера 16.08.2026 **Вердикт:** сделано кодом. Ровно этот дефект закрыт #2819; ошибок DiskFull на shared memory после подъёма shm_size быть не должно. **Доказательство:** PR #2819 — shm_size боевого postgres 64МБ→1ГБ, смержен и проверен на проде 15-16.08 **Независимая проверка.** Вердикт отдельно проверялся вторым проходом, задачей которого было именно опровергнуть закрытие, а не подтвердить его: > Проверил на проде напрямую: docker inspect gendesign-postgres-1 → ShmSize=1073741824, внутри контейнера df -h /dev/shm → 1.0G (использовано 3.3M). Коммит 6ab359a1 в forgejo/main, docker-compose.prod.yml содержит shm_size: 1gb у боевого postgres; контейнер пересоздан (Up 15 часов, т.е. после мержа #2819 15-16.08). Рецидивов нет: в текущем списке issues GlitchTip ни одного DiskFull / 'could not resize shared memory segment'. Единственная оговорка — tradein-postgres по-прежнему 64 МБ (/dev/shm 64M) — но это явно и обоснованно вынесено за рамки задачи самим автором в комментарии ('отдельный контейнер, отдельный простой, отдельное решение'; за 9 минут живого трафика /dev/shm не сдвинулся, ошибок ноль), так что закрытие #2812 этим не блокируется. Побочный вопрос 'что видит пользователь при отказе' в задаче отвечен в комментарии (три из четырёх мест — тихая пустота); если это надо чинить, это отдельный предмет, а не невыполненный пункт #2812. Если что-то из перечисленного всё же живо — переоткройте задачу, разбор мог упустить частный случай.
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#2812
No description provided.