0073. Счётчики занятого объёма, пулы соединений и лимиты загрузок
Статус: accepted. Owner: Ian Panaev. Дата: 2026-10-11.
Контекст
Аудит производительности (2026-10-11) нашёл места, которые первыми ломаются при росте каталога и числа CI-агентов. Процессор цикл событий не блокирует: разбор архивов и JSON ограничен, синхронного ввода-вывода в серверных путях нет. Узкие места — в базе и в лимитах:
- каждое создание загрузки берёт глобальную блокировку
(18471, 2)и под ней суммирует размер всех не освобождённых загрузок, а затем ещё раз по репозиторию; те же суммы считаются на каждом кускеdocker pushи каждую минуту в проверке квот. При миллионах строк это полный проход по таблице внутри глобальной блокировки; - пул API — 10 соединений на 128 одновременных запросов, и до трёх из них постоянно держат служебные сессии (владение хранилищем, закрепления скачиваний, аренда шлюза);
- пул блокировок записи у worker — 2 соединения, а каждое зеркало держит одно на всё копирование части;
- по умолчанию 2 одновременные загрузки на сервер и 1 на ключ; SDK ждал ровно
Retry-After, и отказанные клиенты возвращались одновременно; max_connectionsуправляемого PostgreSQL — 100 по умолчанию.
Решение
- Счётчики занятого объёма (схема 35). Таблица
arkvory_upload_bytes(repository, status, bytes)хранит сумму размеров не освобождённых загрузок по репозиторию и статусу. Её ведёт триггер наarkvory_uploadsв той же транзакции, что и изменение строки, — при любом пути (создание, публикация, отмена, очистка, политика хранения, зеркала, восстановление). Вручную её не обновляет ни один вызывающий код. Внутри одного изменения строки счётчики блокируются в порядке ключа.- Проверки ёмкости и квот, место для OCI-загрузок и сводка использования репозитория читают маленькую таблицу вместо прохода по загрузкам.
- Глобальная блокировка создания остаётся: лимит ёмкости строгий, а держится она теперь миллисекунды.
- Миграция блокирует запись в
arkvory_uploadsна время одного подсчёта (SHARE ROW EXCLUSIVE) и включает триггер в той же транзакции: ни одно изменение не проходит между подсчётом и триггером. Rolling update ждёт её; старый код таблицу не читает. - Таблица производная: в резервную копию не входит, нормализация восстановления пересчитывает её из строк.
- Пулы. Долгоживущие сессии получают свой пул (8 соединений: закрепления одновременных скачиваний тоже берут их, а с 3 скачивание на HA-стенде ждало соединение дольше 5 с) и не занимают соединения запросов. Пул запросов API по умолчанию — 20 (
ARKVORY_DATABASE_POOL_SIZE, нижняя граница прежняя, 4). У worker пул блокировок записи — 2 плюс по соединению на зеркало (до 16). Метрикиarkvory_database_pool_{connections,idle,waiting}по пулам. - PostgreSQL. Управляемый кластер и Compose запускаются с
max_connections=200.idle_in_transaction_session_timeout=60sво всех пулах каталога: зависшая транзакция освобождает блокировки на сервере. - Лимиты загрузок. По умолчанию 8 одновременных загрузок на сервер (
ARKVORY_MAX_UPLOADS, как и раньше до 32) иmin(4, всего)на ключ. Явно заданные значения остаются допустимыми. - Повторы SDK. После
Retry-Afterклиент ждёт ещё до половины этого времени случайно (не дольше политики): отказанные вместе клиенты возвращаются в разное время.
Отклонённые предложения аудита
- Возвращать соединение блокировки записи в пул после
pg_advisory_unlock. Соединение уничтожается намеренно (ADR 0042): блокировки сессии гарантированно уходят вместе с ним, на этом держится защита от второго писателя. Новое локальное соединение стоит миллисекунды на часть от 8 МиБ. lock_timeoutдля обычных запросов. Он действует и на рекомендательные блокировки: публикации, которые сейчас ждут блокировку репозитория до 10 с, падали бы раньше — меняется режим отказа без измеренной пользы.
Последствия
- Схема 35;
MINIMUM_RESTORE_SCHEMAпрежний. Расхождение счётчика с суммой — ошибка, тесты сравнивают их после каждого вида изменения и при параллельных транзакциях. - Публикации одного репозитория уже сериализованы блокировкой репозитория; счётчик горячей точки не добавляет. Создание загрузок сериализовано глобальной блокировкой, как и раньше, но без полного прохода.
- Следующие этапы того же аудита — лишние копии больших объектов (завершение multipart, OCI, npm, зеркала) и деградирующие запросы (политика хранения, поиск, разрешение пакетов, очистка) — меняют путь записи и запросы и решаются отдельно.