Overleaf-Benchmark.pdf
Аннотация
Размер самостоятельно размещённых развёртываний Overleaf обычно выбирают по одному эмпирическому правилу: одно ядро CPU и один гигабайт памяти на пять–десять одновременных пользователей. Мы показываем, что это правило не просто неточно, а структурно неверно: оно предполагает, что ёмкость определяется одним измерением ресурсов, тогда как на самом деле её определяют две независимые «стены», а два программных параметра — ни один из которых не относится к железу — влияют на результат в разы, вплоть до четырёхкратной разницы. Мы измеряем стандартное развёртывание Ayakaleaf Pro v6.2.2 с изолированной компиляцией (TeX Live 2025) в 21 конфигурации CPU/памяти внутри гостевых машин QEMU/KVM, ядра хоста которых зафиксированы на частоте 3,0 ГГц. Рабочая нагрузка — реальная 63-страничная диссертация на XeLaTeX, которую одновременно компилируют до нескольких сотен разных учётных записей пользователей. Мы обнаружили, что при объёме гостевой памяти ниже 32 ГиБ количество ядер почти не имеет значения — при 16 ГиБ измеренная ёмкость гостевых машин с 4, 8 и 16 vCPU различается менее чем на 8%, — а ёмкость вместо этого определяется сверхлинейной «стеной памяти», возникающей из-за общего страничного кэша над деревом TeX Live. Чтобы проверить, сохраняются ли эти закономерности при изменении масштаба на порядок, мы повторяем серию измерений на одном сервере с 64 ядрами и 995 ГиБ памяти. Он выдерживает 1024 одновременные холодные компиляции со 100% успехом — в восемь раз больше числа его потоков, — и мы так и не достигаем его предела. Полезна не эта граница, а «колено» ниже неё: хвостовая задержка растёт на 20–40% при каждом удвоении вплоть до , а затем на 190% при . Поэтому ёмкость, указанная как «наибольший уровень параллелизма без сбоев», завысила бы реально применимую рабочую точку в четыре раза. На этой машине память никогда не является ограничивающим ресурсом; ограничением служит CPU вместе со скоростью, с которой демон контейнеров может запускать новые песочницы, — она насыщается около 200 независимо от того, сколько компиляций запрошено. Кроме того, мы выявляем два эффекта уровня реализации, невидимых при планировании ёмкости. Во-первых, CLSI применяет жёстко заданный в коде предел в 65 одновременных компиляций, который нельзя изменить никакой переменной окружения; сверх него пользователи сразу получают HTTP 503 вместо постановки в очередь. Во-вторых, ограничение памяти на контейнер в Docker runner неэффективно с момента его появления в 2018 году — как по величине, так и по месту применения, — поэтому нехватка памяти выводит из строя весь хост, а не одну компиляцию. Снятие ограничения параллелизма и увеличение тайм-аута компиляции по умолчанию со 180 до 300 с повышает измеренную ёмкость гостевой машины с 8 vCPU / 48 ГиБ с 64 до 268 одновременных компиляций — в 4,2 раза без каких-либо затрат на оборудование. Наконец, мы показываем, что параллелизм в этой системе не даёт ничего, кроме разделения времени, а рабочая нагрузка ограничена исключительно тактовой частотой. Подобранный закон деградации даёт , что близко к идеальному пропорциональному замедлению, а серия измерений по всему диапазону частот машины 1,0–5,5 ГГц сводит тридцать измерений к с и остаточным разбросом 5,1%. Пятикратное с половиной увеличение частоты даёт ускорение в 5,5 раза без убывающей отдачи — именно в этом смысле частота и ядра дают разное: частота ускоряет компиляцию каждого пользователя, а ядра лишь позволяют обслуживать больше пользователей.1. Введение
Overleaf — доминирующий редактор LaTeX для совместной работы, а его локальный дистрибутив широко используется университетами и исследовательскими группами, которые не могут отправлять неопубликованные рукописи в стороннее облако. Выбор размера такого развёртывания — постоянно возникающий практический вопрос: сколько человек при фиксированном бюджете на оборудование могут на самом деле одновременно нажать «Recompile»? Официальная рекомендация — линейное правило: примерно одно ядро и один гигабайт на пять–десять одновременных пользователей, что предполагает плавное и совместное масштабирование ёмкости по обоим ресурсам. Наши измерения опровергают это тремя способами.1.1 Ёмкость определяется двумя независимыми стенами, а не одной
Конфигурация отказывает либо из-за исчерпания памяти — в этом случае сам стек Overleaf падает и возвращает HTTP 502, — либо из-за того, что компиляции превышают серверный тайм-аут, — в этом случае CLSI сообщаетtimedout, хотя гигабайты памяти остаются незанятыми. Эти два режима имеют совершенно разное поведение при масштабировании и разные способы исправления. Добавление ядер в конфигурацию, ограниченную памятью, не просто неэффективно, а иногда даже вредно: мы наблюдаем конфигурации, где увеличение числа ядер снижает ёмкость, потому что с большим количеством ядер параллельные компиляции продвигаются синхронно и их пиковые потребности в памяти совпадают, а не чередуются.
1.2 Программные параметры важнее оборудования
Тайм-аут компиляции — это поле пользователя в MongoDB, значение которого по умолчанию, 180 с, незаметно ограничивает конфигурации, упирающиеся в CPU. Увеличение его до 300 с умножает измеренную ёмкость до 4,2 раза на том же оборудовании. Независимо от этого CLSI отказывает более чем 65 одновременным компиляциям из-за жёстко заданной в коде константы. Любое исследование ёмкости — и любое развёртывание, — которое не учитывает оба фактора, измеряет программное обеспечение, а не машину.1.3 Параллелизм — это разделение времени, а не параллельное выполнение
Поскольку компиляция LaTeX однопоточна, обслуживание одновременных пользователей на ядрах не заставляет систему завершить работу быстрее; каждый пользователь просто ждёт пропорционально дольше. Поэтому вопрос «сколько одновременных пользователей поддерживается» поставлен некорректно, пока не зафиксировано, сколько пользователь готов ждать. Мы делаем эту зависимость явной и количественно её оцениваем.1.4 Вклад работы
- Матрица ёмкости по 21 конфигурации CPU/памяти, измеренная в условиях фиксированной частоты с повторной проверкой, где для каждой конфигурации ограничивающий фактор определяется по сигнатуре отказа.
- Две подобранные модели: модель ёмкости, разделяющая сверхлинейную стену памяти и потолок CPU, и модель задержки, устанавливающая поведение чистого разделения времени.
- Выявление и экспериментальное подтверждение двух проблем реализации в развёрнутой системе, включая ограничение памяти контейнера, которое не работает с 2018 года.
- Количественная оценка компромисса между тайм-аутом компиляции и ёмкостью, который, как мы утверждаем, необходимо указывать вместе с любым показателем параллелизма.
2. Предпосылки
2.1 Путь компиляции
Запрос на компиляцию в Overleaf проходит путьweb clsi контейнер компиляции. В развёртывании с изолированной компиляцией (SIBLING_CONTAINERS_ENABLED=true) CLSI не запускает latexmk внутри своего процесса; он просит Docker-демон хоста, доступный через смонтированный (bind mount) сокет, запустить новый контейнер из образа TeX Live с каталогом проекта, смонтированным в /compile. Таким образом, одна компиляция — это один короткоживущий контейнер, в котором выполняется один процесс latexmk.
Отсюда следуют три вывода, и все три определяют измерения в этой работе. Во-первых, единица работы — однопоточный процесс: XeLaTeX не распараллеливается. Во-вторых, изоляция ресурсов для каждой компиляции определяется тем, что запрашивает Docker runner, — в §6.2 мы показываем, что фактически он не запрашивает ничего. В-третьих, рабочий набор определяется не документом, а деревом TeX Live — доступным только для чтения корпусом объёмом примерно 32 ГиБ, из которого читает каждая параллельная компиляция и который, следовательно, разделяется через страничный кэш хоста. Это разделение и является источником наблюдаемого сверхлинейного масштабирования памяти.
2.2 Включение изолированной компиляции
Overleaf Community Edition запускаетlatexmk внутри самого контейнера приложения. Ayakaleaf Pro, как и Overleaf Server Pro, может вместо этого выполнять каждую компиляцию в соседнем (sibling) контейнере — контейнере, который приложение запускает через Docker-демон хоста, а не вложенным в контейнер приложения. Это включается двумя настройками Toolkit:
SANDBOXED_COMPILES=true, SANDBOXED_COMPILES_SIBLING_CONTAINERS=true и SANDBOXED_COMPILES_HOST_DIR; последняя содержит путь к каталогу компиляции на хосте. Этот путь важен: поскольку контейнер компиляции запускает демон хоста, переданный ему bind mount должен разрешаться в пространстве имён хоста, а не контейнера приложения. Кроме того, config/env.sh в Server Pro в этом режиме принудительно задаёт TEXLIVE_IMAGE_USER=www-data, чтобы файлы, записанные контейнером компиляции, имели единообразного владельца.
Проверка проста: во время компиляции на хосте виден контейнер с именем project-{projectId}-{userId}-{hash}, выполняющий latexmk из образа TeX Live и завершающийся с кодом 0. Именно количество таких единиц мы измеряем на протяжении всей работы, и именно для них в §6.2 мы сообщаем о полном отсутствии ограничений ресурсов.
Соседние контейнеры делают измерения чистыми — каждая компиляция является наблюдаемой, независимо планируемой сущностью ОС, — но это также означает, что CPU и память между компиляциями распределяет гостевое ядро, а не Overleaf. Поэтому каждый закон масштабирования в этой работе — это свойство планировщика Linux, применённого к однопоточным процессам, и именно поэтому он так регулярен.

clsi. Ни то, ни другое не доминирует — стоимость компиляции проекта определяется деревом TeX Live объёмом 32 ГиБ, которое каждая параллельная компиляция читает через общий страничный кэш.

git-bridge. Конфигурация Toolkit по умолчанию (b), которую мы и измеряем, имеет множитель, равный единице, поэтому константа, рассчитанная на одного участника парка, становится пределом всей установки.

clsi-cache. Проект отображается по формуле , то есть пространство хешей делится на столько равных секторов, сколько существует шардов. Это хеширование по модулю, а не консистентное хеширование на основе кольца: увеличение парка с трёх шардов до четырёх перераспределяет всё пространство и переназначает практически каждый проект (a, b). Именно поэтому реализации требуется явный механизм постепенного онлайн-решардинга, перемещающий линейно растущую долю проектов из currentShards в desiredShards в течение временного окна, вместо перемещения , которое дало бы кольцо консистентного хеширования. Когда срабатывает автоматический выключатель (circuit breaker) шарда, соль увеличивается, а шард удаляется из списка кандидатов, поэтому поиск переходит к следующему, а не завершается ошибкой (c).
2.3 Два режима отказа
Каждая измеренная нами конфигурация отказывает ровно одним из двух способов, и это различие видно по статусу ответа, а не выводится косвенно:- Исчерпание памяти — сам стек Overleaf перестаёт отвечать, и запрос возвращает HTTP 502. Доступная гостевая память на уровне отказа обычно ниже 500 МиБ.
- Тайм-аут компиляции — CLSI прерывает компиляцию по достижении тайм-аута пользователя и сообщает статус
timedout. Доступная память на уровне отказа часто составляет несколько гигабайт.
3. Методология
3.1 Испытательный стенд и управление частотой
Все гостевые машины работают под QEMU/KVM на одном хосте Intel Core i9-14900K с 62 ГиБ ОЗУ и NVMe-накопителем. Гостевая ОС — Ubuntu 24.04 с Docker 29.7 и Overleaf Toolkit, развёртывающим Ayakaleaf Pro v6.2.2 с изолированной компиляцией на базеtexlive-full:2025.1.
Потребительский настольный CPU плохо заменяет сервер, если не контролировать его частоту. KVM не предоставляет механизма для задания виртуальной частоты: vCPU — это поток хоста, и он работает на той частоте, на которой работает ядро хоста. Поэтому мы ограничиваем непосредственно хост: отключаем turbo и фиксируем scaling_max_freq на 3,0 ГГц для каждого ядра, а vCPU гостевой машины привязываем к физическим P-ядрам с помощью taskset. Это различие важно для CPU с гибридной архитектурой: E-ядра этой модели имеют базовую частоту 2,4 ГГц и не могут достичь 3,0 ГГц при отключённом turbo, поэтому прогон, случайно попавший на них, незаметно измеряет более медленную машину. Под полной нагрузкой мы проверяем ровно 3000 МГц на всех шестнадцати привязанных потоках. Контрольный скрипт проверяет этот инвариант перед каждым тестом и в противном случае отказывается запускаться; за время исследования он обнаружил один незаметный сброс регулятора частоты (governor).
3.2 Второй стенд: один крупный сервер
Матрица QEMU изолирует по одной переменной за раз, но ограничена шестнадцатью привязанными потоками. Чтобы проверить, выполняются ли те же закономерности на порядок выше, мы повторили серию измерений параллелизма на одном крупном сервере: один AMD EPYC 7773X (Milan-X, 64 ядра / 128 потоков, 768 МиБ L3) с 995 ГиБ ОЗУ, с тем же образом Ayakaleaf Pro v6.2.2 и тем жеtexlive-full:2025.1. В отличие от гостевых машин QEMU, частота этой машины не зафиксирована: это сервер производственного класса, и мы измеряем его именно так.
Потребовались две эксплуатационные меры предосторожности, и о них стоит упомянуть, потому что без них эксперимент измеряет тестовую обвязку, а не сервер. Во-первых, каждый контейнер был ограничен слайсом systemd с MemoryMax=940 GiB, чтобы вышедшая из-под контроля серия исчерпала cgroup, а не хост. Во-вторых, изолированные компиляции создаются демоном хоста, и каждая загрязняет собственный слой copy-on-write — измеренный объём составил 116 МиБ на контейнер, хотя базовый образ размером 20,6 ГиБ общий, — поэтому корневой каталог данных Docker был перенесён на выделенное NVMe-устройство. Серия при записывает примерно 119 ГиБ временных слоёв, что не помещается на стандартную корневую файловую систему.
3.3 Рабочая нагрузка
Документ — реальная 63-страничная магистерская диссертация (шаблон SJTU), компилируемая XeLaTeX черезlatexmk, с рисунками TikZ, обработкой библиографии biblatex и встроенными PDF-ресурсами, — то есть реалистичная, а не синтетическая нагрузка. Одна компиляция на ненагруженной гостевой машине занимает 8,6–9,8 с во всех конфигурациях; это значение мы используем как базовое время без нагрузки .
3.4 Генерация нагрузки
Мы создаём 512 реальных учётных записей пользователей и даём каждой собственную копию проекта, чтобы параллельные компиляции конкурировали так же, как конкурировали бы независимые пользователи, а не делили блокировку одного проекта. Запросы отправляются с хоста на проброшенный порт гостевой машины, поэтому генерация нагрузки не расходует CPU гостевой машины. Параллелизм одновременный, а не ступенчатый. Сначала устанавливается каждый сеанс — вход, токен CSRF, выбор компилятора, — и только затем каждый поток засыпает до общего момента по настенным часам, вычисленного один раз и общего для всех, после чего отправляет свойPOST /project/:id/compile. Это различие не формальность. Ступенчатое нарастание измеряет пропускную способность при стабильной очереди; одновременный всплеск измеряет то, что происходит, когда целая лекционная аудитория студентов нажимает одну и ту же кнопку после одного и того же объявления о дедлайне, — а именно этого случая на самом деле опасаются администраторы. Эти два сценария различаются больше чем на постоянный множитель, потому что во втором очередь компиляции заполняется быстрее, чем демон успевает её разгрести.
Прежде чем удалось достоверно воспроизвести такой всплеск, пришлось устранить четыре практических препятствия. Каждое из них стоит зафиксировать, потому что каждое незаметно превращает эксперимент в измерение тестовой обвязки, а не сервера.
3.4.1 Два ограничителя частоты запросов, а не один
Overleaf ограничивает количество входов с одного адреса источника — 20 попыток в минуту, — а весь наш трафик исходит с одного хоста. Назначение каждому имитируемому пользователю отдельного адресаX-Forwarded-For снимает это ограничение, но тут же упирается во второе, более грубое: лимит на подсеть примерно 200 в минуту. Поэтому распределение пользователей по непрерывному блоку адресов отказывает на 201-й учётной записи. Вместо этого мы выводим синтетический адрес из индекса пользователя так, чтобы последовательные пользователи попадали в разные /24,
что оставляет запас по обоим ограничителям для всей совокупности из 1024 пользователей.
3.4.2 Внедрённый заголовок по умолчанию отбрасывается
Задать заголовок недостаточно. Express учитываетX-Forwarded-For только от узлов, которым ему велено доверять, а trustedProxyIps в Overleaf по умолчанию равен loopback. Поскольку генератор нагрузки обращается к приложению через мост контейнера, а не через интерфейс loopback, заголовок разбирается и затем отбрасывается, и все имитируемые пользователи снова сводятся к одному адресу. Симптом — волна HTTP 429 ровно на двадцатом входе, которую легко ошибочно принять за перегрузку сервера. Сеть шлюза нужно явно добавить в цепочку доверия; в кластерном развёртывании из §4.3 необходимо также добавить CIDR подов и сервисов.
3.4.3 Балансировщик нагрузки перезапишет заголовок, который его просили сохранить
Когда экземпляр находится за прокси, стандартная директиваoption forwardfor добавляет реальный адрес клиента в цепочку — это правильное поведение для production, но здесь оно как раз неверно: синтетический адрес вытесняется собственным адресом генератора нагрузки. Директиву нужно уточнить как option forwardfor if-none, чтобы прокси добавлял значение только тогда, когда клиент его не передал.
3.4.4 Клиенту не хватает файловых дескрипторов раньше, чем серверу — ёмкости
При генератор держит более тысячи одновременных сокетов, и стандартный мягкий лимит в 1024 дескриптора достигается во время установки сеансов, а не во время измерения. Отказ происходит тихо: три сеанса не устанавливаются, и прогон сообщает 1021 вместо 1024, а поток сбора данных, который вызывает внешнюю команду для подсчёта контейнеров, падает сEMFILE и незаметно обрезает телеметрию. Мягкий лимит на генераторе необходимо повысить — жёсткий лимит на нашем хосте уже составлял 1048576 — и повторить прогон. Мы приводим оба прогона в §4.3: исправленный завершает 1024 из 1024 с медианой, отличающейся от обрезанного не более чем на 1,2 с, поэтому первый мы считаем пригодным, но не авторитетным.
3.5 Протокол измерений
Для воспроизводимости потребовалось несколько методологических решений.3.5.1 Прогрев
На только что загруженной гостевой машине страничный кэш пуст, и первые компиляции измеряют ввод-вывод холодного старта, а не установившуюся ёмкость: одна и та же конфигурация 2 vCPU / 2 ГиБ даёт 36,5 с в холодном состоянии и 9,8 с в прогретом — разница в 3,7 раза. Поэтому для каждой конфигурации после загрузки выполняются два отбрасываемых прогревочных прогона с одной компиляцией.3.5.2 Критерий прохождения
Уровень параллелизма считается пройденным, только если каждая компиляция успешна и уровень выдерживает повторный прогон. Это строже, чем порог доли успешных выполнений, и это важно: при 4 vCPU / 16 ГиБ уровень 32 один раз прошёл с медианой 80,2 с, а при повторе все 32 компиляции завершились по тайм-ауту, поэтому мы указываем 31.3.5.3 Поиск
Уровни находятся экспоненциальным поиском границ от начального значения, предсказанного моделью, с последующей точной целочисленной бисекцией. Поскольку критерий работает по принципу «всё или ничего», уровень определяется первым же отказом, поэтому после первого отказа мы прекращаем оставшиеся выполняющиеся запросы — кроме малых уровней, где брошенные компиляции настолько загружают небольшую гостевую машину, что она уже не восстанавливается.3.5.4 Изоляция между уровнями
Перед началом следующего уровня контейнеры компиляции завершаются, а веб-приложение опрашивается, пока снова не начнёт отвечать. Без этого уровень, следующий за сбоем, фиксирует ложный отказ с нулём сеансов.3.5.5 Гигиена хоста
Посторонние виртуальные машины на хосте были выключены: когда 24 ГиБ памяти хоста были заняты другими задачами, та же конфигурация гостевой машины показывала среднюю нагрузку 11,7 вместо 3,2 при одинаковом параллелизме. Нехватка памяти на хосте распространяется на гостевую машину и делает измерение недействительным.4. Результаты
4.1 Матрица ёмкости
В таблице 1 и на рисунке 4 приведён измеренный предел для каждой конфигурации. Чтение по строке приносит первый сюрприз. При 4 ГиБ гостевые машины с 2, 4 и 8 vCPU достигают ровно 9 — четырёхкратное увеличение числа ядер вообще ничего не меняет. При 16 ГиБ они достигают 54, 45 и 57: переход с 4 на 16 ядер даёт 6%, а 8-ядерная гостевая машина на деле хуже 4-ядерной (§5.2). Лишь при 48 ГиБ количество ядер решительно разделяет конфигурации: 143, 268 и 331.
Таблица 1. Максимальное число одновременных компиляций, завершающихся успешно, измеренное при тайм-ауте компиляции 300 с и снятом ограничении параллелизма CLSI. Жирным отмечены конфигурации, ограниченные CPU (компиляции завершаются по тайм-ауту при свободной памяти); остальные ограничены памятью (стек падает с HTTP 502). Строка 2 GiB содержит поправку, обсуждаемую в §5.2.
4.2 Параллелизм — это разделение времени
На рисунке 5 показана серия по всем уровням параллелизма на фиксированной гостевой машине с 8 vCPU / 16 ГиБ. Два режима разделены резким коленом ровно на одной компиляции на ядро. Ниже него среднее время компиляции постоянно — оно меняется с 8,7 с при до 9,1 с при , то есть на 5%. Выше него время растёт строго пропорционально : при мы измеряем 18,5 с и 27,1 с, то есть соотношение против идеального .

4.3 Вертикальное масштабирование до 1024 одновременных компиляций
В таблице 2 и на рисунке 7 приведены результаты серии на крупном сервере. Каждый уровень — холодная компиляция: перед каждым уровнем мы очищаем каталог компиляции и кэш CLSI каждого участвующего проекта черезDELETE /project/:id/output, поэтому ни один уровень не получает выгоды от работы, выполненной на предыдущем. Базовое время одной компиляции на этой машине — 28,8 с; это холодное значение, и его не следует сравнивать с использованным ранее установившимся базовым временем 8,6–9,8 с. Холодное базовое время на гостевых машинах QEMU составляет 28,3 с, так что в пересчёте на поток эти две машины отличаются для данной нагрузки не более чем на два процента.
Таблица 2. Серия измерений параллелизма на одном EPYC 7773X (64 ядра / 128 потоков, 995 ГиБ). Все уровни холодные; базовое время 28,8 с. Пик контейнеров — максимальное число одновременно существующих песочниц.

4.3.1 Машина никогда не отказывает
Каждый уровень завершается на 100%, включая — в восемь раз больше числа потоков. Мы так и не нашли предел ёмкости этой машины: у нас закончилось терпение раньше, чем у неё — запас. Это первая конфигурация в исследовании, где ограничивающим фактором является не память: при пиковое потребление cgroup компиляции составляет 184 ГиБ — пятую часть лимита в 940 ГиБ, — тогда как CPU загружен на 100% при средней нагрузке 166.4.3.2 Деградация сублинейна, потому что допуск ограничен по скорости
Наивная модель разделения времени предсказывает, что потоков стоят задержки. Измеренная стоимость составляет относительно одной компиляции, но лишь относительно — при восьмикратном росте подаваемой нагрузки. Причина видна на рисунке 7(b) и в последнем столбце таблицы 2: хотя 1024 запроса отправляются одновременно, число реально существующих песочниц никогда не превышает 205. Демон не может создавать контейнеры так быстро, как их запрашивают клиенты, поэтому запросы встают в очередь на этапе допуска, а не конкурируют внутри CPU. Именно очередь спасает здесь хвост распределения, причём случайно.4.3.3 Колено находится на 512, а не в точке отказа
Между и задержка растёт в раза при удвоении нагрузки; каждое предыдущее удвоение стоило от до . Ёмкость, выраженная как «наибольшее без отказов», составила бы 1024 и была бы бесполезна для администратора: в этой точке хвостовое ожидание составляет почти восемь минут.4.4 Время компиляции обратно пропорционально тактовой частоте
Поскольку нагрузка ограничена CPU, её стоимость должна масштабироваться как . Мы проверяем это напрямую, изменяя частоту хоста во всём диапазоне машины — 1,0–5,5 ГГц за десять шагов — на гостевой машине без прочих изменений (рисунок 8). Время одной компиляции меняется с 26,5 с до 4,8 с: увеличение частоты в 5,5 раза даёт ускорение в 5,5 раза без убывающей отдачи во всём диапазоне. Произведение постоянно с точностью до 2% на всех десяти частотах. Нормировка на долю ядра сводит все тридцать измерений — три уровня параллелизма на десяти частотах — к единой константе: с остаточным разбросом 5,1% в диапазоне, где сама частота меняется в 5,5 раза. Отсутствие какой-либо кривизны — само по себе результат: если бы нагрузка была ограничена пропускной способностью памяти или вводом-выводом, выходило бы на плато при высокой частоте, когда CPU обгонял бы другой ресурс.
5. Анализ
5.1 Две стены, подобранные по отдельности
Каждая конфигурация классифицируется по сигнатуре отказа (§2.3), после чего стена памяти и потолок CPU подбираются только по тем конфигурациям, которые в них действительно упираются: где — в гибибайтах, а — в vCPU. Показатель стены памяти неизменно сверхлинеен, : предельные затраты памяти на одну дополнительную параллельную компиляцию снижаются по мере роста общего объёма памяти — примерно с 312 МиБ на компиляцию на гостевой машине с 3 ГиБ до примерно 194 МиБ на машине с 32 ГиБ. Механизм — общий страничный кэш над деревом TeX Live, описанный в §2.1: параллельные компиляции читают пересекающиеся файлы шрифтов и макросов, поэтому больший кэш амортизируется на большее их число. Именно поэтому наивное правило «один гигабайт на пять пользователей» занижает оценку для больших машин и завышает для маленьких.
5.2 Где больше ядер ухудшает ситуацию
Уравнение (2) — минимум двух членов и, следовательно, монотонно по , но измерения — нет. Мы наблюдаем две инверсии, при которых добавление ядер снижало ёмкость: при 16 ГиБ (54 против 45) и при 32 ГиБ (145 против 135). Обе происходят в режиме, ограниченном памятью, и механизм в обоих случаях один: при большем числе ядер параллельные компиляции продвигаются синхронно и достигают пикового резидентного размера в один и тот же момент, тогда как при меньшем числе ядер планировщик чередует их, и пики разнесены во времени. На гостевой машине с и без того минимальным запасом памяти именно такое разнесение сохраняет её работоспособность. Модель ёмкости, построенная на среднем использовании ресурсов, не может это выразить: это свойство совпадения пиков. Третью кажущуюся инверсию, при 2 ГиБ, мы теперь не учитываем. Поиск фиксирует ёмкость 2 при 2 vCPU, но 1 при 4 и 8 vCPU, что выглядит как тот же эффект. Повторный анализ исходных серий показывает нечто более простое: при 2 ГиБ уровень прошёл с первой попытки при всех трёх количествах ядер, а затем не прошёл подтверждающий прогон в двух случаях из трёх. Этот уровень — не ёмкость, а подбрасывание монетки, и значение для 2 vCPU — просто удачный бросок. Поэтому для всех трёх количеств ядер мы указываем воспроизводимое значение 1 и не делаем выводов из этой разницы. Мы фиксируем поправку здесь, а не молча исправляем таблицу, потому что отброшенный результат — как раз из тех, что подкрепили бы интересное утверждение.6. Находки на уровне реализации
6.1 Жёстко заданный предел параллелизма
На достаточно больших гостевых машинах ёмкость останавливалась ровно на 65 одновременных компиляциях независимо от запрошенного параллелизма: при мы получили успешных выполнений и немедленных ответовunavailable, при этом число контейнеров держалось на 65, несколько гигабайт памяти оставались неиспользованными, а медианное время компиляции стабильно составляло 77 с — гораздо меньше любого тайм-аута.
Причина — константа в CLSI:
success=65, unavailable=15, стала сообщать success=80.
6.2 Неработающее ограничение памяти контейнера
Проверка работающего контейнера компиляции показывает полное отсутствие изоляции ресурсов:HostConfig, где его ожидает Docker API, поэтому оно отбрасывается, что подтверждает наблюдаемое Memory=0. Обе ошибки присутствуют уже в коммите, который добавил этот файл (9a519f0d3d, март 2018 г.), и пережили конвертацию из CoffeeScript, переформатирование всего репозитория и миграцию с CJS на ESM — ни одно из этих изменений не затрагивало семантику. Примечательно, что MAX_OUTPUT = 1024 * 1024 // 1MB в том же коммите указан правильно, что говорит об описке, а не о непонимании.
Последствия видны в наших измерениях при малом объёме памяти. Поскольку компиляции ничем не ограничены, исчерпание памяти проявляется не в том, что Docker завершает один проблемный контейнер, — оно выводит из строя всю гостевую машину. В конфигурации 2 vCPU / 2 ГиБ мы наблюдали блокировку контрольного SSH-сеанса на 300 с, среднюю нагрузку 68 на двух ядрах и в итоге самопроизвольную перезагрузку гостевой машины. Работающее ограничение на контейнер приводило бы к гораздо более мягкой деградации: слишком большая компиляция завершилась бы ошибкой, а сервис продолжил бы работу.
Единственное ограничение, которое действительно действует, — RLIMIT_CPU, равное секундам. Оно ограничивает время CPU, а не время по настенным часам, а одна компиляция потребляет лишь около 9 с CPU, поэтому оно никогда не срабатывает ни при каком параллелизме; оно защищает от патологических входных данных, например от зациклившегося макроса. Тем не менее оно полезно как индикатор: наблюдение Soft:305 подтверждает, что настройка тайм-аута 300 с действительно дошла до контейнера.
6.3 Тайм-аут компиляции — главный настраиваемый параметр
Поле пользователяfeatures.compileTimeout по умолчанию равно 180 с. Для любой конфигурации, ограниченной CPU, это не запас прочности, а параметр ёмкости, потому что машина, которая всё ещё корректно вычисляет, объявляется отказавшей. Увеличение до 300 с — одно обновление в MongoDB — меняет измеренную ёмкость вплоть до 4,2 раза (таблица 3). Верхний предел — 600 с, он задаётся RequestParser.MAX_TIMEOUT; значения выше молча усекаются.
Таблица 3. Влияние тайм-аута компиляции на измеренную ёмкость.
Последние две строки — контринтуитивная половина результата и причина, по которой мы заново измерили каждую конфигурацию при одном тайм-ауте. Для конфигураций, ограниченных памятью, более длинный тайм-аут снижает ёмкость, потому что каждая компиляция дольше удерживает свой резидентный набор и большее их число перекрывается. Поэтому показатель ёмкости бессмыслен без указания тайм-аута, при котором он измерен, и смешивать их в одной таблице нельзя.
7. Связанные работы
7.1 Рекомендации разработчика
Собственная документация Overleaf по оборудованию констатирует качественные факты, которые мы здесь количественно оцениваем: LaTeX однопоточен, поэтому время компиляции определяется производительностью одного ядра, и «больше ядер поможет, только если вы пытаетесь компилировать больше документов, чем у вас свободных ядер CPU» [1]. Далее там приводится линейное правило выбора размера — база из 2 ядер/3 ГиБ плюс одно ядро и один гигабайт на пять–десять одновременных пользователей, — которое и послужило поводом для этого исследования. Наш вклад — превратить эти утверждения в измеренные закономерности (уравнения (1) и (2)) и показать, где линейное правило перестаёт работать: в нём нет члена для общего страничного кэша, делающего стену памяти сверхлинейной, и нет членов для двух программных параметров, определяющих результат.7.2 Исследования ёмкости сборки и CI
Измерение систем сборки при параллельной нагрузке — хорошо проработанная область за пределами LaTeX. LightSys сообщает, что традиционные системы CI, выполняющие компиляцию внутри контейнеров Docker, деградируют по вводу-выводу с ростом частоты поступления pull request’ов, а узкое место появляется примерно при одиннадцати одновременных запросах [17]; TAOS-CI отмечает, что компиляция доминирует во времени CI по настенным часам, составляя 60–67% общей длительности конвейера на крупных проектах [18]. Наша система отличается в одном отношении, которое оказывается решающим: компиляция LaTeX интерактивна. Задание CI, выполняющееся вдвое дольше, — лишь неудобство; компиляцию, выполняющуюся вдвое дольше, непосредственно ощущает пользователь, ожидающий у панели предварительного просмотра, — поэтому мы рассматриваем тайм-аут не как порог отказа, а как параметр ёмкости.7.3 Накладные расходы контейнеров
В недавних работах задержка запуска контейнеров Docker раскладывается по уровням хранилища [19] и характеризуется производительность контейнеров на периферии (edge) [20]. В нашем случае запуск контейнера для каждой компиляции амортизируется: это небольшая константа по сравнению с 9-секундной компиляцией, и подбираемое нами время без нагрузки её поглощает. Значение имеет другое свойство контейнеров — отсутствие ограничений ресурсов (§6.2), из-за которого превышение памяти одной компиляцией превращается в отказ всего хоста.7.4 LaTeX как недоверенные входные данные
Изолированная компиляция существует потому, что TeX — язык программирования, а документы являются недоверенными входными данными [21, 22]. Именно это проектное решение делает возможным данное исследование — каждая компиляция представляет собой изолированный контейнер с наблюдаемым поведением ресурсов, — и именно оно делает отсутствие ограничения памяти значимым, поскольку администраторы, развёртывающие систему, рассчитывают на изоляцию.7.5 Компилятор как объект исследования
TeX как язык хорошо задокументирован [16], но его поведение как цели сборки привлекло внимание лишь недавно. Тан и Риггер [8] компилируют большой корпус исходников arXiv на разных движках и версиях дистрибутива и обнаруживают, что движки не взаимозаменяемы: лишь доли процента документов дают побайтно идентичный результат в XeTeX и pdfTeX. Этот результат напрямую касается нашей методологии. Ёмкость — свойство документа и движка, поэтому тест, не фиксирующий и то и другое, невоспроизводим; поэтому мы на протяжении всего исследования фиксируем один документ, один движок и один дистрибутив (texlive-full:2025.1) и указываем движок в подписи к каждому рисунку. Это также ограничивает общность наших чисел, о чём стоит сказать прямо: они характеризуют XeLaTeX на этом документе, а не TeX вообще.
Работы о системах сборки LaTeX в основном ведутся практиками. l3build [13] проекта LaTeX3 стандартизирует регрессионное тестирование и упаковку, а независимые тесты сравнивают инструменты-обёртки — обзор 26 систем сборки показывает, что предкомпилированная преамбула даёт выигрыш примерно в 20% по сравнению с обычным запуском и в 40% по сравнению с latexmk [14]. Они оптимизируют одну компиляцию. Это ортогонально тому, что измеряем мы, и сочетается с этим: кэш преамбулы сокращает , а каждый показатель ёмкости в этой работе масштабируется вместе с .
7.6 Управление параллелизмом в редакторе, а не в компиляторе
Часть Overleaf, отвечающая за совместную работу, опирается на хорошо проработанное направление исследований. Операционное преобразование (operational transformation) восходит к Эллису и Гиббсу [9] и было сделано практичным для клиентов с высокой задержкой в системе Jupiter [10], архитектура которой узнаётся вdocument-updater: сервер, упорядочивающий операции, и буфер для каждого документа, с которым синхронизируются клиенты. Бесконфликтные реплицируемые типы данных (CRDT) [11] решают ту же задачу без центрального упорядочивателя. Именно это различие вообще делает возможной топологию из §4.3: поскольку буфер ожидающих обновлений находится в общем Redis, а не в памяти экземпляра, компиляция, направленная на любую реплику, видит последние нажатия клавиш, и привязку компиляций можно выбирать из соображений локальности кэша, а не корректности.
7.7 Модели ёмкости
Закон Амдала [24] ограничивает ускорение от параллелизма, а закон Литтла [23] связывает заполненность с интенсивностью поступления и временем обслуживания; оба использованы выше. Универсальный закон масштабируемости Гюнтера [12] расширяет первый членом регрессии, связанным с задержкой согласованности (coherency delay), и предсказывает, что пропускная способность достигает пика, а затем снижается. Отметим, что наша система не демонстрирует этого регрессивного режима вплоть до : пропускная способность насыщается, задержка растёт, но ничего не обрушивается. Причина структурная, а не случайная: компиляции не разделяют никакого состояния, которое нужно поддерживать согласованным, поэтому добавляемый законом член близок к нулю, а плато допуска из §4.3 ограничивает конкуренцию раньше, чем она успевает стать значимой.8. Рекомендации для администраторов
1
Исправьте два программных параметра до покупки оборудования
Оба изменения бесплатны, и каждое из них ценнее любого отдельного апгрейда оборудования, который мы измеряли. Увеличьте
features.compileTimeout до значения, которое ваши пользователи действительно готовы терпеть, — максимум, принимаемый CLSI, составляет 600 с, — и, если вы ожидаете более 65 одновременных компиляций, либо снимите ограничение compileConcurrencyLimit в производном образе, либо масштабируйтесь горизонтально. Если не сделать ни того, ни другого, вы будете платить за ядра, которые программа отказывается использовать.2
Определяйте размер одной машины по колену, а не по потолку
Серия на крупном сервере (§4.3) разделяет два числа, которые обычно смешивают. Потолок — наибольший параллелизм, при котором ещё возвращается каждый PDF, — составляет не менее 1024 на 64-ядерном сервере, и мы его так и не достигли. Колено — точка, за которой хвостовая задержка перестаёт расти плавно и начинает удваиваться, — находится на 512, а последняя комфортная рабочая точка ниже неё — 256. Между и ожидание растёт с двух минут почти до шести; между 512 и 1024 оно достигает восьми. Администратор, рассчитывающий размер по потолку, получает систему, которая технически работает, но которой никто не хочет пользоваться.Поэтому для этой машины и этого документа рекомендуемая рабочая точка — 256 одновременных компиляций, что в больше числа физических ядер и в больше числа потоков и удерживает около 120 с. Мы предлагаем установить
compileConcurrencyLimit на это значение, а не оставлять его высоким: допуск 1024 компиляций одновременно заставляет всех ждать восемь минут, тогда как допуск 256 с постановкой остальных в очередь обслуживает большинство пользователей за две. Очередь ухудшает положение опоздавших; конкуренция ухудшает положение всех.3
Рассматривайте эти числа как наихудший случай
Каждый уровень в таблице 2 — холодная компиляция, запущенная одновременно. В production не выполняется ни одно из этих условий: прогретая компиляция того же документа занимает 8,6 с против 28,3 с в холодном состоянии — разница в раза, — а реальные пользователи не нажимают кнопку в одну и ту же секунду. Поэтому установившаяся совокупность пользователей, которые перекомпилируют каждые две минуты при типичной доле попаданий в кэш, выдержит значительно больше авторов, чем предполагает одно лишь число параллелизма, — порядка тысячи или более активных авторов при рабочей точке 256. Показатель параллелизма — это граница мгновенного всплеска, а не число мест.
4
Сначала определите бюджет задержки, затем выведите размер
Уравнение (1) непосредственно обращается. Для целевого ожидания при частоте на ядрах допустимый параллелизм равен , где для этого документа. Бюджет 60 с на 8 ядрах при 3 ГГц даёт ; бюджет 120 с удваивает это значение. Публиковать бюджет вместе с ёмкостью — единственный честный способ указать любое из этих значений.
5
Сначала покупайте память, затем ядра, и проверяйте, в какую стену вы упираетесь
Ниже 32 ГиБ мы почти не наблюдали выгоды от дополнительных ядер. Диагностика проста: если отказы проявляются как HTTP 502 при нехватке памяти на гостевой машине, добавьте память; если они проявляются как
timedout при свободной памяти, добавьте ядра или увеличьте тайм-аут. Администраторы могут определить это по той же сигнатуре отказа, которую мы использовали для классификации конфигураций.6
Частота — для удобства работы, ядра — для числа пользователей
Поскольку выполняется без кривизны (2% в диапазоне 1,0–5,5 ГГц), более высокая частота ускоряет каждую компиляцию для каждого пользователя. Больше ядер не ускоряет ни одной отдельной компиляции; они лишь позволяют выполнять больше параллельных. Развёртываниям, где жалуются, что «компиляция медленная», следует покупать частоту; развёртываниям, где жалуются, что «компиляция не проходит перед дедлайном», — память и ядра.
7
За пределом масштабируйтесь горизонтально, а не вертикально
Свыше 65 одновременных компиляций поддерживаемый путь — горизонтальное масштабирование (рисунки 2c и 10, подробнее в §9): несколько экземпляров приложения за балансировщиком нагрузки с привязкой сеансов по cookie, использующие общие централизованные MongoDB, Redis и S3-совместимое хранилище, при этом
git-bridge остаётся в единственном экземпляре. Это умножает предел на экземпляр на число экземпляров — именно так достигает своей ёмкости развёртывание SaaS.8
Не полагайтесь на изоляцию отдельных компиляций
Пока ограничение памяти в Docker runner не исправлено (§6.2), один патологический документ может исчерпать память хоста, вместо того чтобы быть завершённым в одиночку. Администраторам, которым нужна такая гарантия, следует обеспечить её самостоятельно, а не ждать исправления. На крупном хосте мы использовали слайс systemd с жёстким лимитом, на который затем указывается Docker-демон, чтобы каждый создаваемый им контейнер учитывался внутри этого слайса:Одна деталь здесь может стоить целого дня, если её упустить. Слайс с именем
docker-capped.slice располагается не рядом с docker.slice, а внутри него, потому что дефис — это разделитель иерархии, а не часть имени. Если кажется, что лимит не действует, обычно он применён на уровень выше или ниже того места, где на самом деле находятся контейнеры. Проверяйте, считывая пиковое значение из memory.max_usage_in_bytes после прогона, а не доверяя файлу конфигурации: на нашем хосте cgroup компиляции никогда не превышала пятой части лимита даже при 1024 одновременных компиляциях, что само по себе доказывает, что ограничивающим фактором был демон, а не память.
git-bridge, который хранит репозитории на локальном диске без возможности репликации и должен работать в единственном экземпляре рядом с одной назначенной репликой.
9. Эталонное развёртывание на нескольких машинах
Всё, что описано выше, измеряет одну машину. В этом разделе распределённая форма описана достаточно подробно, чтобы её можно было построить, — и, поскольку реальный вопрос администратора не как, а стоит ли, сначала указано, в какой момент это стоит затраченных усилий.9.1 Когда оправдана распределённая форма
Одна машина дешевле в эксплуатации во всех существенных отношениях: один домен отказа, нет общего состояния, которое нужно поддерживать согласованным, нет маршрутизации, которую можно настроить неправильно. Наши данные задают три порога, при которых от неё стоит отказаться.9.1.1 Ниже 65 одновременных компиляций — не стоит
Предел на экземпляр — программная константа, а не аппаратная (§6.1). Пока подаваемая нагрузка к нему не приближается, вторая машина лишь добавляет режимы отказа и ничего не даёт. 64-ядерный хост обслужил 256 одновременных компиляций с полным успехом только после снятия ограниченияcompileConcurrencyLimit; администратор, который ещё не изменил это одно значение, не упирается в оборудование и не должен заниматься его покупкой.
9.1.2 От 65 примерно до 500 — сначала масштабируйтесь вертикально
Вертикальное масштабирование оставалось линейным во всём нашем диапазоне и ни разу не вошло в регрессивный режим. Один крупный хост достиг 1024 одновременных холодных компиляций со 100% успехом (§4.3); колено задержки появилось на 512, не раньше. В этом диапазоне более крупная машина однозначно проще нескольких меньших, а более быстрая, согласно §4.4, улучшает работу каждого пользователя, а не просто позволяет принять их больше.9.1.3 Переходите к распределённой форме ради доступности, а не пропускной способности
Честная причина запускать больше одной реплики приложения ниже предела — одна машина означает один блок питания, одно ядро ОС, одно окно обновления. Это законная причина, и именно её мы бы и назвали; просто это не аргумент ёмкости, и смешение этих двух понятий приводит к тому, что администраторы покупают реплики, когда им нужна была память.9.2 Уровни и их размеры
Топология показана на рисунке 10. Она включает четыре уровня, и они масштабируются по разным величинам — в этом и состоит весь смысл их разделения.9.2.1 Периметр
Один балансировщик нагрузки или два для доступности. Он терминирует TLS и не выполняет ничего ресурсоёмкого; он масштабируется по числу соединений, а не компиляций, и для рассмотренных здесь нагрузок достаточно небольшого экземпляра. Значение имеет его конфигурация, а не размер (§9.3).9.2.2 Реплики приложения
Они несут нагрузку компиляции и являются единственным уровнем, масштабируемым по параллелизму. Определяйте размер каждой реплики по правилам из §8 — сначала память, потом ядра, затем частота, — а затем задайте число реплик так, чтобы покрыть пиковый параллелизм, делённый на предел одной реплики. Реплики не хранят ничего долговременного: на их локальном диске находятся временные файлы компиляции и кэш результатов, и то и другое можно восстановить. Именно это позволяет безопасно добавлять и удалять их, и это стоит проверить, а не предполагать, потому что один неправильно настроенный путьfilestore незаметно превращает этот уровень в хранящий состояние.
9.2.3 Состояние
Redis, MongoDB и S3-совместимое объектное хранилище на отдельных хостах. Redis несёт основную нагрузку, и это наименее очевидно: в нём находятся хранилище сеансов и живой буфер документов, что позволяет компиляции, направленной на любую реплику, видеть нажатия клавиш, сделанные через другую реплику. Администратор, который относится к Redis как к кэшу и рассчитывает его размер с учётом вытеснения, получит компиляции устаревших документов, которые чрезвычайно трудно диагностировать, потому что ничего не отказывает — результат просто неправильный. MongoDB масштабируется по числу проектов, а не по частоте компиляций. Объектное хранилище опционально при одной реплике и обязательно при большем числе.9.2.4 Единственный экземпляр
git-bridge хранит репозитории на локальном диске, поддерживает локальный индекс и не имеет механизма репликации. Он должен работать ровно в одном экземпляре, закреплённом рядом с одной назначенной репликой, и именно из-за него развёртывание оказывается не совсем без состояния. Планируйте его хост соответственно: именно его диск нужно резервировать.
Таблица 4. Эталонные уровни. По параллелизму масштабируется только уровень приложения; выбору его размера посвящён §8.
9.3 Маршрутизация — то, что легко настроить неправильно
Три класса запросов должны попадать в три разных места, а стандартная конфигурация с одним правилом удовлетворяет не более чем двум из них. Трафик компиляции по/project/ следует распределять консистентным хешированием по идентификатору проекта, чтобы кэш компиляции проекта оставался на одной реплике. Мы используем в HAProxy balance hash path,field(3,/) с hash-type consistent и hash-balance-factor 150. Этот выбор важен при горизонтальном масштабировании: при привязке по cookie существующие сеансы бессрочно остаются закреплёнными за исходной репликой, а новая реплика получает только новых пользователей, так что машина, за которую администратор только что заплатил, не берёт на себя никакой нагрузки из той, ради которой её покупали. В нашей конфигурации консистентное хеширование при масштабировании перераспределило 35% проектов против 0% при привязке по cookie.
С трафиком сеансов дело обстоит иначе. Когда upgrade до WebSocket не удаётся и socket.io переходит на XHR polling, последовательные опросы одного сеанса должны попадать на одну реплику, а в пути нет идентификатора проекта, по которому можно хешировать. Этому трафику нужен отдельный бэкенд с привязкой по cookie. Мы спроектировали такое разделение, но не развернули его; мы отмечаем это как пробел, а не выдаём за выполненное.
Наконец, /git/ должен попадать на реплику, рядом с которой работает git-bridge. Он направляется на эту реплику, а не напрямую в git-bridge, потому что мост аутентифицирует свои обратные вызовы через OAuth-эндпоинты приложения и разрешает URL blob-объектов через него; обход реплики ломает аутентификацию и ничего не улучшает.
9.4 Уменьшение масштаба требует буфера для завершения
Удаление реплики не симметрично её добавлению: выполняющаяся компиляция теряется, и пользователь видит отказ, которого он не вызывал. Рабочая последовательность — сначала прекратить новый трафик, подождать и только затем завершать. Мы реализовали это как pre-stop hook, удерживающий под в течение настраиваемого интервала, пока балансировщик помечает бэкенд как выводимый из работы (draining), — достаточно короткого, чтобы протестировать за минуты, а в production достаточно длинного для естественного завершения сеанса — часы, а не секунды. Этот интервал — параметр настройки, определяющий, будет ли эластичность незаметной или выводящей из себя. Ещё одно ограничение мы обнаружили путём измерений, а не при проектировании: автомасштабирование по CPU для этой нагрузки не работает. Собственное использование CPU подом приложения составляло 22 m core при общем значении узла 3997 m core, потому что работа компиляции выполняется в соседних контейнерах, которые под не учитывает. Любой сигнал для масштабирования этого уровня должен учитывать число работающих контейнеров компиляции, а не CPU пода.10. Значение за пределами Overleaf
Ничто в §4.2 или §4.3 не специфично для кода Overleaf. Измеренные закономерности следуют из трёх свойств, общих для любого размещаемого сервиса LaTeX: единица работы — однопоточный процесс, она изолирована в контейнере, а её рабочий набор — большое дерево только для чтения, которое должен удерживать страничный кэш. Три следствия напрямую применимы к любому, кто создаёт подобный сервис.10.1 Сначала память, затем ядра
Самый сильный результат матрицы — отрицательный: ниже 16 ГиБ число ядер почти не имеет значения, и только при 48 ГиБ конфигурации с 4, 8 и 16 vCPU вообще различаются (143, 268, 331). Администратор, понимающий общепринятое правило как «добавлять ядро на каждых пять пользователей», покупает не тот ресурс. Механизм — общий страничный кэш над деревом дистрибутива, и это свойство размера TeX Live, а не какого-либо конкретного фронтенда.10.2 Скорость допуска — это ресурс, о котором обычно забывают
При наш сервер ни разу не держал более 205 живых песочниц (рисунок 7b), хотя все запросы поступили одновременно. Ограничивающим фактором было создание контейнеров, а не компиляция, — что согласуется с измерительными исследованиями, относящими затраты на запуск контейнера к накладным расходам среды выполнения, а не к размеру образа [19, 20]. Сервис, который рассчитывает только CPU и память, обнаружит, что его поведение при всплесках определяется величиной, которую он никогда не измерял. Практическая форма этого вывода — рекомендация из §8: ограничивайте допуск сознательно, потому что выбранная вами очередь лучше очереди, которую вы обнаружите.10.3 Песочница, живущая дольше компиляции, делает модель недействительной
Каждый показатель ёмкости здесь предполагает, что контейнер создаётся, выполняет одну компиляцию и завершается — время жизни в десятки секунд и коэффициент загрузки около единицы только во время работы. Два недавних архитектурных шаблона нарушают это допущение, причём одинаковым образом. Первый — постоянная песочница для каждого пользователя. Выделение каждому пользователю фиксированной частной среды превращает статистически мультиплексируемый пул в набор резервирований: сервис, который за счёт разделения времени мог бы обслуживать 256 одновременных компиляций на 64 ядрах, сможет обслужить лишь 16 пользователей, если каждому выделить четыре ядра, — на порядок меньше на том же оборудовании. Наши данные количественно оценивают цену такого выбора, а не выступают против него: резервирования дают предсказуемость, и курс обмена составляет примерно в рекомендуемой нами рабочей точке. Второй, более новый, — ИИ-агент, разделяющий песочницу с компилятором. На платформах для написания текстов с помощью агентов тот же контейнер, в котором работает XeLaTeX, может также размещать долго работающего агента для программирования, поэтому он занят постоянно, а не всплесками. Практики сообщают ровно о том симптоме, который модель предсказывает для таких развёртываний, — устойчивой медлительности при умеренном числе пользователей [15]. Это взаимодействие стоит описать точно, потому что это не просто «больше нагрузки». Три наших вывода усиливают друг друга. Занятость перестаёт быть всплесковой, поэтому закон разделения времени из §4.2 применяется сразу ко всей совокупности пользователей, а не к той доле, которая сейчас компилирует. Страничный кэш, обеспечивающий сверхлинейную отдачу от памяти из §4.1, теперь делится с собственным рабочим набором агента и перестаёт быть прогретым для TeX. А отсутствующее ограничение памяти контейнера из §6.2 становится гораздо опаснее, потому что контейнер, который никогда не завершается, никогда не возвращает свою память. Мы не измеряли такую платформу и не делаем никаких утверждений о каком-либо конкретном продукте. Мы можем сказать, что наши числа означают для архитектуры: архитектуру, выделяющую каждому пользователю долгоживущую многоядерную песочницу, следует рассчитывать как систему резервирования, а не по приведённым здесь показателям параллелизма, и ожидаемая ёмкость будет ближе к числу её ядер, делённому на число ядер на пользователя, чем к чему-либо из таблицы 1.11. Ограничения достоверности
11.1 Один документ
Все измерения используют один 63-страничный документ XeLaTeX. Абсолютные значения ёмкости для других документов будут другими; законы масштабирования, являющиеся отношениями, — нет. Документ со значительно большим резидентным набором сдвинет стену памяти, не изменив её сверхлинейного характера.11.2 Виртуализированный хост
Гостевые машины работают под KVM на одной физической машине, поэтому абсолютные числа включают накладные расходы виртуализации, а гостевые машины разделяют страничный кэш хоста и NVMe-устройство. Мы устранили крупнейший искажающий фактор, выключив посторонние гостевые машины после того, как заметили, что нехватка памяти на хосте увеличивает среднюю нагрузку внутри гостевой машины более чем в раза при одинаковом параллелизме.11.3 Одновременное поступление
Каждая компиляция запускается в один и тот же момент, что является наихудшим случаем. Реальные пользователи поступают как случайный процесс, поэтому развёртывание, рассчитанное по нашим числам, имеет запас, а не дефицит, — но пик в конце срока подачи ближе к нашей модели, чем к пуассоновской.11.4 Граничные конфигурации
При 2 ГиБ система настолько близка к краху, что повторные прогоны одной и той же конфигурации могут отличаться на одну компиляцию. Мы указываем консервативное значение и не делаем выводов из различий в в этом режиме.12. Доступность
Тестируемая система, инструменты развёртывания и upstream-проект, на котором она основана, общедоступны:- Ayakaleaf Pro — https://github.com/ayaka-notes/ayakaleaf-pro
- Инструментарий развёртывания — https://github.com/ayaka-notes/toolkit
- Документация — https://ayakaleaf-pro.ayaka.space
- Upstream Overleaf — https://github.com/overleaf/overleaf
- Образы TeX Live для компиляции —
ghcr.io/ayaka-notes/texlive-full:2025.1
9a519f0d3d, 5d472e9b38) доступны в истории Overleaf.

