Шлюзы чтения с общим бюджетом
Реализован дополнительный профиль запуска того же API: один writer и до 15 reader-процессов. Все используют одну primary PostgreSQL и одно согласованное файловое хранилище. Каждый процесс получает фиксированную долю download-бюджета по конечной DB lease. Это масштабирование раздачи; репликация, автоматический failover и балансировщик в этот инкремент не входят. Решение: ADR 0009.
Хранилище и права
Сначала writer создаёт storage root и привязку storage-id в БД. Reader проверяет существующие каталоги и identity, не создаёт их и не принимает HTTP-изменения. Даже ключ с write получает 405, Allow: GET, HEAD, code: read_only для изменяющего метода. Репозиторные права продолжают проверяться на каждом чтении. Консоль размещается на writer.
Reader должен видеть ровно те immutable blobs, которые writer опубликовал в каталоге, без задержки файловой репликации. Копирование storage-id не подтверждает наличие или актуальность байтов. Независимые диски с rsync/асинхронной репликацией не подходят. Текущие испытания используют один локальный каталог; конкретная сетевая ФС ещё не сертифицирована. До её испытаний профиль применим к нескольким процессам на одном сервере. Для разных машин нужны проверенные coherency, hard links и fsync на writer, отсутствие устаревшего чтения на reader и соответствующая схема долговечности.
На reader рекомендуется read-only mount содержимого. HTTP-запрет записи не заменяет права ОС. PostgreSQL-соединение reader всё ещё пишет: служебные leases, события диагностики хранилища, время последнего использования персональных токенов и замки защиты содержимого. Отдельная минимальная DB-role пока не реализована. На всех шлюзах должны совпадать service-key scopes и стабильные principal ID; разные ключи одного клиента сохраняют один ID. TLS, секреты и ограничения backend-портов настраиваются по CORE_RUNBOOK.
Настройка
Общие параметры для двух участвующих шлюзов, суммарно 64 MiB/s и до 16 MiB/s на principal:
ARKVORY_GATEWAY_SLOTS=2
ARKVORY_SHARED_DOWNLOAD_BYTES_PER_SECOND=67108864
ARKVORY_SHARED_DOWNLOAD_BYTES_PER_SECOND_PER_PRINCIPAL=16777216На writer:
ARKVORY_ROLE=api
ARKVORY_GATEWAY_SLOT=0На reader:
ARKVORY_ROLE=reader
ARKVORY_GATEWAY_SLOT=1ARKVORY_DATABASE_URL указывает на ту же primary БД, ARKVORY_DATA_DIR — на соответствующий mount общего root. Путь может отличаться, identity обязана совпасть. Остальные обязательные параметры API сохраняются. Запуск — npm start с отдельным окружением каждого процесса. Установщики регистрируют службами только api, worker и backup; reader-шлюз оператор запускает сам, например node releases/<версия>/apps/api/dist/main.js с собственным окружением (ARKVORY_ROLE=reader, ARKVORY_GATEWAY_SLOT, свой ARKVORY_PORT). Launcher установленных служб (runRole) накладывает config/runtime.json поверх окружения процесса, поэтому ключи, заданные там (в том числе ARKVORY_PORT), нельзя переопределить переменными unit-файла: отдельный порт у процесса, запускаемого через launcher, так не получить. Completion worker в базе один. Второй экземпляр не завершается, а ждёт в резерве: он один раз сообщает worker.standby и пробует взять блокировку каждые 5 секунд. Когда первый останавливается, второй начинает работу без перезапуска супервизором.
SLOTS — общее число участников, включая writer, от 2 до 16. Writer всегда slot 0, readers — уникальные 1..SLOTS−1. Глобальный rate обязателен; минимум SLOTS × 65536, максимум 1 TiB/s. Per-principal rate может быть 0 (отключён) либо иметь тот же диапазон. Для каждого процесса выделяется floor(rate / SLOTS); остаток округления не расходуется. Локальные ARKVORY_DOWNLOAD_BYTES_PER_SECOND* могут уменьшить долю, но не увеличить её. Upload-бюджеты остаются на единственном writer.
Неиспользуемые доли не перераспределяются: при остановке одного из двух шлюзов доступна половина бюджета. Это сознательная плата за простой проверяемый предел. Очереди и лимиты активных передач остаются локальными, их ёмкости суммируются по процессам. Динамический глобальный scheduler и приоритеты ещё не реализованы.
Владение долей и сбои
PostgreSQL резервирует slot на 10 секунд. Локальное разрешение действует максимум 8 секунд от начала запроса, включая ожидание соединения и ответа. Продление начинается через 2 секунды после предыдущего успешного подтверждения. UUID процесса и возрастающая generation ограждают нового владельца от старого. Проверяется монотонное и календарное время; просроченное разрешение не возобновляется даже после позднего ответа или восстановления связи.
При потере lease шлюз прекращает выдачу новых квантов payload, возвращает 503 на новые авторизуемые запросы и требует перезапуска. Это относится и к writer: новые upload-кванты и запросы останавливаются консервативно. Уже начатая локальная сборка может корректно завершиться и зафиксировать результат; lease раздачи не заменяет storage-транзакции. Обрыв download требует клиентского retry/Range. Закрытие процесса не сокращает DB expiry. Поэтому повторный запуск того же slot сам ждёт истечения резервации, до 10 секунд после последнего продления. Неподвижная резервация остановленного процесса дожидается, а продлеваемая (тот же slot у работающего шлюза) отклоняется как busy через 2–3 секунды. Ожидание ограничено 15 секундами; супервизор с задержкой всё равно нужен.
Аренда держит собственное соединение с PostgreSQL от запуска до остановки: продление каждые 2 секунды не ждёт общего пула, который передачи и запросы каталога могут занять дольше срока аренды. Каждому шлюзу нужно на одно соединение больше, чем ARKVORY_DATABASE_POOL_SIZE.
Предел зависит от стабильности часов: суммарный сдвиг/дрейф часов БД и узла за lease должен быть существенно меньше двухсекундного запаса. Скачок DB clock вперёд, изменение времени при failover или длительная остановка VM требуют остановки/ограждения старых процессов перед запуском замены. Lease не является аппаратным fencing. Уже разрешённые байты могут оставаться в ограниченных пользовательских, TCP и proxy буферах. Контролируется payload приложения с допустимым burst, а не физический uplink; см. TRAFFIC_CONTROL.
Авторизованный /health/ready возвращает role, writable (всегда false на reader), sharedDownloads: { slot, slots, active, leaseSeconds } и локальные transfers. В standalone sharedDownloads равен null. При потерянном владении — 503. /health/live показывает только живой HTTP-процесс и не подходит для выбора обслуживающего шлюза. Для балансировщика без ключа подходит публичный /health/status: он отвечает 503, пока шлюз не может обслуживать (потеря владения, lease или drain).
Захват роли после сбоя зависит от того, когда PostgreSQL освободит сессионные замки умершего процесса. При обрыве сети без FIN сервер узнаёт об этом только по TCP keepalive. Задайте на PostgreSQL tcp_keepalives_idle, tcp_keepalives_interval и tcp_keepalives_count (например, 10 с, 5 с и 3) или tcp_user_timeout. Иначе новый writer или worker может ждать системных часов по умолчанию.
Обновление и обслуживание
- Остановить все прежние API, readers и worker; сделать согласованный backup БД и содержимого.
- Выполнить
npm run migrateновым кодом. Миграции additive, но включать общий профиль рядом со старыми бинарниками нельзя: они не соблюдают новый бюджет. Миграция не проверяет живых участников, а старый reader узнаёт о новой схеме только по readiness, поэтому остановка всех процессов перед ней обязательна. - Настроить одинаковую policy и principal ID, запустить writer, затем readers. Проверить readiness и Range download на каждом участнике.
- Подключать внешний балансировщик после этих проверок.
Policy сохраняется в БД; её отсутствие на reader, несовпадение rates/числа slots и повторное занятие slot блокируют startup. Запуск standalone рядом с shared-профилем запрещён. Даже после остановки участников сохранённая policy защищает от случайного запуска без лимитов. Advisory barriers продолжают запрещать второй writer и обслуживание при живом reader.
Онлайн-изменение policy не поддерживается. Для изменения бюджета/числа slots либо возврата к standalone: отключить вход, остановить и проверить остановку всех участников/worker, дождаться истечения всех записей arkvory_gateway_leases, сделать backup. Администратор выбранной Arkvory-БД может затем в одной транзакции удалить строки arkvory_gateway_leases и arkvory_download_policy. После этого запустить writer с новой конфигурацией, затем соответствующие readers. Это сброс только координационного состояния, не каталога артефактов. Никогда не выполнять сброс при живых процессах или неопределённом состоянии узла. Автоматическая смена policy пока отсутствует.
Offline GC/scrub требует остановки writer, всех readers и worker. Reader удерживает maintenance barrier до закрытия DB-соединения. При partition оператор обязан подтвердить остановку процесса независимо от отсутствия его соединения в PostgreSQL.
Внешняя маршрутизация
Балансировщик развёртывается отдельно. Изменяющие методы и /console/ отправляются только writer. Чтения байтов (GET/HEAD /api/v1/repositories/{repository}/artifacts/{id}/content, .../packages/content и .../asset/content) могут распределяться между writer и readers. Остальные запросы безопаснее оставлять на writer. Проверяйте authenticated readiness каждого backend; reader writable: false является нормальным состоянием. Не вводите redirects с ключом в URL. Сохраняйте Authorization, Range, If-Range, ETag и потоковую передачу без полной proxy-буферизации.
SDK может продолжить immutable download через другой здоровый backend за тем же входным адресом. Активное TCP-соединение между серверами не переносится. Конфигурация балансировщика и двухсерверный HA здесь не развёртывались.
Проверки
Unit tests проверяют доли/округление, задержку ответа, expiry, паузу процесса и запрет оживления старого lease. PostgreSQL/HTTP tests запускают writer и два отдельных reader-процесса, сравнивают байты и общий темп, проверяют ACL/405/Range, убивают reader и скачивают проверяемый файл через writer. TCP proxy задерживает трафик reader↔PostgreSQL: локальная lease останавливает активный поток, новый процесс получает slot после DB expiry, восстановление связи не оживляет старый процесс. Также проверяются поколения, конфликт policy, непроинициализированное хранилище, standalone bypass и maintenance barrier. Это локальные отказные испытания, не проверка двух физических машин и репликации.