perf(bookings): BOOKINGS_JOBS=1 по умолчанию (3× быстрее на инкрементах)

- Зачем:
  - бенчмарк на i7-11800H / WSL2 показал, что jobs=1 генерирует +1 день за ~3 мин,
    а jobs=2 — за ~9 мин из-за lock contention на gen.events FOR UPDATE SKIP LOCKED.
    Тюнинг PG (shared_buffers, work_mem) и synchronous_commit=off не влияют.
- Что:
  - дефолт BOOKINGS_JOBS изменён с 2 на 1 в Makefile и .env.example.
  - добавлен docs/internal/bookings_generation_benchmark.md с полной матрицей экспериментов.
  - обновлены комментарии и bookings/README.md.
- Проверка:
  - make bookings-init && time make bookings-generate-day (~3 мин).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-09 18:06:56 +03:00
co-authored by Claude Opus 4.6
parent 85d2776b44
commit c81bdb86d9
4 changed files with 81 additions and 4 deletions
@@ -0,0 +1,75 @@
# Бенчмарк генерации bookings: параллельность и тюнинг PostgreSQL
> Дата: 2026-03-09
> Железо: Intel Core i7-11800H @ 2.30GHz (8 ядер / 16 потоков), игровой ноутбук
> Среда: WSL2, Docker Desktop, PostgreSQL 16 в контейнере
> Данные: seed-дамп 60 дней (~500k bookings, ~180k boarding_passes, ~1.2M tickets)
## Контекст
Генератор demodb (`postgrespro/demodb`) использует очередь событий `gen.events` с обработкой через `process_queue()`. При `jobs>1` воркеры запускаются через `dblink_send_query` и конкурируют за очередь через `SELECT ... FOR UPDATE SKIP LOCKED`.
Задача: найти оптимальную конфигурацию для `make bookings-generate-day` (инкремент +1 день).
## Методика
- Между экспериментами: `make bookings-init BOOKINGS_JOBS=N` (восстановление из seed-дампа, ~18 сек).
- Замер: `time make bookings-generate-day BOOKINGS_JOBS=N`.
- Контроль: `max(book_date)` должен сдвинуться на +1 день.
- Тюнинг PG: `ALTER SYSTEM SET` + `docker compose restart bookings-db`.
Параметры тюнинга PostgreSQL:
```
shared_buffers = 512MB (дефолт: 128MB)
work_mem = 64MB (дефолт: 4MB)
maintenance_work_mem = 256MB (дефолт: 64MB)
effective_cache_size = 1GB (дефолт: 4GB)
wal_buffers = 16MB (дефолт: -1, авто)
checkpoint_completion_target = 0.9 (дефолт: 0.9)
random_page_cost = 1.1 (дефолт: 4.0)
```
## Результаты
| # | Тюнинг PG | sync_commit | Jobs | Время | vs baseline |
|---|-----------|-------------|------|--------|--------------|
| 1 | нет | on | 1 | 2m 48s | **baseline** |
| 2 | нет | on | 2 | 8m 36s | 3× хуже |
| 3 | да | on | 1 | 3m 01s | шум |
| 4 | да | on | 2 | 8m 42s | 3× хуже |
| 5 | да | off | 1 | 2m 50s | шум |
| 6 | да | off | 2 | 8m 39s | 3× хуже |
Повторный инкремент (прогретый кэш): 5m 03s (jobs=2), не замерялся (jobs=1).
## Выводы
### 1. jobs=1 оптимален для инкрементов
Для генерации +1 дня параллельность через dblink **контрпродуктивна**:
- Два воркера конкурируют за одну очередь `gen.events` через `FOR UPDATE SKIP LOCKED` — это row-level lock contention.
- Overhead: dblink-соединения, синхронизация через `gen.stat_jobs`, polling через `dblink_is_busy()`.
- На WSL2 дополнительно: виртуализированный I/O не масштабируется при параллельных записях.
При генерации с нуля (`make bookings-generate`, десятки тысяч событий) jobs>1 **может** давать прирост, но не тестировалось в этом бенчмарке.
### 2. Тюнинг PostgreSQL не влияет
Увеличение shared_buffers в 4 раза, work_mem в 16 раз и т.д. не дало измеримого эффекта. Узкое место — не буферы и не I/O, а сама логика генератора: последовательная обработка событий с COMMIT после каждого.
### 3. synchronous_commit=off не влияет
Генератор делает COMMIT после каждого события (~сотни раз за день). Ожидалось, что `synchronous_commit=off` ускорит запись WAL. Эффект не обнаружен — вероятно, WAL-буферы и так справляются при одном потоке.
### 4. VACUUM-ивенты — отдельная проблема
Генератор вставляет в очередь событие `VACUUM` каждую неделю модельного времени. Обработчик запускает `VACUUM ANALYZE` всей базы через `dblink_exec`. При 500k+ строках это занимает минуты. Решение: `DELETE FROM gen.events WHERE type = 'VACUUM'` перед `continue()` в инкрементальных скриптах.
## Итоговая конфигурация
```
BOOKINGS_JOBS=1 # синхронно, без dblink — ~3 мин на +1 день
BOOKINGS_INIT_DAYS=60 # seed-дамп покрывает ~34 дня бронирований + boarding_passes
```
Тюнинг PostgreSQL не требуется для текущих объёмов данных.