- Зачем:
- бенчмарк на 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>
4.9 KiB
Бенчмарк генерации 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 не требуется для текущих объёмов данных.