Task #6129
closedTask #6116: EPIC: Speed up the stats updater daemon (scripts/workflow/update_stats.sh)
Индексы под горячий путь демона: stat_slices вообще без индексов
100%
Description
Реализовано в ветке speedup/stats-daemon, коммит 4816752b, миграция V135__20260826__add_stats_daemon_indexes.sql. Версия поднята 1.6.5 → 1.6.6.
Проблема¶
На stat_slices не было ни одного индекса, кроме первичного ключа: таблица создана в V98, а общий проход по индексам был в V89, то есть раньше. При этом это самая большая таблица в схеме — строка на компанию на метрику на день, за год.
Это делало #6118 опасным, а не полезным. Пропуск уже скачанных интервалов добавил запрос
SELECT * FROM stat_slices
WHERE metric_name = ? AND date_start >= ? AND date_end <= ?
ORDER BY date_start
на каждую пару компания-метрика. Без индекса каждый такой запрос — seq scan по всей таблице, и выигрыш от снятых обращений к iiko съедался бы базой. Тот же запрос обслуживает чтение статистики из API, так что он горячий и вне демона.
Что добавлено¶
-
stat_slices (metric_name, date_start)— равенство по metric_name, диапазон и сортировка по date_start, ровно форма запроса выше.date_endтретьей колонкой не нужен: по второму диапазону btree искать не умеет, останется фильтром. -
companies (user_id)— колонка появилась вV102, после индексного проходаV89, и осталась без индекса. Используется вfindAllByUserOrIdIn. -
iiko_requests (created_at)— индексов не было вовсе, а росла таблица быстрее всех. Нужен для вычистки старых строк. -
iiko_auth_tokens (is_active, created_at DESC)— подfindAllByisActiveOrderByCreatedAtDesc.
Предел btree и защита от него¶
metric_name — склейка uuid всех компаний партнёра. У btree потолок 2704 байта на кортеж. Замерено на живом postgres, реальные uuid:
| компаний | длина | btree |
|---|---|---|
| 70 | 2621 | ок |
| 80 | 2991 | index row size 3016 exceeds btree version 4 maximum 2704 |
Предел — около 72 кофеен на партнёра. Если такое имя уже есть, CREATE INDEX падает вместе со всей миграцией и стартом приложения.
Поэтому миграция смотрит на фактическое max(length(metric_name)) и при превышении порога берёт hash по metric_name плюс btree по date_start. Хуже (равенство без диапазона, дальше BitmapAnd), но лучше seq scan, и деплой не падает.
Порог 2000, а не 2660 — с запасом: проверка видит только сегодняшние имена, а партнёр может обрасти кофейнями завтра, и тогда сломается уже не миграция, а INSERT на проде.
Проверено¶
- миграция применена флайвеем к живому postgres в прогоне тестов, без ошибок;
- все четыре индекса присутствуют в
pg_indexesс ожидаемыми определениями; - ветка
ELSE(hash + btree) проверена отдельно на пробной таблице; - прогон: 559 тестов, 66 падений — те же, что и до изменений (#6128).
Чего проверить нельзя: поведение планировщика на боевых объёмах. В тестовой базе в stat_slices 98 строк, там EXPLAIN всё равно выберет seq scan.
При выкатке¶
Обычный CREATE INDEX берёт ACCESS EXCLUSIVE на время построения — на большом stat_slices это пауза на запись. Если простой недопустим, создайте индексы заранее через CREATE INDEX CONCURRENTLY: все CREATE идут с IF NOT EXISTS, миграция тогда станет пустой.
Отдельно: на сервере рядом с jar лежит VERSION, из которого скрипты берут номер. В репозитории его нет — при выкатке 1.6.6 обновить руками.
Updated by Redmine Admin about 7 hours ago
- Status changed from New to Resolved
- % Done changed from 0 to 100
Updated by Redmine Admin about 7 hours ago
- Related to Task #6118: Fetch only the missing date tail instead of re-downloading 365 days every round added