Postgres на стоковом конфиге: shared_buffers 128 МБ на базу 21 ГБ, wal_compression off при доле FPI 55-86% #2991

Closed
opened 2026-08-20 17:48:04 +00:00 by lekss361 · 2 comments
Owner

Эпик: #2989

Находка

Вся конфигурация Postgres — стоковая, замерено на проде 2026-08-20:

Параметр Сейчас Следствие
shared_buffers 128 МБ на базу 21 ГБ 169 млрд blks_read за 91,75 сут ≈ 175 МБ/с мимо кеша
max_wal_size 1 ГБ 288 чекпойнтов в сутки, full-page images 55–86 % всего WAL
wal_compression off при такой доле FPI это самая дешёвая победа
work_mem 4 МБ
maintenance_work_mem 64 МБ автовакуум по listings идёт 193 раза в сутки
random_page_cost 4 настройка под HDD, а диск NVMe

Отдельно: контейнеру tradein-postgres разрешено 3 ГБ (docker-compose.prod.yml:126), а потребляет он 292 МБ — память просто не роздана.

Дефицита RAM на машине нет: MemAvailable 6,1 ГБ (50 %), si/so = 0, Dirty 1,1 МБ. Занятый своп — исторический осадок за 95 дней аптайма, а не текущее давление.

Важно про порядок

Конфиг правится первым, до любых изменений схемы. Иначе эффект от конфига спишут на разделение таблиц и не поймут, что сработало.

И ловушка на новом сервере: mem_limit: 3g станет потолком раньше postgresql.conf — при shared_buffers = 16GB контейнер убьёт OOM-киллер. Поднять до 40–48 ГБ, не до 64: нужен запас на page cache вне контейнера.

Acceptance

  • max_wal_size и wal_compression = zstd — на текущем сервере, не дожидаясь переезда; замерить WAL до и после
  • shared_buffers поднят в рамках нынешнего mem_limit, замерена доля попаданий
  • pg_stat_statements установлен (расширение доступно в образе, но не подключено) — без него следующий разбор снова придётся вести по косвенным признакам
  • mem_limit в docker-compose.prod.yml приведён в соответствие с целевым shared_buffers
  • synchronous_commit = off через ALTER ROLE для скраперной роли, глобально остаётся on — в базе платежи

Полный целевой postgresql.conf под 64 ГБ и NVMe — в документе эпика.

Scope: docker-compose.prod.yml, конфиг Postgres.

Эпик: #2989 ## Находка Вся конфигурация Postgres — стоковая, замерено на проде 2026-08-20: | Параметр | Сейчас | Следствие | |---|---|---| | `shared_buffers` | **128 МБ** на базу 21 ГБ | 169 млрд `blks_read` за 91,75 сут ≈ 175 МБ/с мимо кеша | | `max_wal_size` | **1 ГБ** | 288 чекпойнтов в сутки, full-page images 55–86 % всего WAL | | `wal_compression` | **off** | при такой доле FPI это самая дешёвая победа | | `work_mem` | 4 МБ | | | `maintenance_work_mem` | 64 МБ | автовакуум по `listings` идёт 193 раза в сутки | | `random_page_cost` | 4 | настройка под HDD, а диск NVMe | Отдельно: контейнеру `tradein-postgres` разрешено 3 ГБ (`docker-compose.prod.yml:126`), а потребляет он 292 МБ — память просто не роздана. **Дефицита RAM на машине нет**: `MemAvailable` 6,1 ГБ (50 %), `si/so` = 0, `Dirty` 1,1 МБ. Занятый своп — исторический осадок за 95 дней аптайма, а не текущее давление. ## Важно про порядок Конфиг правится **первым**, до любых изменений схемы. Иначе эффект от конфига спишут на разделение таблиц и не поймут, что сработало. И ловушка на новом сервере: `mem_limit: 3g` станет потолком **раньше** `postgresql.conf` — при `shared_buffers = 16GB` контейнер убьёт OOM-киллер. Поднять до 40–48 ГБ, не до 64: нужен запас на page cache вне контейнера. ## Acceptance - [ ] `max_wal_size` и `wal_compression = zstd` — на текущем сервере, не дожидаясь переезда; замерить WAL до и после - [ ] `shared_buffers` поднят в рамках нынешнего `mem_limit`, замерена доля попаданий - [ ] `pg_stat_statements` установлен (расширение доступно в образе, но не подключено) — без него следующий разбор снова придётся вести по косвенным признакам - [ ] `mem_limit` в `docker-compose.prod.yml` приведён в соответствие с целевым `shared_buffers` - [ ] `synchronous_commit = off` через `ALTER ROLE` для скраперной роли, глобально остаётся `on` — в базе платежи Полный целевой `postgresql.conf` под 64 ГБ и NVMe — в документе эпика. Scope: `docker-compose.prod.yml`, конфиг Postgres.
lekss361 added the
chore
performance
priority/p1
scope/db
scope/devops
tradein
labels 2026-08-20 17:50:52 +00:00
Collaborator

Конфиг живёт на проде с 20.08 21:02 UTC — первый замер «после» (21.08), с оговорками

pg_settings на проде: shared_buffers=983048kB (≈960 МБ), max_wal_size=4GB, wal_compression=zstd, work_mem=16MB, maintenance_work_mem=256MB, random_page_cost=1.1, effective_cache_size≈7.5GB, checkpoint_completion_target=0.9, shared_preload_libraries=pg_stat_statements + расширение создано (уже пригодилось: по нему найден geometry-потребитель в #2997). mem_limit контейнера 3 ГБ — shared_buffers в него укладывается с запасом.

Что измерено (окно t0=10:10:56Z → t1=11:36:19Z, 85 мин, дневная нагрузка)

показатель до (из постановки, 92 сут) после
чекпойнты 288/сутки +3 за 85 мин ≈ 51/сутки; с рестарта 34 за 14.6 ч ≈ 56/сутки (checkpoint_timeout=30min)
WAL ~7 ГБ/сутки (#2992) +26 МиБ за 85 мин (0.43 ГиБ/сутки днём); с рестарта 479 МиБ за 14.6 ч ≈ 0.79 ГиБ/сутки, включая ночь сбора
FPI 55–86 % байт WAL 150 012 из 1 852 093 записей = 8.1 % по числу записей (по байтам pg_stat_wal не даёт)
blks_read мимо кеша ≈175 МБ/с в среднем за 92 сут 172 МиБ за 85 мин = 0.03 МБ/с
hit ratio 96.35 % накопительно за 92 сут 96.5 % в окне — не отличается

Оговорки, без которых таблица врёт:

  1. Падение WAL — смесь двух правок: wal_compression=zstd/реже чекпойнты (с 20.08 21:02) и апсерт-гейт #2992 (с 21.08 ~08:40, перестал переписывать неизменённые строки). Разделить по времени нельзя: ночь сбора (главный источник записи) пришлась на «конфиг без гейта», день — на «конфиг + гейт» с малой записью. Честное разделение — по замеру #2992 22.08 ~09:00 UTC (Δ n_tup_upd и Δ LSN за полные сутки с гейтом).
  2. hit ratio и blks_read за 85 дневных минут ни о чём: нагрузка несопоставима со среднесуточной. Сутки-окно: t2 = 22.08 ~11:36 UTC, тогда же checkpoints/WAL за полные сутки — допишу.
  3. Побочное: listings без автовакуума с 19.08 05:11 при n_dead_tup=21 177 / n_live=107 048 — порог (≈21.5k) просто не достигнут, потому что апдейтов с 08:37 всего +2 325 (это эффект гейта #2992, а не конфига; до гейта автовакуум шёл 193 раза в сутки).

Acceptance — где стоим

  • max_wal_size / wal_compression=zstd на текущем сервере — применено; WAL до/после — выше, с оговоркой 1
  • shared_buffers поднят в рамках mem_limit; доля попаданий — в окне 96.5 % (не изменилась), суточный замер 22.08
  • pg_stat_statements подключён и работает
  • mem_limit согласован с shared_buffers (960 МБ при 3 ГБ)
  • synchronous_commit=off через ALTER ROLE для скраперной роли — невозможно без отдельной роли: скрапер и бэкенд ходят одной ролью tradein (pg_roles: только tradein и gendesign_reader, у обеих rolconfig пуст); ALTER ROLE tradein задел бы и платежи. Нужна отдельная роль для tradein-scraper — это решение по схеме доступа, не правка конфига.
## Конфиг живёт на проде с 20.08 21:02 UTC — первый замер «после» (21.08), с оговорками `pg_settings` на проде: `shared_buffers=983048kB` (≈960 МБ), `max_wal_size=4GB`, `wal_compression=zstd`, `work_mem=16MB`, `maintenance_work_mem=256MB`, `random_page_cost=1.1`, `effective_cache_size≈7.5GB`, `checkpoint_completion_target=0.9`, `shared_preload_libraries=pg_stat_statements` + расширение создано (уже пригодилось: по нему найден geometry-потребитель в #2997). `mem_limit` контейнера 3 ГБ — shared_buffers в него укладывается с запасом. ### Что измерено (окно t0=10:10:56Z → t1=11:36:19Z, 85 мин, дневная нагрузка) | показатель | до (из постановки, 92 сут) | после | |---|---|---| | чекпойнты | 288/сутки | +3 за 85 мин ≈ **51/сутки**; с рестарта 34 за 14.6 ч ≈ 56/сутки (checkpoint_timeout=30min) | | WAL | ~7 ГБ/сутки (#2992) | +26 МиБ за 85 мин (0.43 ГиБ/сутки днём); **с рестарта 479 МиБ за 14.6 ч ≈ 0.79 ГиБ/сутки, включая ночь сбора** | | FPI | 55–86 % байт WAL | 150 012 из 1 852 093 записей = 8.1 % по числу записей (по байтам pg_stat_wal не даёт) | | blks_read мимо кеша | ≈175 МБ/с в среднем за 92 сут | 172 МиБ за 85 мин = 0.03 МБ/с | | hit ratio | 96.35 % накопительно за 92 сут | **96.5 % в окне** — не отличается | **Оговорки, без которых таблица врёт:** 1. Падение WAL — **смесь двух правок**: `wal_compression=zstd`/реже чекпойнты (с 20.08 21:02) и апсерт-гейт #2992 (с 21.08 ~08:40, перестал переписывать неизменённые строки). Разделить по времени нельзя: ночь сбора (главный источник записи) пришлась на «конфиг без гейта», день — на «конфиг + гейт» с малой записью. Честное разделение — по замеру #2992 22.08 ~09:00 UTC (Δ n_tup_upd и Δ LSN за полные сутки с гейтом). 2. hit ratio и blks_read за 85 дневных минут ни о чём: нагрузка несопоставима со среднесуточной. **Сутки-окно: t2 = 22.08 ~11:36 UTC**, тогда же checkpoints/WAL за полные сутки — допишу. 3. Побочное: `listings` без автовакуума с 19.08 05:11 при `n_dead_tup=21 177 / n_live=107 048` — порог (≈21.5k) просто не достигнут, потому что апдейтов с 08:37 всего +2 325 (это эффект гейта #2992, а не конфига; до гейта автовакуум шёл 193 раза в сутки). ### Acceptance — где стоим - [x] `max_wal_size` / `wal_compression=zstd` на текущем сервере — применено; WAL до/после — выше, с оговоркой 1 - [x] `shared_buffers` поднят в рамках `mem_limit`; доля попаданий — в окне 96.5 % (не изменилась), суточный замер 22.08 - [x] `pg_stat_statements` подключён и работает - [x] `mem_limit` согласован с `shared_buffers` (960 МБ при 3 ГБ) - [ ] `synchronous_commit=off` через `ALTER ROLE` для скраперной роли — **невозможно без отдельной роли**: скрапер и бэкенд ходят одной ролью `tradein` (`pg_roles`: только `tradein` и `gendesign_reader`, у обеих `rolconfig` пуст); `ALTER ROLE tradein` задел бы и платежи. Нужна отдельная роль для `tradein-scraper` — это решение по схеме доступа, не правка конфига.
Author
Owner

Закрыто — конфиг влит (PR #3012) и подтверждён в рантайме

Проверял не наличие строк в файле, а фактические настройки живого tradein-postgres (это принципиально: PR может быть смержен, деплой пройти, а контейнер продолжать работать со старта — так и случилось 20.08, эффект появился только когда деплой дошёл до up -d --no-deps postgres).

SELECT name, setting, unit, source FROM pg_settings на проде сейчас:

Параметр Значение source
shared_buffers 98304 × 8kB = 768 MB (было 128 MB) command line
effective_cache_size 786432 × 8kB = 6 GB command line
work_mem 16 MB command line
maintenance_work_mem 256 MB command line
max_wal_size 4096 MB command line
wal_compression zstd command line
shared_preload_libraries pg_stat_statements command line

Все семь имеют source = command line, то есть приходят из command: в tradein-mvp/docker-compose.prod.yml, а не из postgresql.auto.conf. Это важно для будущих правок: ALTER SYSTEM пишет в auto.conf (source configuration file) и проигрывает командной строке — задав одно и то же обоими способами, получишь противоречащий мусор в auto.conf при работающей настройке из compose.

Значения подобраны под текущий сервер и не выходят за mem_limit=3g. В compose оставлен явный комментарий на переезд (#2989): mem_limit поднимать до правки shared_buffers, иначе лимит контейнера сработает раньше postgresql.conf и shared_buffers=16GB при mem_limit=3g даст OOM-kill на старте. Целевое на 64 ГБ — 40-48g контейнеру, не 64g: нужен запас под page cache вне контейнера.

Что осталось не сделанным сознательно и в этот issue не входит: пересчёт значений под 64 ГБ (делается в окне переезда) и synchronous_commit=off для скраперной роли.

Refs #2989

## Закрыто — конфиг влит (PR #3012) и **подтверждён в рантайме** Проверял не наличие строк в файле, а фактические настройки живого `tradein-postgres` (это принципиально: PR может быть смержен, деплой пройти, а контейнер продолжать работать со старта — так и случилось 20.08, эффект появился только когда деплой дошёл до `up -d --no-deps postgres`). `SELECT name, setting, unit, source FROM pg_settings` на проде сейчас: | Параметр | Значение | source | |---|---|---| | `shared_buffers` | 98304 × 8kB = **768 MB** (было 128 MB) | `command line` | | `effective_cache_size` | 786432 × 8kB = 6 GB | `command line` | | `work_mem` | 16 MB | `command line` | | `maintenance_work_mem` | 256 MB | `command line` | | `max_wal_size` | 4096 MB | `command line` | | `wal_compression` | **zstd** | `command line` | | `shared_preload_libraries` | **pg_stat_statements** | `command line` | Все семь имеют `source = command line`, то есть приходят из `command:` в `tradein-mvp/docker-compose.prod.yml`, а не из `postgresql.auto.conf`. Это важно для будущих правок: `ALTER SYSTEM` пишет в `auto.conf` (source `configuration file`) и **проигрывает** командной строке — задав одно и то же обоими способами, получишь противоречащий мусор в `auto.conf` при работающей настройке из compose. **Значения подобраны под текущий сервер и не выходят за `mem_limit=3g`.** В compose оставлен явный комментарий на переезд (#2989): `mem_limit` поднимать **до** правки `shared_buffers`, иначе лимит контейнера сработает раньше `postgresql.conf` и `shared_buffers=16GB` при `mem_limit=3g` даст OOM-kill на старте. Целевое на 64 ГБ — 40-48g контейнеру, не 64g: нужен запас под page cache вне контейнера. Что осталось не сделанным сознательно и в этот issue не входит: пересчёт значений под 64 ГБ (делается в окне переезда) и `synchronous_commit=off` для скраперной роли. Refs #2989
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#2991
No description provided.