Два дополнительных сюрприса ждали нас после стабилизации основной производительности.
Проблема 1: Foreign Keys блокируют строки
Проверка FK в PostgreSQL выполняет SELECT 1 FROM parent_table WHERE id = ? FOR NO KEY UPDATE. Это блокирует строку в родительской таблице на время транзакции. Для таблицы-справочника регионов (42 строки, обращения из каждой транзакции) это создавало contention:
-- Мониторинг блокировок
SELECT relation::regclass, mode, count(*)
FROM pg_locks
WHERE relation = 'regions'::regclass
GROUP BY relation, mode;
-- RowShareLock: 847 (одновременно!)
Решение: мы убрали FK на «горячих» справочниках и заменили их проверкой на уровне приложения. Позже эта проблема была классифицирована как известный баг в Postgres Pro и исправлена в патче.
Проблема 2: HOT Update на счётчиках
Таблица-счётчик обращений обновлялась 300-500 раз в секунду (одна строка на филиал). PostgreSQL использует HOT (Heap Only Tuple) update для ускорения — обновлённая строка размещается на той же странице без обновления индексов.
Но HOT update на одной строке создавал длинные цепочки версий, замедляя чтение:
-- Диагностика HOT chains
SELECT schemaname, relname, n_tup_hot_upd, n_live_tup
FROM pg_stat_user_tables
WHERE relname = 'branch_counters';
-- n_tup_hot_upd: 45,000,000 n_live_tup: 42
Решение: создание индекса на изменяемые поля отключает HOT update, но PostgreSQL начинает обновлять индекс при каждом UPDATE. Для счётчика мы изменили архитектуру: вместо обновления одной строки — INSERT в лог-таблицу с периодической агрегацией:
-- Вместо UPDATE branch_counters SET count = count + 1
INSERT INTO counter_log (branch_id, delta, ts)
VALUES (42, 1, now());
-- Агрегация раз в минуту
INSERT INTO branch_counters_agg (branch_id, total_count)
SELECT branch_id, SUM(delta)
FROM counter_log
WHERE ts > now() - interval '1 minute'
GROUP BY branch_id
ON CONFLICT (branch_id)
DO UPDATE SET total_count = branch_counters_agg.total_count + EXCLUDED.total_count;
Оставить комментарий