Skip to content

0076. Планировщик скачиваний: сессии, очередь, приоритеты и окна ​

Статус: proposed (этапы 1, 2а, 2б и часть этапа 3 реализованы). Owner: Ian Panaev. Дата: 2026-10-14.

Контекст ​

Массовое обновление: 1000 клиентов одновременно скачивают по 4–20 ГБ через канал 1 Гбит/с. Это 4–20 ТБ, то есть 9–45 часов передачи при любом коде. Задача планировщика — не ускорить канал, а сделать так, чтобы ни один клиент не отвалился, каждый знал, когда начнётся его скачивание, и первые закончили через минуты, а не все разом в конце.

Что сейчас (ADR 0004, 0007, 0008, 0009, 0022):

  • Допуск выдаётся на HTTP-запрос. SDK и arkvoryctl читают файл диапазонами по 8 МиБ, и каждый диапазон заново проходит AdmissionQueue: 20 ГБ — около 2500 допусков. 1000 клиентов делят канал поровну, и все заканчивают одновременно.
  • Очередь сервера в памяти: 16 активных, 64 ждущих (8 на principal), ожидание до 20 с. Переполнение — сразу 503 busy с фиксированным Retry-After: 2. Ни места в очереди, ни срока клиент не узнаёт.
  • Бюджет повторов SDK — 20 на всю операцию и 5 на запрос, ожидание дольше 60 с считается отказом. При тысяче клиентов бюджет кончается за минуты: обновление падает. arkvoryctl сохраняет префикс, но продолжит только при повторном запуске.
  • Под одним ключом (частый случай для парка агентов) активны не больше 4 скачиваний, остальные получают отказ.
  • Сторонние клиенты (docker/oras, git-lfs, npm/Unity, curl/wget по ссылкам и путям) видят только статус и Retry-After; их повторами мы не управляем.

При одинаковых файлах среднее время завершения минимально, когда файлы обслуживаются по очереди целиком, и максимально при делении поровну. Поэтому единица планирования — сессия скачивания, а не запрос.

Решение ​

1. Клиент никогда не сдаётся из-за занятости ​

Ответ 503 busy или 429 с Retry-After — это «приходи позже», а не сбой. В SDK такие ожидания не расходуют бюджет повторов (maxRetries, maxAttempts) и не ограничены maxDelayMs: клиент ждёт указанное время плюс случайную добавку, пока не истечёт общий срок операции, заданный вызывающим (waitLimitMs, по умолчанию без срока), или не сработает отмена. Бюджет повторов остаётся для настоящих сбоев: обрыв сети, 502/504, таймаут попытки. arkvoryctl download по умолчанию ждёт без срока (--wait-limit задаёт срок) и печатает, сколько ещё ждать.

2. Сессия скачивания ​

Сессия — допуск на всё скачивание одного объекта (артефакт, путь, пакет, слой OCI, объект LFS, tarball npm) одним клиентом. Пока сессия активна, её диапазоны идут без повторного допуска; полоса по-прежнему делится BandwidthGovernor (ADR 0008).

  • Явная сессия. SDK, CLI и консоль просят талон: POST /api/v1/downloads с Idempotency-Key и объектом. Ответ — состояние queued или active, место в очереди, оценка начала startsAt с интервалом, checkAt (когда спросить снова) и срок жизни. Диапазоны сессии несут заголовок Arkvory-Download-Id.
  • Неявная сессия. Запрос без талона встаёт в ту же очередь под ключом «principal + объект + Arkvory-Download-Id», который клиент создаёт сам на одно скачивание (SDK и arkvoryctl — с этапа 2а). Без id (сторонние инструменты, ссылки, старые клиенты) ключ — адрес клиента, и клиенты за одним NAT с одним ключом делят одну сессию. Пока сессия ждёт, клиент с id получает 503 busy с Retry-After до checkAt и заголовками Arkvory-Queue-Position и Arkvory-Starts-At; запрос без id сервер сначала держит до очереди ограниченное время.
  • Активная сессия держит место, пока идут байты. Пауза между диапазонами дольше idleSeconds (30 с), выдача всех байтов объекта или отмена освобождают место.
  • Ждущая сессия, которую клиент не подтвердил вовремя (не пришёл к checkAt плюс запас), удаляется — «неявка». Неявка не наказывается: новый запрос встаёт в конец очереди своего класса.

3. Стратегии ​

Оператор выбирает стратегию во время работы, без перезапуска:

  • ordered (по умолчанию) — классы приоритета с весами, внутри класса FIFO, между principal класса — по кругу. Ждущий дольше agingMinutes поднимается на класс выше: никто не голодает.
  • smallest-first — внутри класса первыми идут меньшие объекты (минимум среднего времени при разных размерах), со старением, чтобы большие не ждали вечно.
  • shared — прежнее поведение: допуск на запрос, канал делится поровну. Для тех, кому важнее равномерность, чем раннее завершение.

Смена стратегии действует на следующие решения о допуске. Активные сессии не прерываются.

4. Классы приоритета ​

Встроенные классы: interactive (люди в консоли и CLI), release (CI и развёртывания), bulk (массовые обновления), background (синхронизация зеркал и прочее фоновое).

  • Безопасные умолчания. Люди — interactive, сервисные аккаунты и ссылки — release; ссылка — отдельный принципал, не выпустивший её. Веса 8/4/2/1. Для interactive резервируется одно место (от четырёх мест): консоль работает даже при полной очереди обновлений.
  • Настройка. Оператор меняет веса, резерв и прочие параметры (этап 2а — переменными окружения, этап 3 — в консоли) и класс каждого сервисного аккаунта (этап 3; класс входит в сервисную политику с её ревизией и аудитом). Клиент может только понизить свой класс заголовком Arkvory-Download-Class или в запросе талона (например, агент обновления просит bulk), но не повысить; неизвестное значение — 400, применённый класс возвращается в ответе.

5. Окна ​

  • Окна клиента. Талон принимает notBefore и notAfter. Планировщик не начинает сессию раньше notBefore и сразу говорит, если с текущей очередью к notAfter не успеть (startsAt позже срока; клиент решает сам).
  • Окна оператора. У класса могут быть окна по расписанию (дни недели, часы, часовой пояс): например, bulk — только 22:00–06:00. Вне окна класс получает outsideShare полосы (по умолчанию 0: только в окне). Начатая в окне сессия доводится до конца. Оценки начала учитывают окна.

6. Оценка начала ​

Планировщик знает размер каждого объекта, остаток активных сессий и измеренную скорость выдачи (скользящее среднее за последнюю минуту, не меньше настроенной полосы, если она задана). Начало ждущей сессии — момент, когда перед ней освободится место, с учётом классов, резерва и окон; клиенту отдаются точка и интервал. Оценка пересчитывается при каждом обращении. checkAt — середина до начала, но не позже 5 минут и не раньше 5 секунд, с разбросом по талону: опросы не приходят толпой. Точность оценок видна в метриках.

7. Ресурсы и утечки ​

  • Все структуры ограничены: сессий всего (по умолчанию 65 536), на principal (4096 — парк агентов под одним ключом), длина неявного ключа. Сверх лимита — 503 busy с Retry-After, а не рост памяти.
  • Нет таймера на сессию: один такт планировщика (500 мс) пересчитывает очередь и удаляет просроченное; поиск — по индексам (класс → principal → FIFO), без сканирования всей очереди на каждый запрос.
  • Сессия освобождается при закрытии ответа, отмене, ошибке и на такте при простое: каждый путь проверяется тестом на счётчики (active, waiting возвращаются к нулю).
  • Остановка сервиса: ждущие получают 503 busy с Retry-After, активные доводятся по ADR 0048.

8. Хранение и несколько процессов ​

Этап 2 держит очередь в памяти процесса записи за портом DownloadScheduler (application). Талон — подписанный токен с id, классом и временем постановки: после перезапуска клиент приходит с тем же талоном и встаёт на своё прежнее место, а не в конец. Этап 4 добавляет реализацию порта на PostgreSQL (выдача мест с арендой и fencing, как arkvory_jobs), общую для процесса записи и шлюзов чтения (ADR 0009); её таблицы исключаются из резервной копии.

9. Видимость ​

  • arkvoryctl download: «в очереди, 312-й, начало ≈ 14:05 (через 47 мин)», затем прогресс. Консоль показывает это же на странице «Скачивания».
  • Оператор: страница настроек скачиваний (стратегия, классы, веса, резерв, окна) и состояние очереди — длина по классам, ожидание, прогноз опустошения.
  • Метрики: сессии по классам и состояниям, время ожидания, неявки, отклонение оценки от факта. Readiness — агрегаты без id.

10. Этапы (каждый — отдельный PR) ​

  1. Устойчивый клиент (п. 1) и Retry-After по фактической очереди вместо фиксированных 2 с.
  2. Планировщик в памяти, в двух PR:
    • 2а — неявные сессии на сервере: стратегии, классы и резерв, все параметры очереди переменными окружения, оценки и заголовки очереди, Arkvory-Download-Id и Arkvory-Download-Class, вытеснение, удержание запросов сторонних клиентов, место и начало в SDK и arkvoryctl, метрики и readiness;
    • 2б — подписанный талон очереди, переживающий перезапуск, окна клиента в SDK и arkvoryctl, id скачивания в контрольной точке arkvoryctl.
  3. Настройки оператора: стратегия, веса, резерв, классы сервисных аккаунтов, окна классов; страница консоли.
  4. Очередь в PostgreSQL для шлюзов чтения и HA.
  5. Нагрузочный гейт: масштабированная модель (например, 500 клиентов по 40 МБ на канале 40 МБ/с) проверяет, что все докачали без ошибок, оценка начала отклоняется не больше заданного, первые заканчивают за время одного файла.

Уточнения реализации (этап 2а) ​

Проверены разбором кода и детерминированной симуляцией массового обновления (tests/download-simulation.test.mjs).

  • Порог. Сессией скачиваются объекты больше ARKVORY_DOWNLOAD_SESSION_MIN_BYTES (16 МиБ). Меньшие (пара диапазонов) допускаются на запрос прежней очередью: они не должны ждать за многогигабайтными скачиваниями.
  • Порядок проверок. Очередь встаёт после авторизации и метаданных: отказ в доступе, HEAD и 304 места не занимают.
  • Завершение. Сессия хранит отданные целиком диапазоны (слитый список интервалов, не больше 64; сверх него забывается самый короткий — это только откладывает завершение до idleSeconds). Начало скачивания — наименьшее смещение запросов, кроме чтений хвоста: суффиксный диапазон или ограниченный диапазон до последнего байта (каталог ZIP) начала не задают, открытый bytes=X- и запрос без Range — задают. Сессия завершена, когда диапазоны покрывают байты от начала до последнего и других ответов нет: возобновление с середины (curl -C -, wget -c, SDK с сохранённым префиксом) завершается без байтов до смещения, чтение конца файла первым сессию не закрывает. Прерванный ответ байтов не добавляет: в покрытие идёт только ответ, отданный до конца. Пауза дольше idleSeconds освобождает место, но 10 минут скачивание хранит время прихода и отданные диапазоны; вернувшись, оно встаёт в очередь по прежнему времени прихода, а не получает место раньше ждущих.
  • Только хвост. SDK, возобновивший скачивание внутри последнего диапазона, просит ограниченный диапазон до конца: это неотличимо от чтения хвоста, и сессия не завершается сразу. Поэтому сессия, которая читала только хвост, держит место без ответов 5 секунд (не дольше idleSeconds), а не idleSeconds: следующее чтение клиента ZIP идёт сразу, а место после, возможно, законченного скачивания не простаивает 30 секунд. Ушедший так клиент сохраняет время прихода и отданный хвост, как после паузы.
  • Место без резервирования. За отсутствующим клиентом место не держится: такт отмечает первых в очереди, вдвое больше свободных мест (с местами сессий, заканчивающих в 5 с), и место берёт первый пришедший. Иначе места простаивали, пока клиент шёл к своему checkAt: симуляция показала до 10–20 % потерь на коротких скачиваниях, после правки — 0.
  • Оценка. Модель освобождения мест по остаткам активных сессий при пиковой секундной скорости за минуту, поделённой между идущими скачиваниями. Средняя скорость падала при простое и растягивала оценки, клиенты приходили позже, и простой рос. checkAt — середина до начала, от 2 с до 5 мин; первым в очереди — от 1 с. Новичку первый ответ — через 2 с без места: его место станет известно на такте, а конец очереди может оказаться следующим по кругу.
  • Доля принципала. ARKVORY_MAX_DOWNLOADS_PER_PRINCIPAL ограничивает принципала, пока первым в очереди стоит другой; без претендентов свободное место отдаётся и сверх доли.
  • Вытеснение. Без свободных мест голова очереди более высокого класса или принципал ниже доли получает место, которое на границе диапазонов уступает клиент протокола (с Arkvory-Download-Id), держащий его не меньше минуты, ниже классом или выше доли. Не больше одного за такт и только пока голове не освободилось место иначе; уступивший начинает ожидание заново, без накопленного старения, и не берёт место, пока ждёт голова, которой он уступил: иначе он, спрашивая раньше головы, забирал бы место обратно ещё на минуту. Инструменты без id не вытесняются: многие не переживают 503 посреди файла. ARKVORY_DOWNLOAD_PREEMPT=false отключает.
  • Сторонние клиенты. Запрос без id сервер держит до очереди до ARKVORY_DOWNLOAD_HOLD_SECONDS (по умолчанию срок прежней очереди, 20 с), не больше ARKVORY_TRANSFER_QUEUE_LIMIT одновременно: так docker, npm и браузер по ссылке ведут себя не хуже, чем до очереди.
  • Сессия за NAT. Одновременно идут не больше rangesPerSession (4) ответов сессии: клиенты без id за одним адресом не обходят очередь целиком.
  • Старение поднимает не выше release: полоса людей остаётся их. agingMinutes = 0 отключает.
  • Резерв по умолчанию — одно место при четырёх и больше местах: при двух местах резерв отдавал бы людям половину канала.
  • Шлюзы чтения до общей очереди этапа 4 по умолчанию в shared: очередь в памяти процесса за балансировщиком без привязки клиента теряла бы позиции.
  • Часы монотонные: сдвиг системного времени не сбрасывает очередь и не держит места.
  • Стоимость такта — сортировка O(n log n); 65 536 ждущих — около 30–40 мс на такт. Длительность последнего такта видна в readiness. Следующий такт — через 500 мс или через десять длительностей последнего, если это дольше: очередь у предела ARKVORY_DOWNLOAD_QUEUE_LIMIT (до 1 048 576) занимает не больше десятой доли цикла событий ценой более редкой передачи мест и пересчёта оценок.
  • Заголовки без префикса X- (RFC 6648); прежние X-Arkvory-* не переименовываются.
  • До этапа 2б талонов нет: после перезапуска процесса сессии встают в очередь заново.

Уточнения реализации (этап 2б) ​

  • Талон вместо ресурса. Отдельный ресурс POST /api/v1/downloads не нужен: всё, что он давал, уже идёт заголовками ответа на сам запрос байтов (место, начало, срок проверки). Позиция, переживающая перезапуск, — заголовок Arkvory-Queue-Ticket: 2.<время прихода>.<время выдачи>.<HMAC-SHA256> (время — миллисекунды по стене в base 36) над хешем ключа сессии и обоими временами. Клиент возвращает последний полученный талон; новый процесс ставит скачивание на прежнее время прихода. Порядок очереди — по времени прихода, затем по номеру. Талон нельзя перенести на другое скачивание или сдвинуть раньше. Он принимается 15 минут после выдачи (10 минут хранения паузы и запас) и выдаётся заново с каждым ответом с тем же временем прихода: ожидание дольше суток место не теряет, а старый талон нельзя повторить, чтобы вернуть раннее время. Время выдачи до 2 минут впереди часов узла (расхождение часов HA) считается текущим; приход позже выдачи и выдача дальше в будущем отвергаются. Неверный талон игнорируется, скачивание встаёт заново. Талоны прежнего формата 1. не читаются: талон — подсказка, его потеря стоит места один раз при обновлении.
  • Ключ подписи — одна строка arkvory_download_ticket_key (миграция 38). Её создаёт первый процесс записи, читают все процессы установки, поэтому талон верен после перезапуска и на другом узле HA. В резервную копию не входит: восстановленная установка делает свой. Шлюз чтения без ключа (до миграции при поэтапном обновлении или до первого старта процесса записи) работает без талонов и ищет ключ снова каждые 30 секунд, пока не найдёт.
  • Окна клиента — на стороне клиента, сервер для них не нужен. notBefore: SDK и arkvoryctl --not-before не спрашивают раньше. notAfter: если Arkvory-Starts-At позже срока или срок прошёл в очереди, SDK завершает скачивание с queue_deadline, arkvoryctl --not-after — с кодом 4, а не ждёт впустую. Время — RFC 3339 или ЧЧ:ММ (ближайшее наступление). Ожидание любой длины SDK ждёт частями: таймер платформы держит не больше 2^31−1 мс и больший срок срабатывает сразу. До notBefore SDK ждёт шагами не больше минуты и каждый раз заново сверяет остаток с часами: таймер не считает сон ноутбука и паузу ВМ, а часы могут перевести, и одно долгое ожидание сдвинуло бы начало на часы. Retry-After больше часа (ошибка прокси) SDK ждёт час и спрашивает снова. Окна оператора по классам остаются этапом 3.
  • arkvoryctl хранит Arkvory-Download-Id в контрольной точке скачивания: новый процесс продолжает то же скачивание в очереди. Старые контрольные точки получают id при первом использовании, без несовместимости.
  • Часы. Талон и Arkvory-Starts-At — во времени по стене, очередь — в монотонном; планировщик переводит одно в другое.

Уточнения реализации (этап 3, первая часть) ​

  • Настройки в БД. Одна запись arkvory_download_settings (миграция 40) с ревизией: strategy, sessions, reserve, perPrincipal, weights, sessionMinBytes, idleSeconds, agingMinutes, preempt, rangesPerSession. Переменные окружения остаются значениями по умолчанию; запись хранит только изменённые поля, {} — возврат к окружению. Длина очереди и удержание сторонних клиентов размеряют память при старте и остаются только в окружении.
  • Проверка до сохранения. Изменение проверяется политикой этого сервера (overrideDownloadPolicy): настройки, которые не могут работать, не сохраняются (400 с полем). Если sessions меняется без reserve/perPrincipal, они понижаются, чтобы поместиться, и никогда не повышаются молча. Веса задаются только все четыре сразу.
  • Применение без перезапуска. DownloadScheduler.configure действует со следующего решения; активные сессии сохраняют места. Процесс записи читает ревизию раз в 10 секунд и при старте; изменение через API применяется сразу. Сохранённое, но не подходящее этому серверу после смены окружения: очередь работает на окружении, problem объясняет почему.
  • Шлюзы чтения не применяют запись до общей очереди этапа 4: их очереди — на процесс. GET на шлюзе показывает его окружение с пояснением в problem, PUT на шлюзе — read_only. В установке со шлюзами процесс записи не уходит со стратегии shared (400 на /overrides/strategy): иначе диапазоны одного скачивания встречали бы два вида очереди. Запись с другой стратегией, сохранённая до появления шлюзов, тоже не применяется: работает окружение, problem объясняет почему.
  • Устойчивое чтение записи. Запись читается мягко: поля новой версии (сохранённые до отката) пропускаются, известное поле вне диапазона даёт problem, и работает окружение. Ошибка БД при старте не останавливает процесс: фоновая задача повторяет чтение каждые 10 секунд.
  • Порядок ревизий. Применяется только более новая ревизия, чем работающая: поздний ответ старого чтения не откатывает свежее изменение. GET применяет запись, если она новее.
  • admission. При стратегии shared допуск идёт по запросу через ограничения окружения, и настройки сессий не действуют: ответ говорит admission: per-request, консоль объясняет это. Удерживаемый запрос перепроверяет стратегию и порог: после переключения на shared или подъёма порога он уходит к допуску по запросу, а не ждёт места в очереди.
  • Конкурентность. PUT /api/v1/system/downloads/settings с expectedRevision — CAS одним SQL-оператором; 409 revision_mismatch при гонке. Записываются updated_by и имя администратора управляющей площадки (ADR 0077) для ключа управления.
  • Запись не входит в резервную копию: настройки подобраны под оборудование и канал источника.
  • Классы сервисных аккаунтов и окна классов — следующая часть этапа 3.

Последствия ​

  • Правило ADR 0007 «Retry-After дольше maxDelayMs — отказ» заменено п. 1: для ответов busy и rate_limited клиент ждёт, сколько просит сервер, в пределах waitLimitMs. Остальные повторы ADR 0007 не меняются.
  • Публичный контракт расширяется аддитивно: ресурс /api/v1/downloads, заголовки Arkvory-Download-Id, Arkvory-Download-Class, Arkvory-Queue-Ticket, Arkvory-Queue-Position, Arkvory-Starts-At, причина download_queued, поле класса в сервисной политике. Существующие клиенты работают без изменений: без талона они получают неявную сессию.
  • ARKVORY_MAX_DOWNLOADS остаётся пределом одновременных запросов для объектов до порога и для shared; места сессий задаёт ARKVORY_MAX_DOWNLOAD_SESSIONS (по умолчанию то же число).
  • Сторонние клиенты сначала ждут на сервере, затем получают честный Retry-After; их дальнейшие повторы остаются на их совести. Для массовых раздач рекомендуются SDK или arkvoryctl.
  • Очередь не увеличивает пропускную способность канала. Для 20 ТБ через 1 Гбит/с по-прежнему нужны зеркала и шлюзы на площадках, волны через хаб и, позже, дельты между версиями.

Arkvory · ProAnimaStudio