К восьми утра в бухгалтерии открывают сводный отчёт. Если за ночь очередь заданий не опустела, первую половину дня люди будут смотреть на данные, которые наполовину вчерашние. Поэтому срок здесь жёсткий: в 07:00 очередь должна быть пуста, а всё, что не успело, превращается в чьё-то утреннее объяснение. Вопрос звучит просто: сколько заданий VPS успеет обработать за ночь? На него хочется ответить одним делением. Это деление и есть главный подозреваемый.
Ниже один учебный пример разобран по числам. Задачи в нём условные, но способ расчёта переносится на любую ночную очередь: от пересчёта каталога до выгрузки документов во внешнюю систему. Сначала разберёмся, что именно измерять, потом как распределить работников, где упрётся система и как один неудачно стоящий тяжёлый элемент сдвигает финиш на полчаса.
Среднее время ошибается в обе стороны
Привычная формула выглядит так: берём число заданий, умножаем на среднее время одного, делим на количество ядер и сравниваем с ночным окном. Она удобна тем, что даёт ответ за минуту, и опасна тем, что почти всегда правдоподобна. Ошибается она сразу в двух направлениях.
Во-первых, в «среднем времени» смешано всё: секунды, которые процессор реально считает, секунды ожидания ответа чужого сервиса и секунды записи результата. Ожидание ядро не занимает, поэтому деление на ядра завышает нагрузку на процессор там, где задания в основном ждут. Во-вторых, среднее скрывает неоднородность. Если большинство заданий укладывается в доли секунды, а малая часть считается десятками секунд и одно длится почти час, то среднее выглядит безобидно, а ночь определяют именно тяжёлые элементы и самое длинное из них. Они не «растворяются» в очереди: у каждого есть своя продолжительность, и она упирается в конкретного работника.
Поэтому расчёт начинается не с деления, а с измерения. И измерять надо не самое лёгкое задание, на котором быстрее всего получается красивая цифра.
Одно задание — три разных времени
Каждое пакетное задание проходит три фазы, у которых разные хозяева и разные потолки:
- Счёт. Разбор файла, преобразование, расчёт. Тут занят процессор, и время растёт с загрузкой ядер.
- Ожидание. Запрос к внешнему API, к соседнему сервису, к файловому хранилищу. Процессор свободен, но работник занят, потому что сидит и ждёт ответа. Потолок здесь лежит снаружи: лимит партнёра на параллельные запросы или скорость его ответов.
- Запись. Сохранение результата в базу. Время зависит от числа одновременных сессий и от того, как база справляется с их общим потоком.
Чтобы разделить эти времена, внутри задания достаточно трёх отметок: начало, конец счёта и ожидания, конец записи. Для счёта полезно отдельно снимать процессорное время процесса, а не только длительность по часам: разница между ними как раз и есть ожидание и очереди на ядро. Отметки пишутся в журнал вместе с типом задания, а дальше по каждому типу считаются медиана, 95-й перцентиль и максимум.
Три правила замера, которые экономят потом несколько ночей:
- В выборку входят все типы заданий из реальной очереди, включая редкие тяжёлые. Если тяжёлых одно на двадцать, в сотню заданий их попадёт пять, и этого уже хватит, чтобы увидеть разброс.
- Замер идёт при том числе работников, которое вы собираетесь запускать ночью. Одинокий работник скрывает конкуренцию за запись в базу и за память; при четырёх она проявляется.
- Замер идёт на машине того же класса, что и боевая. Характеристики плана (число vCPU, объём памяти, NVMe-диск) — это отправная точка для теста, а не готовая цифра пропускной способности. Сколько заданий в секунду потянет ваше приложение, покажет только прогон вашего кода на ваших данных.
Учебная ночь: двенадцать тысяч заданий трёх сортов
Возьмём очередь, которая к 23:00 набирается до 12 000 заданий; срок — 07:00, то есть окно в восемь часов, или 28 800 секунд. Все числа ниже условные, придуманные для расчёта; ваши замеры дадут другие, но порядок действий останется тем же. Допустим, измерение показало три типа.
| Тип | Заданий | Счёт, с | Ожидание, с | Запись, с | Итого на задание, с |
|---|---|---|---|---|---|
| А. Короткие: разбор и нормализация | 9 000 | 0,4 | 0,2 (справочник по сети) | 0,1 | 0,7 |
| Б. Обращение к внешнему API | 2 500 | 0,3 | 4,0 | 0,2 | 4,5 |
| В. Тяжёлые: пересчёт документа | 500 | 40 | 0 | 2 | 42 |
Суммарное «настенное» время: 9 000 × 0,7 = 6 300 с, 2 500 × 4,5 = 11 250 с, 500 × 42 = 21 000 с, всего 38 550 с. Среднее на задание — около 3,2 с. Формула «объём ÷ ядра» для плана с четырьмя vCPU даёт 38 550 ÷ 4 ≈ 9 640 с, то есть около 2 ч 41 мин. На фоне окна в 8 часов это выглядит как сплошной запас, и можно идти спать. Посмотрим, что скрыто за этой цифрой.
Где формула промахивается
Если пересчитать только то, что действительно занимает процессор, картина меняется. Счёт: 9 000 × 0,4 = 3 600 с, 2 500 × 0,3 = 750 с, 500 × 40 = 20 000 с. Итого 24 350 процессорных секунд. На четырёх ядрах это 6 090 с, около 1 ч 41 мин: ожидания внешнего сервиса, которые формула честно «раскладывала на ядра», процессору ничего не стоят.
Но четырёх ядер для работников на самом деле нет. База данных, которая в этом примере живёт на той же машине, и сама система тоже хотят процессор. Допустим, на них уходит одно ядро; для работников остаётся три, и теоретический минимум превращается в 24 350 ÷ 3 ≈ 8 100 с, то есть около 2 ч 15 мин. Это нижняя граница при идеальной загрузке без простоев.
И главное: 20 000 из 24 350 процессорных секунд — это пятьсот тяжёлых заданий, 82 % всей работы. Они определяют ночь, а их пропускная способность зависит не от «ядер вообще», а от того, сколько тяжёлых работников вы запустили и как они стартуют. К этому мы вернёмся, а пока разведём типы по разным путям.
Три дорожки вместо общей кучи
Если все задания лежат в одной очереди и разбираются одним пулом, тяжёлые задания начинают занимать места, которые нужны коротким. Короткое задание на 0,7 с ждёт, пока освободится работник, занятый сорока секундами пересчёта, и в очереди образуется затор из мелочи, хотя ядра формально заняты полезным делом. В документации Celery прямо рекомендуется направлять долгие и короткие задачи разным исполнителям, и расчёт показывает, почему: у них разные узкие места и разное оптимальное число работников (Celery: выполнение и доставка задач).
Для нашего примера естественны три дорожки:
- Тяжёлая (тип В): 2 работника, по одному на ядро. Каждый почти всё время считает: 40 с из 42 приходятся на процессор, так что отдельное ядро на работника — разумная отправная точка.
- Лёгкая (тип А): 2 работника на оставшееся ядро. Один работник загрузил бы ядро лишь на 0,4 ÷ 0,7 ≈ 57 %, потому что остальное время он ждёт справочник по сети; второй закрывает паузы первого.
- Внешняя (тип Б): 6 слотов ожидания. Они почти не нагружают процессор: 0,3 с счёта на 4,5 с задания — это около 7 % ядра на слот, и всей дорожке нужно примерно 0,4 ядра при полной загрузке. Почему слотов именно шесть, объясняет следующий раздел.
Теперь проверим, укладывается ли это в три ядра. Тяжёлая дорожка требует двух ядер, лёгкая — до одного, внешняя — около 0,4. Сумма немного больше трёх, но все дорожки не занимают процессор одновременно на полную: внешняя большую часть времени спит, а лёгкая закончится заметно раньше тяжёлой. Если же запустить для сравнения один общий пул из десяти работников, ядра придётся делить вслепую, и тяжёлые задания будут растягиваться за счёт мелких.
Теперь время по дорожкам. Тяжёлая: 500 × 42 ÷ 2 = 10 500 с, это 2 ч 55 мин. Лёгкая и внешняя делят одно ядро: суммарно 3 600 + 750 = 4 350 процессорных секунд. Если рассчитывать на 80 % ядра (остальное съедают переключения и мелкие паузы), получается около 5 440 с, то есть примерно 1 ч 30 мин. Внешняя дорожка независимо упирается в свой потолок, и его считаем отдельно.
Шесть окон наружу
Допустим, партнёрский сервис разрешает не больше шести одновременных запросов от вашего клиента. Каждое задание типа Б держит такое окно около 4 секунд. Пропускная способность дорожки — окна, делённые на время удержания: 6 ÷ 4 = 1,5 задания в секунду. На 2 500 заданий уйдёт 2 500 ÷ 1,5 ≈ 1 670 с, примерно 28 минут. Седьмой и восьмой работники этой дорожки ничего не ускорят: лишние запросы упрутся в отказы по лимиту и вернутся повторами, то есть добавят нагрузки без пользы.
Тут в расчёте появляется второй множитель — поведение самого партнёра. Если он замедлится до восьми секунд на запрос, те же шесть окон дадут 6 ÷ 8 = 0,75 задания в секунду, и дорожка растянется на 2 500 ÷ 0,75 ≈ 3 330 с, почти час. А если он начнёт зависать, роль играет таймаут. В документации Celery прямо сказано, что сетевым операциям нужны собственные таймауты, иначе исполнитель может зависнуть (Celery: выполнение и доставка задач). Посчитаем, что это значит для нас. Без таймаута зависший запрос держит окно бесконечно; шесть зависших запросов подряд — и вся внешняя дорожка стоит до утра, не выдав ни ошибки, ни результата. С таймаутом 30 секунд те же шесть окон при полностью лежащем партнёре обрабатывают 6 ÷ 30 = 0,2 задания в секунду; первый проход по очереди займёт 2 500 ÷ 0,2 = 12 500 с, около 3 ч 28 мин, и после этого придут повторы. Много, но это конечная цифра, которую видно в мониторинге и по которой можно принять решение вовремя, а не в 07:00.
Выбор таймаута выбирается от измеренной длительности: он должен заметно превышать 95-й перцентиль нормального ответа (в нашем примере 4 секунды типичны, значит, 30 с оставляют большой запас), но не быть таким, чтобы деградация партнёра съедала всё окно. Одно замечание о масштабе: шесть окон не нагружают сетевой стек, но если ваша очередь состоит из сотен тысяч коротких запросов с высокой параллельностью, размер таблицы отслеживания соединений становится отдельной статьёй расхода; для этого случая есть расчёт размера таблицы conntrack под свою нагрузку.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации Россия, США, Франция и Великобритания. Оплата картой РФ и по СБП.
Запись и память: потолки, которые не видны в top
Процессор — самый заметный ресурс, но не единственный потолок. У базы данных есть свой предел одновременной записи. Допустим, нагрузочный прогон показал: до шести одновременных сессий записи время записи не растёт, а на двенадцати оно удваивается. Для нашей схемы это значит, что число работников, которые одновременно пишут, должно оставаться около шести. При двух тяжёлых, двух лёгких и шести окнах внешней дорожки в сумме десять процессов, но пишут они лишь короткими вспышками: тяжёлое задание проводит в записи 2 из 42 секунд, внешнее — 0,2 из 4,5. Вероятность того, что все десять пишут разом, мала, а вот попытка «ускорить» лёгкую дорожку десятью работниками принесла бы только очередь на запись: счёт там уже упирается в одно ядро, а выросшее время записи ударило бы и по соседям.
Второй скрытый потолок — память. У PostgreSQL параметр work_mem применяется к отдельным операциям сортировки и хеширования, а их в одном запросе может быть несколько, и параллельные сессии складывают свои потребности; если памяти не хватает, операции уходят во временные файлы, а параллельные работники расходуют дополнительные ресурсы (PostgreSQL resource consumption). Для расчёта это значит, что пик памяти базы надо оценивать как число активных сессий, умноженное на число операций в запросе и на work_mem, а не как одну цифру из конфигурации.
Составим бюджет для плана с 8 ГБ памяти, снова на условных цифрах из замера:
- система и база в покое: 2,0 ГБ;
- два тяжёлых работника по 1,2 ГБ: 2,4 ГБ;
- два лёгких по 0,3 ГБ: 0,6 ГБ;
- шесть окон внешней дорожки по 0,15 ГБ: 0,9 ГБ;
- пик work_mem: 6 сессий × 2 операции × 64 МБ ≈ 0,77 ГБ.
Итого около 6,7 ГБ из 8, запас порядка 1,3 ГБ. Третий тяжёлый работник добавил бы ещё 1,2 ГБ и съел бы запас почти целиком. Выходит, что третьего тяжёлого работника на этой машине останавливает не процессор, а память: именно поэтому одной строки «ядра ÷ задания» мало.
Если работники и база запущены в контейнерах, нужно помнить, что по умолчанию контейнер Docker не имеет ресурсных ограничений и может занять всё, что доступно хосту; ограничения процессора и памяти настраиваются отдельно (Docker resource constraints). Для ночной очереди это практичный инструмент: лимит на тяжёлую дорожку не даст двум пересчётам задушить базу. Но лимит защищает соседей, а не ускоряет саму дорожку: ограничение ресурса не равно обещанию, что приложение будет работать быстро.
Повторы стоят времени, а иногда и денег
В расчёте выше все задания проходят с первого раза. Ночью так не бывает: партнёр иногда отвечает ошибкой, процесс падает, таймаут срабатывает. Повтор стоит столько же, сколько первая попытка, и его надо заложить в расчёт. Допустим, по замерам 10 % заданий типа Б и 3 % типа В возвращаются в очередь. Для внешней дорожки это ещё 250 заданий по 4,5 с, то есть меньше 190 с работы на шесть окон: почти ничего. Для тяжёлой — 15 заданий по 42 с, 630 с работы, то есть ещё около пяти минут на двух работниках. По отдельности смешно, но если повтор случается на 90 % тяжёлого задания, потеряна почти вся его длительность, и самые дорогие повторы приходятся именно на тяжёлые задания.
Есть и вопрос корректности. В Celery подтверждение сообщения и завершение бизнес-операции — это разные моменты; параметр acks_late переносит подтверждение, но у него есть исключения при завершении процесса, поэтому от повторного выполнения всё равно нужна защита самой задачи (Celery: выполнение и доставка задач). Для расчёта это означает следующее: задание, которое может быть выполнено дважды, нельзя дважды учитывать, дважды списывать или дважды отправлять. Практически результат стоит записывать по ключу бизнес-операции, например «документ + версия», чтобы повтор перезаписывал ту же строку, а не добавлял вторую. Внешний вызов сложнее: если процесс упал после ответа партнёра, но до записи, повтор отправит запрос заново. Здесь нужен ключ повторной отправки, зависящий от самой операции, а не от номера попытки, либо предварительная проверка состояния на стороне партнёра. Транзакция вашей базы внешний вызов не охватывает, поэтому защита от дублей должна быть отдельной.
Один длинный хвост: полчаса из ничего
Теперь вернёмся к тяжёлой дорожке и добавим в неё одно необычное задание. Допустим, среди 500 пересчётов один документ огромный: на него уходит 55 минут, то есть 3 300 секунд. Остальные 499 задач по-прежнему по 42 с, всего 20 958 с.
Всё зависит от того, когда оно стартует. Если задание длиной 3 300 с оказалось в начале очереди и один работник занят им, второй разбирает остальные, и когда первый освободится, они делят остаток поровну. Общая работа 24 258 с на двух работников — около 12 130 с, то есть 3 ч 22 мин. Если же длинное оказалось в самом хвосте очереди, оба работника сначала разбирают 499 обычных за 20 958 ÷ 2 ≈ 10 480 с, и только потом один из них берёт длинное, которое занимает ещё 3 300 с. Финиш: 13 780 с, около 3 ч 50 мин, и всё это время второй работник простаивает. Разница между двумя порядками — 1 650 с, почти полчаса, возникшие не из-за нехватки мощности, а из-за порядка выдачи.
| Вариант | Тяжёлых работников | Длинное стартует | Окончание дорожки |
|---|---|---|---|
| Без длинного, 500 × 42 с | 2 | — | 10 500 с, 2 ч 55 мин |
| Длинное первым | 2 | в начале | ≈ 12 130 с, 3 ч 22 мин |
| Длинное последним | 2 | в конце | ≈ 13 780 с, 3 ч 50 мин |
| Длинное последним | 5 | в конце | ≈ 7 490 с, 2 ч 05 мин |
| Длинное первым | 5 | в начале | ≈ 4 850 с, 1 ч 21 мин |
Последние две строки относятся к плану на 8 vCPU: там можно оставить одно ядро системе и базе, выделить пять работников тяжёлой дорожке и два лёгкой. Обычная часть очереди ускоряется в 2,5 раза (с 10 480 до 4 190 с), а вся дорожка при невыгодном порядке — только в 1,84 раза (с 13 780 до 7 490 с): хвост длиной 3 300 с никуда не делся. Добавление работников сжимает обычную часть, но не сам длинный элемент: быстрее его выполнит только другой способ, например разделить документ на части или стартовать его первым. Выдача задач по убыванию ожидаемой длительности — дешёвый способ забрать эти полчаса даром: нужна лишь оценка длительности при постановке, например по размеру входных данных.
Практический вывод для расчёта: к обычной части тяжёлой дорожки всегда добавляйте время самого длинного задания, которое вы реально наблюдали, а не среднее. Максимум из замеров — это то, что случится хотя бы раз.
Сколько пускать в очередь к вечеру
Теперь можно ответить на вопрос, ради которого всё затевалось: сколько заданий принимать в ночную очередь. Принимаемый объём определяется не желанием, а бюджетом времени и самой узкой дорожкой.
Окно — 28 800 с. Планировать будем на 70 %: это 20 160 с, около 5 ч 36 мин. Остальное — резерв на повторы сверх оценки, замедление диска или партнёра и неожиданности. Дальше по дорожкам:
- Тяжёлая. Из бюджета вычитаем самый длинный наблюдавшийся хвост: 20 160 − 3 300 = 16 860 с. Два работника за это время обработают 2 × 16 860 ÷ 42 ≈ 800 обычных тяжёлых заданий. С учётом 3 % повторов остаётся около 780.
- Внешняя. При 1,5 задания в секунду и 10 % повторов эффективно около 1,36 задания в секунду; на 16 000 с это порядка 21 800 заданий, и дорожка упирается скорее в стабильность партнёра, чем в объём.
- Лёгкая. Ядро с коэффициентом 0,8 даёт около 16 100 процессорных секунд. Даже вместе с внешней дорожкой это десятки тысяч коротких заданий, так что граница очень далека.
Получается, что на этой машине вечерняя очередь должна ограничиваться по тяжёлым заданиям: примерно 780 штук. Всё остальное — запас. Если к 23:00 пришло 1 100 тяжёлых, одно из двух: оставить часть на следующую ночь либо перейти на план покрупнее. Для плана с 8 vCPU и пятью тяжёлыми работниками тот же расчёт даёт 5 × 16 860 ÷ 42 ≈ 2 000, а с повторами около 1 950. Среди тарифов MAATRIX это ступень Maat (8 vCPU, 16 ГБ RAM, 240 ГБ NVMe); Heka (4 vCPU, 8 ГБ, 160 ГБ) соответствует расчёту выше, а Uab (2 vCPU, 4 ГБ) подойдёт для очередей без тяжёлых заданий. Параметры плана задают только отправную точку: подтвердить, что именно ваш код на ваших данных справляется, можно лишь прогоном с замеренными ранее отметками. Машины на NVMe, AMD EPYC и с root-доступом позволяют настроить лимиты, число работников и параметры базы под такой замер; услуга оформляется быстро, поэтому пробный прогон на плане нужного класса не требует долгой подготовки.
Отдельно о том, как выбирать план. Методология AWS Well-Architected связывает оптимизацию затрат с функциональными требованиями, использованием ресурсов и согласованием спроса с мощностью (AWS Well-Architected Cost Optimization Pillar). В нашей задаче функциональное требование — срок 07:00; спрос — вечерняя очередь, мощность — число работников и ядра под них. Сопоставляя их, вы получаете ответ на вопрос «какой план нужен», а не угадываете. Если объём очереди растёт от месяца к месяцу, пересчёт придётся повторять: те же тяжёлые 780 заданий через полгода могут превратиться в 1 400.
Обещание, у которого есть основание
В начале разговор шёл про одну цифру — 2 ч 41 мин, полученную делением. В итоге получилось другое: лёгкая дорожка заканчивается примерно за полтора часа, внешняя — за полчаса при исправном партнёре, а тяжёлая определяет ночь и в зависимости от порядка выдачи заканчивается через 2 ч 55 мин, 3 ч 22 мин или 3 ч 50 мин. Все варианты умещаются в бюджет 5 ч 36 мин, но каждый из них можно обосновать по числам, а не по интуиции.
Практическая картина такая. Срок «очередь пуста к семи» можно обещать, когда известны размер очереди к началу ночи, разбивка по типам, измеренное время каждого типа и его фазы, потолки партнёра и базы, число работников на дорожках и самое долгое задание из реально наблюдавшихся. Без любой из этих величин обещание превращается в надежду. И когда в 23:00 в очередь встаёт 1 100 тяжёлых заданий вместо 780, вы заранее знаете, что делать: сдвигать часть заданий, увеличивать план или менять порядок. А утренний отчёт откроется с актуальными данными.