Skip to main content

Overleaf-Benchmark.pdf

Анотація

Самостійно розміщені розгортання Overleaf зазвичай розраховують за одним емпіричним правилом: одне ядро CPU та один гігабайт пам’яті на п’ять–десять одночасних користувачів. Ми показуємо, що це правило не просто неточне, а структурно хибне, оскільки воно припускає, що пропускну здатність визначає один вимір ресурсів, тоді як насправді її визначають дві незалежні стіни, і оскільки два програмні параметри — жоден з яких не є апаратним — впливають на результат аж у чотири рази. Ми вимірюємо стандартне розгортання Ayakaleaf Pro v6.2.2 з ізольованою компіляцією (TeX Live 2025) у 21 конфігурації CPU/пам’яті всередині гостьових систем QEMU/KVM, ядра хоста яких зафіксовано на частоті 3.0 GHz. Навантаженням слугує реальна 63-сторінкова дисертація XeLaTeX, яку одночасно компілюють до кількох сотень різних облікових записів користувачів. Ми виявили, що за обсягу гостьової пам’яті менше 32 GiB кількість ядер майже не має значення — за 16 GiB виміряна пропускна здатність гостьових систем із 4, 8 і 16 vCPU відрізняється менш ніж на 8%, — а натомість пропускну здатність визначає надлінійна стіна пам’яті, що виникає через спільний кеш сторінок для дерева TeX Live. Щоб з’ясувати, чи зберігаються ці закономірності при зміні масштабу на порядок, ми повторюємо вимірювання на одному сервері з 64 ядрами та 995 GiB пам’яті. Він витримує 1024 одночасні холодні компіляції зі 100% успіхом — у вісім разів більше, ніж кількість його потоків, — і ми так і не досягаємо його межі. Корисним числом є не ця межа, а точка перегину нижче неї: хвостова затримка зростає на 20–40% за кожного подвоєння до N=256N=256, а потім на 190% при N=512N=512. Тому пропускна здатність, визначена як “найбільша паралельність, за якої немає збоїв”, завищувала б придатну робочу точку вчетверо. На цій машині пам’ять ніколи не є обмежувальним ресурсом; межею є CPU разом зі швидкістю, з якою демон контейнерів може запускати нові пісочниці, що насичується близько 200 незалежно від кількості запитаних компіляцій. Ми також виявили два ефекти на рівні реалізації, непомітні під час планування потужностей. По-перше, CLSI застосовує жорстко закодовану межу в 65 одночасних компіляцій, яку не можна змінити жодною змінною середовища; понад цю межу користувачі одразу отримують HTTP 503 замість постановки в чергу. По-друге, обмеження пам’яті на контейнер у Docker runner не діє з моменту його впровадження у 2018 році — як через своє значення, так і через місце застосування, — тож подія нестачі пам’яті валить увесь хост, а не одну компіляцію. Зняття межі паралельності та збільшення типового тайм-ауту компіляції зі 180 s до 300 s підвищує виміряну пропускну здатність гостьової системи з 8 vCPU / 48 GiB з 64 до 268 одночасних компіляцій — у 4.2 раза без жодних витрат на обладнання. Нарешті, ми показуємо, що паралельність у цій системі не дає нічого, крім розподілу часу, і що навантаження обмежене лише тактовою частотою. Підібраний закон деградації T(N)=T1max⁡(1,N/C)bT(N)=T_1\max(1,N/C)^{b} дає b=0.914b=0.914, що близько до ідеального пропорційного сповільнення, а розгортка частоти в повному діапазоні машини 1.0–5.5 GHz зводить тридцять вимірювань до T=(k/f)max⁡(1,N/C)T=(k/f)\max(1,N/C) з k=27.9GHz⋅sk=27.9 GHz·s і залишковим розкидом 5.1%. Збільшення частоти в 5.5 раза дає прискорення в 5.5 раза без спадної віддачі — саме в цьому сенсі частота й ядра дають різне: частота пришвидшує компіляцію кожного користувача, а ядра лише дають змогу обслужити більше користувачів.

1. Вступ

Overleaf — провідний редактор LaTeX для спільної роботи, а його локальний дистрибутив широко використовують університети й дослідницькі групи, які не можуть надсилати неопубліковані рукописи до сторонньої хмари. Розрахунок такого розгортання — постійне практичне питання: за фіксованого бюджету на обладнання скільки людей насправді можуть одночасно натиснути “Recompile”? Офіційна рекомендація — лінійне правило: приблизно одне ядро та один гігабайт на п’ять–десять одночасних користувачів, що передбачає плавне й спільне масштабування пропускної здатності за обома ресурсами. Наші вимірювання суперечать цьому в трьох аспектах.

1.1 Пропускну здатність визначають дві незалежні стіни, а не одна

Конфігурація зазнає збою або через вичерпання пам’яті — тоді падає сам стек Overleaf і повертає HTTP 502, — або через перевищення компіляціями серверного тайм-ауту — тоді CLSI повідомляє timedout, хоча гігабайти пам’яті залишаються невикористаними. Ці два режими мають зовсім різну поведінку масштабування й різні засоби усунення. Додавання ядер до конфігурації, обмеженої пам’яттю, не просто неефективне, а часом навіть шкідливе: ми вимірюємо конфігурації, у яких збільшення кількості ядер зменшує пропускну здатність, оскільки з більшою кількістю ядер паралельні компіляції просуваються синхронно, і їхні пікові потреби в пам’яті збігаються, замість того щоб чергуватися.

1.2 Програмні параметри важать більше за обладнання

Тайм-аут компіляції — це поле користувача в MongoDB, типове значення якого, 180 s, непомітно обмежує конфігурації, обмежені CPU. Його збільшення до 300 s підвищує виміряну пропускну здатність до 4.2 раза на тому самому обладнанні. Незалежно від цього CLSI відхиляє понад 65 одночасних компіляцій через жорстко закодовану константу. Будь-яке дослідження пропускної здатності — і будь-яке розгортання, — що не враховує обох чинників, вимірює програмне забезпечення, а не машину.

1.3 Паралельність — це розподіл часу, а не паралелізм

Оскільки компіляція LaTeX однопотокова, обслуговування NN одночасних користувачів на CC ядрах не змушує систему завершити роботу швидше; натомість кожен користувач чекає пропорційно довше. Тому питання “скільки одночасних користувачів підтримується” некоректне, доки не визначено, скільки користувач готовий чекати. Ми робимо цю залежність явною та кількісно її оцінюємо.

1.4 Внесок

  • Матриця пропускної здатності для 21 конфігурації CPU/пам’яті, виміряна в умовах фіксованої частоти з повторною перевіркою, з визначенням обмежувального чинника для кожної конфігурації за сигнатурою збою.
  • Дві підібрані моделі: модель пропускної здатності, що відокремлює надлінійну стіну пам’яті від стелі CPU, і модель затримки, що встановлює поведінку чистого розподілу часу.
  • Виявлення та експериментальне підтвердження двох проблем реалізації в розгорнутій системі, зокрема обмеження пам’яті контейнера, яке не діє з 2018 року.
  • Кількісна оцінка компромісу між тайм-аутом компіляції та пропускною здатністю, яку, на нашу думку, слід зазначати поряд із будь-яким показником паралельності.

2. Передумови

2.1 Шлях компіляції

Запит на компіляцію в Overleaf проходить шлях web →\rightarrow clsi →\rightarrow контейнер компіляції. У розгортанні з ізольованою компіляцією (SIBLING_CONTAINERS_ENABLED=true) CLSI не запускає latexmk у власному процесі; він просить Docker-демон хоста, доступний через змонтований сокет, запустити новий контейнер з образу TeX Live, у якому каталог проєкту змонтовано в /compile. Отже, одна компіляція — це один короткоживучий контейнер, що виконує один процес latexmk. Звідси випливають три наслідки, і всі три визначають вимірювання в цій статті. По-перше, одиницею роботи є однопотоковий процес: XeLaTeX не розпаралелюється. По-друге, ізоляція ресурсів для кожної компіляції така, якою її запитує Docker runner, — у §6.2 ми показуємо, що фактично він не запитує нічого. По-третє, робочий набір визначається не документом, а деревом TeX Live — корпусом приблизно 32 GiB лише для читання, з якого читає кожна паралельна компіляція і який тому спільно використовується через кеш сторінок хоста. Саме це спільне використання є джерелом надлінійного масштабування пам’яті, яке ми спостерігаємо.

2.2 Увімкнення ізольованої компіляції

Overleaf Community Edition запускає latexmk безпосередньо в контейнері застосунку. Ayakaleaf Pro, як і Overleaf Server Pro, натомість може запускати кожну компіляцію в сусідньому (sibling) контейнері — контейнері, який застосунок запускає в Docker-демоні хоста, а не вкладено всередині контейнера застосунку. Це вмикають два налаштування Toolkit:
Toolkit монтує Docker-сокет хоста в контейнер застосунку й перетворює ці налаштування на змінні середовища, які читає CLSI: 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, застосованого до NN однопотокових процесів, і саме тому він такий регулярний.
Рисунок 1. Один запит на компіляцію, простежений через мікросервіси Community Edition. Розподіл на кроках і важливий для пропускної здатності: текст документа копіюється в тіло запиту, тоді як бінарні ресурси передаються за посиланням і завантажуються clsi. Жоден із них не є визначальним — вартість компіляції проєкту визначається 32-гігабайтним деревом TeX Live, яке кожна паралельна компіляція читає через спільний кеш сторінок.
Рисунок 2. Три топології розгортання та місце, де в кожній із них виникає межа компіляцій на екземпляр; панелі, розміщені стосом, позначають реплікацію. Константа в 65 компіляцій захищає один CLSI, тож флот SaaS множить її на кількість екземплярів і зон (a), а горизонтальне масштабування, що підтримується Server Pro та Ayakaleaf Pro, множить її на кількість екземплярів (c) — ціною централізованих MongoDB, Redis і S3-сумісного сховища, балансувальника навантаження з прив’язкою сеансів через cookie (результат компіляції записується на локальний диск екземпляра, тож компіляція та подальше завантаження PDF мають потрапити на той самий екземпляр) і єдиного git-bridge. Типова конфігурація Toolkit (b), яку ми й вимірюємо, має множник один, тож константа, розрахована на одного члена флоту, стає межею всієї інсталяції.
Рисунок 3. Вибір шарда в clsi-cache. Проєкт відображається за формулою crc32⁡(projectId-i) mod ∣shards∣\operatorname{crc32}(\text{projectId}\text{-}i)\bmod|\text{shards}|, тобто простір хешів розрізається на стільки рівних секторів, скільки є шардів. Це хешування за модулем, а не узгоджене хешування на основі кільця: збільшення флоту з трьох шардів до чотирьох перерозподіляє весь простір і переназначає практично кожен проєкт (a, b). Саме тому реалізації потрібен явний механізм поступового онлайн-перешардування, який протягом часового вікна переносить лінійно зростаючу частку проєктів з currentShards до desiredShards, замість переміщення K/nK/n, яке дало б кільце узгодженого хешування. Коли спрацьовує автоматичний вимикач (circuit breaker) шарда, сіль ii збільшується, а шард вилучається зі списку кандидатів, тож пошук продовжує перевіряти наступні шарди замість того, щоб завершитися збоєм (c).

2.3 Два режими збою

Кожна виміряна нами конфігурація зазнає збою рівно одним із двох способів, і ця відмінність видна безпосередньо зі статусу відповіді, а не виводиться опосередковано:
  • Вичерпання пам’яті — сам стек Overleaf перестає відповідати, і запит повертає HTTP 502. Доступна гостьова пам’ять на рівні збою зазвичай менша за 500 MiB.
  • Тайм-аут компіляції — CLSI перериває компіляцію після досягнення тайм-ауту користувача й повідомляє статус timedout. Доступна пам’ять на рівні збою часто становить кілька гігабайтів.
Ми класифікуємо кожну конфігурацію за цією сигнатурою, а не за евристикою на основі співвідношень ресурсів, тож на питання “у яку стіну ми вперлися” можна відповісти на основі самих даних.

3. Методологія

3.1 Тестовий стенд і контроль частоти

Усі гостьові системи працюють під QEMU/KVM на одному хості з Intel Core i9-14900K, 62 GiB RAM і сховищем 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 GHz на кожному ядрі й прив’язуємо vCPU гостьової системи до фізичних P-ядер за допомогою taskset. На CPU з гібридними ядрами ця відмінність важлива: E-ядра цього процесора мають базову частоту 2.4 GHz і не можуть досягти 3.0 GHz при вимкненому turbo, тож запуск, який випадково потрапить на них, непомітно вимірюватиме повільнішу машину. Під повним навантаженням ми перевіряємо, що частота становить рівно 3000 MHz на всіх шістнадцяти прив’язаних потоках. Скрипт-запобіжник перевіряє цю умову перед кожним бенчмарком і в разі її порушення відмовляється запускатися; під час дослідження він виявив одне непомітне скидання регулятора частоти.

3.2 Другий тестовий стенд: один великий сервер

Матриця QEMU ізолює по одній змінній за раз, але обмежена шістнадцятьма прив’язаними потоками. Щоб з’ясувати, чи діють ті самі закономірності на порядок вище, ми повторили розгортку паралельності на одному великому сервері: один AMD EPYC 7773X (Milan-X, 64 ядра / 128 потоків, 768 MiB L3) з 995 GiB RAM, що виконує той самий образ Ayakaleaf Pro v6.2.2 з тим самим texlive-full:2025.1. На відміну від гостьових систем QEMU, частоту цієї машини не зафіксовано: це сервер виробничого класу, і ми вимірюємо його саме як такий. Знадобилися два операційні запобіжні заходи, про які варто сказати, бо без них експеримент вимірює тестову обв’язку, а не сервер. По-перше, кожен контейнер було обмежено слайсом systemd з MemoryMax=940 GiB, щоб розгортка, яка вийшла з-під контролю, вичерпала cgroup, а не хост. По-друге, ізольовані компіляції створює демон хоста, і кожна з них змінює власний шар copy-on-write — за вимірюваннями 116 MiB на контейнер, хоча базовий образ розміром 20.6 GiB спільний, — тому кореневий каталог даних Docker було перенесено на окремий пристрій NVMe. Розгортка при N=1024N=1024 записує приблизно 119 GiB тимчасових шарів, що не вміщується на стандартній кореневій файловій системі.

3.3 Навантаження

Документ — реальна 63-сторінкова магістерська дисертація (шаблон SJTU), що компілюється XeLaTeX через latexmk і містить рисунки TikZ, обробку бібліографії biblatex і вбудовані PDF-ресурси, тобто реалістичне, а не синтетичне навантаження. Одна компіляція на ненавантаженій гостьовій системі займає 8.6–9.8 s в усіх конфігураціях; це значення ми використовуємо як базовий рівень вільного виконання T1T_1.

3.4 Генерування навантаження

Ми створюємо 512 справжніх облікових записів користувачів і даємо кожному власну копію проєкту, щоб паралельні компіляції конкурували саме так, як конкурували б незалежні користувачі, а не через спільне блокування проєкту. Запити надсилаються з хоста на перенаправлений порт гостьової системи, тож генерування навантаження не споживає CPU гостьової системи. Паралельність є одночасною, а не розподіленою в часі. Спочатку встановлюється кожен сеанс — вхід, CSRF-токен, вибір компілятора, — і лише потім кожен потік засинає до спільного моменту реального часу, обчисленого один раз і спільного для всіх, а тоді надсилає свій POST /project/:id/compile. Ця відмінність не є педантизмом. Поступове наростання вимірює пропускну здатність за стабільної черги; одночасний сплеск вимірює те, що відбувається, коли аудиторія студентів натискає ту саму кнопку після того самого оголошення про дедлайн, — а саме цього й бояться оператори. Ці два випадки різняться більш ніж на сталий множник, оскільки в другому черга компіляцій заповнюється швидше, ніж демон встигає її розібрати. Перш ніж такий сплеск можна було відтворити достовірно, довелося усунути чотири практичні перешкоди. Кожну з них варто зафіксувати, бо кожна непомітно перетворює експеримент на вимірювання тестової обв’язки, а не сервера.

3.4.1 Два обмежувачі частоти запитів, а не один

Overleaf обмежує кількість входів з однієї адреси джерела — 20 спроб на хвилину, — а весь наш трафік надходить з одного хоста. Призначення кожному імітованому користувачеві окремої адреси X-Forwarded-For знімає це обмеження, але одразу наштовхується на друге, грубіше: ліміт приблизно 200 на хвилину для підмережі. Тому розподіл користувачів у суцільному блоці адрес дає збій на 201-му обліковому записі. Натомість ми обчислюємо синтетичну адресу з індексу користувача так, щоб послідовні користувачі потрапляли в різні /24, 203.  ⌊i/250⌋ mod 100+1.  i mod 250+1.  i mod 200+10,\texttt{203.}\;\big\lfloor i/250 \big\rfloor \bmod 100 + 1\texttt{.}\; i \bmod 250 + 1\texttt{.}\; i \bmod 200 + 10 , що залишає запас для обох обмежувачів для всієї сукупності з 1024 користувачів.

3.4.2 Доданий заголовок типово відкидається

Задати заголовок недостатньо. Express враховує X-Forwarded-For лише від вузлів, яким йому наказано довіряти, а trustedProxyIps в Overleaf типово дорівнює loopback. Оскільки генератор навантаження звертається до застосунку через міст контейнера, а не через інтерфейс loopback, заголовок розбирається, а потім відкидається, і всі імітовані користувачі знову зводяться до однієї адреси. Симптомом є хвиля HTTP 429 рівно на двадцятому вході, яку легко помилково сприйняти як перевантаження сервера. Мережу шлюзу потрібно явно додати до ланцюжка довіри; у кластерному розгортанні з §4.3 також потрібно додати CIDR подів і сервісів.

3.4.3 Балансувальник навантаження перезапише заголовок, який його просили зберегти

Коли екземпляр розташовано за проксі, стандартна опція option forwardfor додає справжню адресу клієнта до ланцюжка — це правильна поведінка для виробничого середовища й саме те, що тут шкодить: синтетична адреса витісняється адресою самого генератора навантаження. Директиву потрібно уточнити як option forwardfor if-none, щоб проксі додавав значення лише тоді, коли клієнт його не надав.

3.4.4 У клієнта закінчуються файлові дескриптори раніше, ніж у сервера закінчується пропускна здатність

При N=1024N=1024 генератор тримає понад тисячу одночасних сокетів, і типове м’яке обмеження в 1024 дескриптори досягається ще під час встановлення сеансів, а не під час вимірювання. Збій відбувається тихо: три сеанси не встановлюються, і запуск повідомляє про 1021, а не 1024, тоді як потік збору даних, що запускає зовнішні команди для підрахунку контейнерів, завершується з EMFILE і непомітно обрізає телеметрію. М’яке обмеження на генераторі потрібно підвищити — жорстке обмеження на нашому хості вже становило 1048576 — і повторити запуск. У §4.3 ми наводимо обидва запуски: виправлений завершує 1024 з 1024 з медіаною, що відрізняється від обрізаного запуску не більш ніж на 1.2 s, тому ми вважаємо перший придатним, але не авторитетним.

3.5 Протокол вимірювань

Для відтворюваності знадобилося кілька методологічних рішень.

3.5.1 Прогрівання

На щойно завантаженій гостьовій системі кеш сторінок порожній, і перші компіляції вимірюють введення-виведення холодного старту, а не пропускну здатність у сталому режимі: та сама конфігурація 2 vCPU / 2 GiB дає 36.5 s у холодному стані й 9.8 s у прогрітому — різниця в 3.7 раза. Тому для кожної конфігурації після завантаження виконуються два прогрівальні запуски з однією компіляцією, результати яких відкидаються.

3.5.2 Критерій успіху

Рівень паралельності вважається пройденим, лише якщо кожна компіляція завершується успішно і рівень витримує повторний запуск. Це суворіше за поріг частки успішних запусків, і це має значення: при 4 vCPU / 16 GiB рівень 32 один раз пройшов з медіаною 80.2 s, а під час повтору всі 32 компіляції завершилися тайм-аутом, тому ми повідомляємо значення 31.

3.5.3 Пошук

Рівні визначаються експоненційним пошуком меж від початкового значення, передбаченого моделлю, з подальшим точним цілочисельним бісекційним пошуком. Оскільки критерій працює за принципом “усе або нічого”, рівень визначається першим збоєм, тож щойно один запит завершується невдачею, ми відкидаємо решту запитів, що виконуються, — за винятком малих рівнів, де покинуті компіляції настільки завантажують невелику гостьову систему, що вона вже не відновлюється.

3.5.4 Ізоляція між рівнями

Перш ніж розпочати наступний рівень, контейнери компіляції завершуються, а вебзастосунок опитується, доки знову не почне відповідати. Без цього рівень, що йде після аварії, фіксує хибний збій із нулем сеансів.

3.5.5 Гігієна хоста

Сторонні віртуальні машини на хості було вимкнено: коли 24 GiB пам’яті хоста було виділено іншим системам, та сама конфігурація гостьової системи показувала середнє навантаження 11.7 замість 3.2 за однакової паралельності. Тиск на пам’ять хоста поширюється на гостьову систему й робить вимірювання недійсними.

4. Результати

4.1 Матриця пропускної здатності

Таблиця 1 і рисунок 4 наводять виміряну межу для кожної конфігурації. Перший сюрприз видно, якщо читати її по рядках. За 4 GiB гостьові системи з 2, 4 і 8 vCPU досягають рівно 9 — збільшення кількості ядер учетверо нічого не змінює. За 16 GiB вони досягають 54, 45 і 57: перехід від 4 до 16 ядер дає 6%, а 8-ядерна гостьова система насправді гірша за 4-ядерну (§5.2). Лише за 48 GiB кількість ядер чітко розрізняє конфігурації: 143, 268 і 331.
Рисунок 4. Виміряна пропускна здатність у матриці конфігурацій. (a) Кожна конфігурація як стовпчик, згрупований за пам’яттю й забарвлений за кількістю ядер; суцільні стовпчики обмежені пам’яттю (гостьова система падає через вичерпання пам’яті), а заштриховані обмежені CPU (компіляції завершуються тайм-аутом, хоча пам’ять ще є). Читання групи зліва направо показує, як мало дає кількість ядер нижче 16 GiB; читання між групами показує надлінійну віддачу від пам’яті. (b) Ті самі точки порівняно з підібраною моделлю Nmax⁡=min⁡(0.69R1.60, 26.4C)N_{\max}=\min(0.69R^{1.60},\,26.4C); штрихова лінія — стіна пам’яті, а пунктирні горизонталі — стелі CPU для кожної кількості ядер. Конфігурація обмежена тією з двох стін, яку вона досягає першою. Другий сюрприз видно, якщо читати по стовпцях: за фіксованої кількості ядер пропускна здатність зростає з пам’яттю надлінійно, приблизно як R1.6R^{1.6}, через кеш сторінок, як пояснено в §5.1. Таблиця 1. Максимальна кількість одночасних компіляцій, що завершуються успішно, виміряна з тайм-аутом компіляції 300 s і знятою межею паралельності CLSI. Жирним позначено конфігурації, обмежені CPU (компіляції завершуються тайм-аутом, хоча пам’ять ще є); решта обмежені пам’яттю (стек падає з HTTP 502). Рядок 2 GiB містить виправлення, описане в §5.2.

4.2 Паралельність — це розподіл часу

Рисунок 5 показує розгортку всіх рівнів паралельності на фіксованій гостьовій системі з 8 vCPU / 16 GiB. Два режими розділені різким перегином рівно на одній компіляції на ядро. Нижче нього середній час компіляції сталий — він змінюється з 8.7 s при N=1N=1 до 9.1 s при N=C=8N=C=8, тобто на 5%. Вище нього час зростає строго пропорційно до N/CN/C: при N=16,24N=16,24 ми вимірюємо 18.5 s і 27.1 s, тобто співвідношення 1:2.13:3.121:2.13:3.12 проти ідеального 1:2:31:2:3.
Рисунок 5. Затримка компіляції залежно від паралельності на фіксованому обладнанні. Перегин розташований при N=CN=C; за ним виміряне сповільнення відповідає N/CN/C з точністю до 5–7%. Усі п’ятнадцять рівнів пройдено повністю успішно.
Рисунок 6. Затримка компіляції залежно від паралельності для кількох конфігурацій. На кожній панелі обладнання фіксоване, а запропоноване навантаження змінюється; вертикальна лінія позначає N=CN=C. Ліворуч від неї криві пласкі, а праворуч лінійні щодо N/CN/C, що є сигнатурою розподілу часу, а не конкуренції: робота не стає дорожчою, вона просто чекає своєї черги. Підбір T(N)=T1max⁡(1,N/C)bT(N)=T_1\max(1,N/C)^{b} за всіма успішними вимірюваннями в дослідженні дає b=0.914b=0.914 (Rlog⁡2=0.904R^2_{\log}=0.904, n=81n=81). Показник степеня, що практично не відрізняється від одиниці, — це кількісне твердження про те, що компіляція є однопотоковою одиницею роботи, обмеженою CPU, і що паралельність не допомагає й не шкодить, окрім поділу ядер. Практичний висновок незручний для планування потужностей: конфігурація може прийняти довільну кількість користувачів без збоїв, змушуючи при цьому кожного з них чекати пропорційно довше. При N=56N=56 на цій гостьовій системі всі компіляції досі успішні, але кожен користувач чекає 64.8 s замість 8.7 s.

4.3 Вертикальне масштабування до 1024 одночасних компіляцій

Таблиця 2 і рисунок 7 наводять результати розгортки на великому сервері. Кожен рівень — це холодна компіляція: перед кожним рівнем ми очищаємо каталог компіляції та кеш CLSI кожного проєкту-учасника через DELETE /project/:id/output, тож жоден рівень не отримує переваг від роботи, виконаної на попередньому рівні. Базовий час однієї компіляції на цій машині становить 28.8 s; це холодний показник, і його не слід порівнювати з базовим рівнем сталого режиму 8.6–9.8 s, використаним раніше; холодний базовий рівень на гостьових системах QEMU становить 28.3 s, тож у перерахунку на потік обидві машини для цього навантаження відрізняються в межах двох відсотків. Таблиця 2. Розгортка паралельності на одному EPYC 7773X (64 ядра / 128 потоків, 995 GiB). Усі рівні холодні; базовий рівень 28.8 s. Пік контейнерів — максимальна кількість пісочниць, що існують одночасно.
Рисунок 7. Вертикальне масштабування на одному великому сервері. (a) Затримка залежно від запропонованої паралельності; затінена область позначає режим за перегином. (b) Кількість фактично активних пісочниць ніколи не відповідає кількості запитаних — вона насичується близько 200, — тоді як cgroup компіляції ніколи не використовує більше п’ятої частини свого ліміту.

4.3.1 Машина ніколи не дає збою

Кожен рівень завершується на 100%, зокрема N=1024N=1024 — у вісім разів більше за кількість потоків. Ми не знайшли межі пропускної здатності цієї машини; у нас закінчилося терпіння раніше, ніж у неї закінчився запас. Це перша конфігурація в дослідженні, де обмежувальним чинником не є пам’ять: при N=1024N=1024 cgroup компіляції досягає піку 184 GiB, тобто п’ятої частини свого ліміту 940 GiB, тоді як CPU завантажений на 100% із середнім навантаженням 166.

4.3.2 Деградація сублінійна, бо прийом запитів обмежений за швидкістю

Наївна модель розподілу часу передбачає, що 8×8\times потоків коштують 8×8\times затримки. Виміряна вартість становить 9.7×9.7\times відносно однієї компіляції, але лише 3.8×3.8\times відносно N=128N=128 — за восьмикратного збільшення запропонованого навантаження. Причину видно на рисунку 7(b) і в останньому стовпці таблиці 2: хоча 1024 запити надсилаються одночасно, кількість фактично активних пісочниць ніколи не перевищує 205. Демон не може створювати контейнери так швидко, як просять клієнти, тож запити стають у чергу на етапі прийому, а не конкурують за CPU. Саме черга рятує тут хвіст розподілу, і робить вона це випадково.

4.3.3 Перегин — на 512, а не в точці збою

Між N=256N=256 і N=512N=512 затримка p95p_{95} зростає в 2.9×2.9\times за подвоєння навантаження; кожне попереднє подвоєння коштувало від 1.2×1.2\times до 1.4×1.4\times. Пропускна здатність, визначена як “найбільше NN, за якого немає збоїв”, дорівнювала б 1024 і була б марною для оператора: у цій точці хвостове очікування становить майже вісім хвилин.

4.4 Час компіляції обернено пропорційний частоті

Оскільки навантаження обмежене CPU, його вартість має масштабуватися як 1/f1/f. Ми перевіряємо це безпосередньо, змінюючи частоту хоста в усьому діапазоні машини, 1.0–5.5 GHz, десятьма кроками на гостьовій системі без інших змін (рисунок 8). Час однієї компіляції змінюється з 26.5 s до 4.8 s: збільшення частоти в 5.5 раза дає прискорення в 5.5 раза без спадної віддачі в будь-якій частині діапазону. Добуток T ⁣⋅ ⁣fT\!\cdot\!f залишається сталим з точністю до 2% на всіх десяти частотах. Нормалізація за часткою ядер зводить усі тридцять вимірювань — три рівні паралельності на десяти частотах — до однієї константи: T(N,f)  =  kf max⁡ ⁣(1,NC),k=27.9 GHz⋅sT(N,f) \;=\; \frac{k}{f}\,\max\!\left(1,\frac{N}{C}\right), \qquad k = 27.9\ \mathrm{GHz\cdot s} із залишковим розкидом 5.1% у діапазоні, в якому сама частота змінюється в 5.5 раза. Відсутність будь-якої кривини сама по собі є результатом: якби навантаження було обмежене пропускною здатністю пам’яті чи введенням-виведенням, TT вирівнювався б на високих частотах, коли CPU обганяв би інший ресурс.
Рисунок 8. Розгортка частоти. (a) T=k/fT=k/f з підібраною гіперболою. (b) Після ділення на max⁡(1,N/C)\max(1,N/C) усі точки зводяться до однієї константи, що підтверджує рівняння (1). Рівняння (1) має прямий наслідок для закупівель, який легко сформулювати й легко неправильно застосувати: частота покращує досвід кожного окремого користувача, а кількість ядер лише дає змогу прийняти більше користувачів. Машина з частотою на 20% вищою компілює на 20% швидше для всіх, без спадної віддачі; удвічі більша кількість ядер зовсім не пришвидшує нічию компіляцію.

5. Аналіз

5.1 Дві стіни, підібрані окремо

Кожну конфігурацію класифіковано за сигнатурою збою (§2.3), після чого стіну пам’яті та стелю CPU підібрано лише за тими конфігураціями, які справді в них упираються: Nmax⁡=min⁡(ARp,  kcC)N_{\max} = \min\left(A R^{p},\; k_c C\right) де RR вимірюється в гібібайтах, а CC — у vCPU. Показник степеня стіни пам’яті стабільно надлінійний, p>1p>1: гранична вартість пам’яті для однієї додаткової паралельної компіляції зменшується зі зростанням загального обсягу пам’яті — з приблизно 312 MiB на компіляцію на гостьовій системі з 3 GiB до близько 194 MiB на системі з 32 GiB. Механізмом є спільний кеш сторінок для дерева TeX Live, описаний у §2.1: паралельні компіляції читають файли шрифтів і макросів, що перетинаються, тож більший кеш амортизується на більшу кількість компіляцій. Саме тому наївне правило “один гігабайт на п’ять користувачів” занижує прогноз для великих машин і завищує для малих.
Рисунок 9. Ті самі дані у вигляді двох поверхонь над площиною (C,R)(C,R). (a) Пропускна здатність: підібрана поверхня — це хребет, а не площина: вона круто піднімається з пам’яттю й майже пласка вздовж осі ядер, доки пам’ять не перестає бути обмеженням, — саме тому рядок 48 GiB єдиний, де кількість ядер розрізняє конфігурації. (b) Затримка залежно від паралельності для кожної конфігурації; підібрану залежність T=12.6 (N/C)0.91T=12.6\,(N/C)^{0.91} показано штриховою лінією, а тайм-аут 180 s зображено площиною. Конфігурація зазнає збою там, де її суцільна крива перетинає цю площину, що наочно показує, наскільки безпосередньо налаштування тайм-ауту визначає заявлену пропускну здатність.

5.2 Де більша кількість ядер погіршує ситуацію

Рівняння (2) — це мінімум двох членів, тож воно монотонне щодо CC, але вимірювання — ні. Ми спостерігаємо дві інверсії, у яких додавання ядер зменшило пропускну здатність: за 16 GiB (54 проти 45) і за 32 GiB (145 проти 135). Обидві виникають у режимі обмеження пам’яттю, і механізм в обох однаковий: з більшою кількістю ядер паралельні компіляції просуваються синхронно й досягають пікового резидентного розміру одночасно, тоді як з меншою кількістю ядер планувальник чергує їх, і піки розносяться в часі. На гостьовій системі, запас пам’яті якої вже мінімальний, саме це рознесення утримує її від падіння. Модель пропускної здатності, побудована на середньому використанні ресурсів, не може цього виразити; це властивість збігу піків. Третю уявну інверсію, за 2 GiB, ми тепер не враховуємо. Пошук фіксує пропускну здатність 2 при 2 vCPU, але 1 при 4 і 8 vCPU, що виглядає як той самий ефект. Повторний аналіз сирих розгорток показує дещо простіше: за 2 GiB рівень N=2N=2 успішно пройшов з першої спроби для всіх трьох кількостей ядер, а потім не пройшов підтверджувальний запуск для двох із трьох. Цей рівень — не пропускна здатність, а підкидання монети, і значення для 2 vCPU — це просто вдалий кидок. Тому для всіх трьох кількостей ядер ми наводимо відтворюване значення 1 і не робимо жодних висновків з цієї різниці. Ми фіксуємо це виправлення тут, а не непомітно змінюємо таблицю, бо відкинутий результат — саме з тих, що могли б підтвердити цікаве твердження.

6. Висновки щодо реалізації

6.1 Жорстко закодована межа паралельності

На достатньо великих гостьових системах пропускна здатність зупинялася рівно на 65 одночасних компіляціях незалежно від запитаної паралельності: при N=66,80,96,128N=66,80,96,128 ми зафіксували 6565 успішних запитів і 1,15,31,631,15,31,63 миттєвих відповідей unavailable, при цьому кількість контейнерів трималася на 65, кілька гігабайтів пам’яті залишалися невикористаними, а медіанний час компіляції стабільно становив 77 s — набагато менше за будь-який тайм-аут. Причина — константа в CLSI:
Порівняння нестроге, тож ефективна межа дорівнює 64+1=6564+1=65, що точно збігається з вимірюванням. Надлишкові запити отримують HTTP 503 — їх відхиляють, а не ставлять у чергу, тож з погляду користувача кнопка компіляції просто не спрацьовує. На відміну від усіх інших налаштовуваних параметрів у тому самому файлі, цей не читає жодної змінної середовища; його було запроваджено в upstream у серпні 2024 року, і змінити його можна лише модифікацією образу. Після підвищення цієї межі та сама гостьова система з 16 vCPU / 32 GiB, яка при N=80N=80 повідомляла success=65, unavailable=15, натомість повідомила success=80.

6.2 Недієве обмеження пам’яті контейнера

Огляд працюючого контейнера компіляції показує повну відсутність ізоляції ресурсів:
Відсутність будь-якої квоти CPU є задуманою й пояснює, чому показник степеня розподілу часу з §4.2 такий чистий: ніщо не спотворює конкуренцію між компіляціями. Проте відсутність обмеження пам’яті не є задуманою. Docker runner таки запитує його:
Тут дві помилки. Значення дорівнює 10244=1tebiB1024^4=1 tebiB, тоді як коментар має на увазі 102431024^3; крім того, поле розміщено на верхньому рівні параметрів створення, а не всередині HostConfig, де його очікує Docker API, тож воно відкидається, що й підтверджує спостережуване Memory=0. Обидві помилки присутні в коміті, який створив цей файл (9a519f0d3d, березень 2018 року), і пережили перетворення з CoffeeScript, переформатування всього репозиторію та міграцію з CJS на ESM, жодна з яких не переглядала семантику. Примітно, що MAX_OUTPUT = 1024 * 1024 // 1MB у тому самому коміті правильне, що вказує на описку, а не на нерозуміння. Наслідок видно в наших вимірюваннях з малим обсягом пам’яті. Оскільки компіляції нічим не обмежені, вичерпання пам’яті проявляється не як завершення Docker одного контейнера-порушника, а як падіння всієї гостьової системи. У конфігурації 2 vCPU / 2 GiB ми спостерігали, що SSH-сеанс моніторингу був заблокований на 300 s, середнє навантаження сягало 68 на двох ядрах, а гостьова система зрештою сама перезавантажилася. Працездатне обмеження на рівні контейнера давало б набагато м’якшу деградацію: завелика компіляція завершилася б збоєм, а сервіс уцілів би. Єдине обмеження, яке таки діє, — це RLIMIT_CPU, що дорівнює timeout+5\text{timeout}+5 секунд. Воно обмежує процесорний час, а не реальний, і одна компіляція споживає лише близько 9 s процесорного часу, тож воно ніколи не спрацьовує за жодної паралельності; воно захищає від патологічних вхідних даних, як-от макросу, що вийшов з-під контролю. Утім, воно є корисним індикатором: значення Soft:305 підтверджує, що налаштування тайм-ауту 300 s справді дійшло до контейнера.

6.3 Тайм-аут компіляції — головний налаштовуваний параметр

Поле користувача features.compileTimeout типово дорівнює 180 s. Для будь-якої конфігурації, обмеженої CPU, це не запас безпеки, а параметр пропускної здатності, оскільки машина, яка все ще коректно обчислює, оголошується такою, що дала збій. Збільшення його до 300 s — одне оновлення в MongoDB — змінює виміряну пропускну здатність аж у 4.2 раза (таблиця 3). Максимум становить 600 s і задається RequestParser.MAX_TIMEOUT; більші значення непомітно обрізаються. Таблиця 3. Вплив тайм-ауту компіляції на виміряну пропускну здатність. Останні два рядки — контрінтуїтивна половина результату й причина, через яку ми повторно виміряли кожну конфігурацію з одним тайм-аутом. Для конфігурацій, обмежених пам’яттю, довший тайм-аут зменшує пропускну здатність, оскільки кожна компіляція довше утримує свій резидентний набір і більше компіляцій перекриваються. Тому показник пропускної здатності не має сенсу без зазначення тайм-ауту, за якого його виміряно, і ці два випадки не можна змішувати в одній таблиці.

7. Пов’язані роботи

7.1 Рекомендації постачальника

Власна документація Overleaf щодо обладнання констатує якісні факти, які ми тут оцінюємо кількісно: LaTeX однопотоковий, тому час компіляції визначає продуктивність одного ядра, і “більше ядер допоможе лише тоді, коли ви намагаєтеся компілювати більше документів, ніж у вас є вільних ядер CPU” [1]. Далі вона наводить лінійне правило розрахунку — база 2 ядра/3 GiB плюс одне ядро й один гігабайт на п’ять–десять одночасних користувачів, — яке й стало мотивацією для цього дослідження. Наш внесок полягає в тому, щоб перетворити ці твердження на виміряні закони (рівняння (1) і (2)) і показати, де лінійне правило не працює: у ньому немає члена для спільного кешу сторінок, що робить стіну пам’яті надлінійною, і немає членів для двох програмних параметрів, які визначають результат.

7.2 Дослідження пропускної здатності систем збирання та CI

Вимірювання систем збирання за паралельного навантаження добре відпрацьоване поза контекстом LaTeX. LightSys повідомляє, що звичайні системи CI, які компілюють у контейнерах Docker, деградують за введенням-виведенням зі зростанням частоти надходження pull request-ів, і вузьке місце з’являється приблизно на одинадцяти одночасних запитах [17]; TAOS-CI зазначає, що компіляція домінує в загальному часі CI, становлячи 60–67% тривалості всього конвеєра для великих проєктів [18]. Наша система відрізняється в одному аспекті, який виявляється вирішальним: компіляція LaTeX інтерактивна. Завдання CI, яке триває вдвічі довше, — це незручність; компіляцію, що триває вдвічі довше, безпосередньо помічає користувач, який чекає на панель попереднього перегляду, — саме тому ми розглядаємо тайм-аут не як поріг збою, а як параметр пропускної здатності.

7.3 Накладні витрати контейнерів

Нещодавні роботи розкладають затримку запуску контейнерів Docker за рівнями сховища [19] і характеризують продуктивність контейнерів на периферії мережі [20]. У нашому випадку запуск контейнера для кожної компіляції амортизується: це невелика константа порівняно з 9-секундною компіляцією, і підібраний нами час вільного виконання T1T_1 її поглинає. Важливою властивістю контейнера є відсутність обмежень ресурсів (§6.2), через яку перевищення пам’яті однією компіляцією перетворюється на збій усього хоста.

7.4 LaTeX як недовірені вхідні дані

Ізольована компіляція існує тому, що TeX є мовою програмування, а документи — недовіреними вхідними даними [21, 22]. Саме це проєктне рішення уможливлює наше дослідження — кожна компіляція є ізольованим контейнером зі спостережуваною поведінкою ресурсів — і саме воно робить відсутність обмеження пам’яті суттєвою, оскільки оператори, які розгортають систему, припускають наявність ізоляції.

7.5 Компілятор як об’єкт дослідження

TeX як мову добре задокументовано [16], але його поведінка як цілі збирання привернула увагу лише нещодавно. Тан і Ріґґер [8] компілюють великий корпус джерел arXiv різними рушіями та версіями дистрибутивів і виявляють, що вибір рушія не є взаємозамінним: лише частка відсотка документів дає байт-ідентичний результат у XeTeX і pdfTeX. Цей результат безпосередньо стосується нашої методології. Пропускна здатність — це властивість документа і рушія, тож бенчмарк, який не фіксує обидва, не є відтворюваним; тому ми скрізь фіксуємо один документ, один рушій і один дистрибутив (texlive-full:2025.1) і зазначаємо рушій у підписі кожного рисунка. Це також обмежує загальність наших чисел, і це варто сказати прямо: вони характеризують XeLaTeX на цьому документі, а не TeX загалом. Роботи щодо систем збирання LaTeX здебільшого мають практичне походження. l3build від проєкту LaTeX3 [13] стандартизує регресійне тестування та пакування, а незалежні бенчмарки порівнюють інструменти-обгортки: огляд 26 систем збирання показує, що попередньо скомпільована преамбула дає приблизно 20% виграшу порівняно зі звичайним запуском і 40% порівняно з latexmk [14]. Ці роботи оптимізують окрему компіляцію. Вони ортогональні до того, що вимірюємо ми, і поєднуються з ним: кеш преамбули скорочує T1T_1, а кожен показник пропускної здатності в цій статті масштабується разом із T1T_1.

7.6 Керування паралельністю в редакторі, а не в компіляторі

Механізм спільної роботи в Overleaf спирається на добре розвинену лінію досліджень. Операційне перетворення (operational transformation) походить від Елліса та Ґіббса [9] і стало практичним для клієнтів з високою затримкою завдяки системі Jupiter [10], дизайн якої впізнається в document-updater: сервер, що впорядковує операції, і буфер для кожного документа, з яким синхронізуються клієнти. Безконфліктні репліковані типи даних (CRDT) [11] розв’язують ту саму задачу без центрального секвенсора. Саме ця відмінність робить топологію з §4.3 взагалі працездатною: оскільки буфер незастосованих оновлень зберігається в спільному Redis, а не в пам’яті екземпляра, компіляція, спрямована на будь-яку репліку, бачить останні натискання клавіш, і прив’язку компіляцій можна обирати з міркувань локальності кешу, а не коректності.

7.7 Моделі пропускної здатності

Закон Амдала [24] обмежує прискорення від паралелізму, а закон Літтла [23] пов’язує зайнятість із частотою надходження та часом обслуговування; обидва використано вище. Універсальний закон масштабованості Ґантера [12] розширює перший членом регресії для затримки узгодженості, передбачаючи, що пропускна здатність досягає піку, а потім знижується. Зазначимо, що наша система не демонструє цього регресивного режиму аж до N=1024N=1024: пропускна здатність насичується, а затримка зростає, але ніщо не обвалюється. Причина структурна, а не випадкова: компіляції не мають спільного стану, який потрібно підтримувати узгодженим, тож член, який додає закон, близький до нуля, а плато прийому з §4.3 обмежує конкуренцію раніше, ніж вона могла б стати суттєвою.

8. Рекомендації для операторів

1

Виправте два програмні параметри, перш ніж купувати обладнання

Обидва безкоштовні, і кожен важить більше, ніж будь-яке окреме оновлення обладнання, яке ми вимірювали. Збільште features.compileTimeout до значення, яке ваші користувачі справді готові терпіти, — максимум, який приймає CLSI, становить 600 s, — і, якщо очікуєте понад 65 одночасних компіляцій, або підвищте compileConcurrencyLimit у похідному образі, або масштабуйтеся горизонтально. Якщо не зробити ні того, ні іншого, ви платитимете за ядра, які програмне забезпечення відмовляється використовувати.
2

Розраховуйте одну машину за точкою перегину, а не за стелею

Розгортка на великому сервері (§4.3) розділяє два числа, які зазвичай плутають. Стеля — найбільша паралельність, за якої все ще повертається кожен PDF, — на 64-ядерному сервері становить щонайменше 1024, і ми її так і не досягли. Перегин — точка, за якою хвостова затримка перестає плавно зростати й починає подвоюватися, — розташований на 512, а остання комфортна робоча точка нижче нього — 256. Між N=256N=256 і N=512N=512 очікування p95p_{95} зростає з двох хвилин майже до шести; між 512 і 1024 воно досягає восьми. Оператор, який розраховує систему за стелею, постачає систему, яка технічно працює, але якою ніхто не хоче користуватися.Тому для цієї машини й цього документа рекомендована робоча точка — 256 одночасних компіляцій, що в 4×4\times більше за кількість фізичних ядер і в 2×2\times більше за кількість потоків і утримує p95p_{95} близько 120 s. Ми радимо задати compileConcurrencyLimit саме таким, а не залишати високим: прийом 1024 компіляцій одночасно змушує всіх чекати вісім хвилин, тоді як прийом 256 із постановкою решти в чергу обслуговує більшість користувачів за дві. Черга погіршує ситуацію для тих, хто прийшов пізно; конкуренція — для всіх.
3

Сприймайте ці числа як найгірший випадок

Кожен рівень у таблиці 2 — це холодна компіляція, запущена одночасно. Жодна з цих умов не виконується у виробничому середовищі: прогріта компіляція того самого документа займає 8.6 s проти 28.3 s у холодному стані, тобто в 3.33.3 раза менше, а реальні користувачі не натискають кнопку в ту саму секунду. Тому стабільна сукупність користувачів, які перекомпілюють документ кожні дві хвилини з типовою часткою влучань у кеш, витримає значно більше авторів, ніж підказує саме число паралельності, — близько тисячі чи більше активних авторів у робочій точці 256. Показник паралельності — це межа миттєвого сплеску, а не кількість місць.
4

Спершу визначте бюджет затримки, а потім визначте розмір

Рівняння (1) безпосередньо обертається. Для цільового очікування TT на частоті ff на CC ядрах допустима паралельність становить N≤C fT/kN \le C\,fT/k, де k≈28GHz⋅sk\approx28 GHz·s для цього документа. Бюджет 60 s на 8 ядрах за 3 GHz дає N≤51N\le51; бюджет 120 s подвоює це значення. Публікація бюджету поряд із пропускною здатністю — єдиний чесний спосіб указати будь-яке з цих значень.
5

Купуйте спершу пам'ять, потім ядра, і перевіряйте, у яку стіну ви впираєтеся

Нижче 32 GiB ми майже не виміряли користі від додаткових ядер. Діагностика проста: якщо збої проявляються як HTTP 502 і гостьовій системі бракує пам’яті, додайте пам’ять; якщо вони проявляються як timedout, а пам’ять ще є, додайте ядра або збільште тайм-аут. Оператори можуть визначити це за тією самою сигнатурою збою, яку ми використовували для класифікації конфігурацій.
6

Обирайте частоту для зручності, ядра — для кількості користувачів

Оскільки T∝1/fT\propto 1/f виконується без кривини (2% у діапазоні 1.0–5.5 GHz), вища частота пришвидшує кожну компіляцію для кожного користувача. Більша кількість ядер не пришвидшує жодної окремої компіляції; вона лише дає змогу приймати більше паралельних. Розгортанням, де скаржаться, що “компіляції повільні”, варто купувати частоту; розгортанням, де скаржаться, що “компіляції падають перед дедлайном”, — пам’ять і ядра.
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 одночасних компіляцій, що саме по собі свідчить про те, що обмежувальним чинником був демон, а не пам’ять.
Рисунок 10. Еталонна топологія горизонтально масштабованого розгортання, побудована на основі перевіреної нами конфігурації. Репліки застосунку взаємозамінні й не зберігають нічого довготривалого, тож їх можна вільно додавати й видаляти. Три компоненти такими не є: Redis, чий буфер документів дає змогу компіляції, спрямованій на будь-яку репліку, бачити останні натискання клавіш; об’єктне сховище, яке за більш ніж однієї репліки стає обов’язковим, а не необов’язковим; і 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% проєктів, тоді як cookie — 0%. Трафік сеансів інший. Коли оновлення до WebSocket не вдається і socket.io переходить до XHR-опитування, послідовні опитування одного сеансу мають потрапляти на одну репліку, а в шляху немає ідентифікатора проєкту для хешування. Цей трафік потребує окремого бекенду з прив’язкою через cookie. Ми спроєктували такий поділ, але не розгорнули його; ми позначаємо це як прогалину, а не стверджуємо, що це зроблено. Нарешті, /git/ має потрапляти на репліку, поруч з якою працює git-bridge. Він маршрутизується на цю репліку, а не безпосередньо на git-bridge, оскільки міст автентифікує свої зворотні виклики через OAuth-ендпоінти застосунку й розпізнає URL-адреси blob-об’єктів через нього; обхід репліки порушує автентифікацію, нічого не покращуючи.

9.4 Зменшення масштабу потребує буфера для завершення

Видалення репліки не симетричне її додаванню: компіляція, що виконується, втрачається, і користувач бачить збій, якого не спричиняв. Робоча послідовність — спершу зупинити новий трафік, зачекати й лише потім завершити роботу репліки. Ми реалізували це як pre-stop hook, що утримує под протягом налаштовуваного інтервалу, поки балансувальник позначає бекенд як такий, що завершує роботу (draining), — досить короткого, щоб протестувати за кілька хвилин, а у виробничому середовищі досить довгого для природного завершення сеансу, тобто годин, а не секунд. Саме цей інтервал визначає, чи буде еластичність непомітною, чи дратівливою. Ще одне обмеження ми виявили вимірюванням, а не під час проєктування: автомасштабування за CPU для цього навантаження не працює. Власне використання поду застосунку становило 22 m core проти загальних 3997 m core на вузлі, оскільки робота з компіляції відбувається в сусідніх контейнерах, які под не враховує. Будь-який сигнал для масштабування цього рівня має враховувати кількість запущених контейнерів компіляції, а не CPU поду.

10. Наслідки за межами Overleaf

Ніщо в §4.2 чи §4.3 не є специфічним для коду Overleaf. Виміряні закони випливають із трьох властивостей, спільних для будь-якого хостингового сервісу LaTeX: одиницею роботи є однопотоковий процес, він ізольований у контейнері, а його робочий набір — велике дерево лише для читання, яке має вміщатися в кеші сторінок. Три наслідки безпосередньо стосуються кожного, хто створює такий сервіс.

10.1 Забезпечте пам’ять, потім ядра

Найсильніший результат матриці — негативний: нижче 16 GiB кількість ядер майже не має значення, і лише за 48 GiB конфігурації з 4, 8 і 16 vCPU взагалі розходяться (143, 268, 331). Оператор, який розуміє загальноприйняте правило як “додати ядро на кожні п’ять користувачів”, купує не той ресурс. Механізмом є спільний кеш сторінок для дерева дистрибутива, і це властивість розміру TeX Live, а не будь-якого конкретного фронтенду.

10.2 Швидкість прийому — це ресурс, про який зазвичай забувають

При N=1024N=1024 наш сервер ніколи не утримував понад 205 активних пісочниць (рисунок 7b), хоча всі запити надійшли одночасно. Обмежувачем було створення контейнерів, а не компіляція, — що узгоджується з дослідженнями, які пов’язують вартість запуску контейнерів з накладними витратами середовища виконання, а не з розміром образу [19, 20]. Сервіс, який розраховує лише CPU і пам’ять, виявить, що його поведінкою під час сплесків керує величина, яку він ніколи не вимірював. Практична форма цього — рекомендація з §8: обмежуйте прийом свідомо, бо черга, яку ви обрали, краща за чергу, яку ви виявили.

10.3 Пісочниця, що переживає свою компіляцію, робить модель недійсною

Кожен показник пропускної здатності тут передбачає, що контейнер створюється, виконує одну компіляцію й завершується — тривалість життя в десятки секунд і коефіцієнт заповнення, близький до одиниці, лише під час роботи. Два нещодавні патерни проєктування порушують це припущення, і порушують його однаково. Перший — постійна пісочниця для кожного користувача. Виділення кожному користувачеві фіксованого приватного середовища перетворює статистично мультиплексований пул на набір резервувань: сервіс, який міг би обслуговувати 256 паралельних компіляцій на 64 ядрах завдяки розподілу часу, зможе обслужити лише 16 користувачів, якщо кожному виділити чотири окремі ядра, — на порядок менше на тому самому обладнанні. Наші дані кількісно оцінюють вартість такого вибору, а не заперечують його: резервування дають передбачуваність, і курс обміну становить приблизно 16×16\times у рекомендованій нами робочій точці. Другий, новіший, — ШІ-агент, який ділить пісочницю з компілятором. На платформах для написання текстів за допомогою агентів той самий контейнер, що виконує XeLaTeX, може також розміщувати довготривалого агента для програмування, тож він зайнятий безперервно, а не сплесками. Практики повідомляють саме про той симптом, який модель передбачає для таких розгортань, — стабільну повільність за помірної кількості користувачів [15]. Цю взаємодію варто сформулювати точно, бо це не просто “більше навантаження”. Три наші висновки посилюють один одного. Зайнятість перестає бути сплесковою, тож закон розподілу часу з §4.2 застосовується до всієї сукупності користувачів одночасно, а не лише до частки тих, хто зараз компілює. Кеш сторінок, який забезпечує надлінійну віддачу від пам’яті з §4.1, тепер ділиться з власним робочим набором агента й перестає бути прогрітим для TeX. А відсутність обмеження пам’яті контейнера з §6.2 стає набагато небезпечнішою, бо контейнер, який ніколи не завершується, ніколи не повертає свою пам’ять. Ми не вимірювали таку платформу й не робимо жодних тверджень щодо будь-якого конкретного продукту. Ми можемо сказати лише те, що наші числа означають для проєктування: архітектуру, яка надає кожному користувачеві довготривалу багатоядерну пісочницю, слід розраховувати як систему резервувань, а не за наведеними тут показниками паралельності, і пропускна здатність, на яку вона може розраховувати, ближча до кількості ядер, поділеної на кількість ядер на користувача, ніж до будь-чого з таблиці 1.

11. Загрози валідності

11.1 Один документ

Усі вимірювання використовують один 63-сторінковий документ XeLaTeX. Абсолютні значення пропускної здатності для інших документів відрізнятимуться; закони масштабування, які є співвідношеннями, не повинні. Документ зі значно більшим резидентним набором зсунув би стіну пам’яті, не змінивши її надлінійного характеру.

11.2 Віртуалізований хост

Гостьові системи працюють під KVM на одній фізичній машині, тож абсолютні числа включають накладні витрати віртуалізації, а гостьові системи спільно використовують кеш сторінок хоста й пристрій NVMe. Ми пом’якшили найбільший спотворювальний чинник, вимкнувши сторонні гостьові системи після того, як помітили, що тиск на пам’ять хоста завищує середнє навантаження всередині гостьової системи більш ніж у 3×3\times за однакової паралельності.

11.3 Одночасне надходження

Кожна компіляція запускається в один момент, що є найгіршим випадком. Реальні користувачі надходять як стохастичний процес, тож розгортання, розраховане за нашими числами, має запас, а не дефіцит, — але пік наприкінці терміну подання ближчий до нашої моделі, ніж до пуассонівської.

11.4 Граничні конфігурації

За 2 GiB система настільки близька до колапсу, що повторні запуски тієї самої конфігурації можуть відрізнятися на одну компіляцію. Ми наводимо консервативне значення й не робимо висновків з різниць ±1\pm 1 у цьому режимі.

12. Доступність

Досліджувана система, інструменти розгортання та upstream-проєкт, від якого вона походить, є загальнодоступними: Кожне місце у вихідному коді, на яке ми посилаємося, наведено як шлях відносно репозиторію з номером рядка для Ayakaleaf Pro v6.2.2, а два upstream-коміти, які ми датуємо (9a519f0d3d, 5d472e9b38), доступні в історії Overleaf.

13. Внесок авторів

Musicminion спроєктував дослідження, надав і обслуговував тестові стенди, визначав напрям досліджень і перевірив кожне наведене тут вимірювання. Claude Opus 5 (Anthropic) створив і обслуговував тестову обв’язку бенчмарку, автоматизував розгортання, провів археологію вихідного коду, підготував рисунки й написав чернетку рукопису. Обидва автори переглянули остаточний текст. Там, де запуск позначено як забруднений — розгортка з 1021 сеансом у §4.3 та аномальний рівень N=256N=256 у §4.1, — дефект було виявлено під час перевірки, і запуск було повторено перед публікацією, а не непомітно відкинуто. Читачам слід зважати, що правила авторства ACM, IEEE та ICMJE наразі резервують авторство за сторонами, здатними нести відповідальність за роботу, і вимагали б зазначати внесок другого автора як розкриття інформації, а не в списку авторів. Ми явно описуємо тут розподіл праці, щоб запис був точним за будь-якої з цих конвенцій.

14. Висновок

Планування потужностей для самостійно розміщеного Overleaf — це не питання масштабування одного ресурсу. Три висновки мають змінити підхід до нього. По-перше, нижче 32 GiB гостьової пам’яті кількість ядер майже не має значення: за 16 GiB пропускна здатність гостьових систем із 4, 8 і 16 vCPU відрізняється менш ніж на 8%. Межу задає пам’ять — через спільний кеш сторінок для дерева TeX Live; ядра починають мати значення лише тоді, коли пам’яті вдосталь. По-друге, два програмні параметри важать більше за обладнання. Зняття жорстко закодованої в CLSI межі в 65 компіляцій і збільшення типового тайм-ауту компіляції в 180 s підняли пропускну здатність гостьової системи з 8 vCPU / 48 GiB з 64 до 268 паралельних компіляцій — у 4.2 раза без додаткового обладнання. Жоден із них не можна виявити з документації з конфігурації; один узагалі не налаштовується. По-третє, питання “скільки одночасних користувачів підтримує ця машина” недостатньо визначене. Паралельність у цій системі — це чистий розподіл часу, а пропускна здатність — це те, що допускає тайм-аут. Чесна форма відповіді вказує обидва значення: ця машина обслуговує NN одночасних компіляцій, якщо користувачі готові чекати TT секунд, де NN і TT пов’язані рівнянням (1). Ми також повідомляємо про прихований дефект: обмеження пам’яті на контейнер у Docker runner не діє з 2018 року — як через значення, так і через місце застосування. Його практичний наслідок полягає в тому, що вичерпання пам’яті в невеликому розгортанні валить увесь сервіс, а не лише одну відповідальну за це компіляцію.

Джерела

[1] Overleaf. Hardware requirements, документація для локального розгортання. https://docs.overleaf.com/on-premises/getting-started/requirements/hardware-requirements [2] Overleaf. Horizontal scaling, документація для локального розгортання. https://docs.overleaf.com/on-premises/maintenance/horizontal-scaling [3] Overleaf. Microservices, документація для локального розгортання. https://docs.overleaf.com/on-premises/getting-started/microservices [4] Overleaf. Репозиторій вихідного коду. https://github.com/overleaf/overleaf [5] Ayaka-notes. Ayakaleaf Pro. https://github.com/ayaka-notes/ayakaleaf-pro [6] Ayaka-notes. Overleaf Toolkit. https://github.com/ayaka-notes/toolkit [7] D. Karger, E. Lehman, T. Leighton, R. Panigrahy, M. Levine and D. Lewin. Consistent Hashing and Random Trees: Distributed Caching Protocols for Relieving Hot Spots on the World Wide Web. STOC, 1997. [8] J. Tan and M. Rigger. Inconsistencies in TeX-Produced Documents. In Proc. 33rd ACM SIGSOFT International Symposium on Software Testing and Analysis (ISSTA), Vienna, 2024. doi: https://doi.org/10.1145/3650212.3680370 [9] C. A. Ellis and S. J. Gibbs. Concurrency Control in Groupware Systems. In Proc. ACM SIGMOD, pp. 399–407, 1989. [10] D. A. Nichols, P. Curtis, M. Dixon and J. Lamping. High-Latency, Low-Bandwidth Windowing in the Jupiter Collaboration System. In Proc. ACM UIST, pp. 111–120, 1995. [11] M. Shapiro, N. Preguiça, C. Baquero and M. Zawirski. Conflict-Free Replicated Data Types. In Proc. SSS, pp. 386–400, 2011. [12] N. J. Gunther. Guerrilla Capacity Planning: A Tactical Approach to Planning for Highly Scalable Applications and Services. Springer, 2007. [13] The LaTeX3 Project. l3build — A Testing and Building System for (La)TeX. CTAN. [14] M. Isaksson. Which LaTeX Build System Is Fastest? A Benchmark. https://blog.martisak.se/latex-build-systems-comparison/ [15] Звіти практиків про стабільну затримку на платформах для написання текстів за допомогою агентів, які розміщують постійного агента для програмування разом із компілятором LaTeX в окремій пісочниці для кожного користувача. Ми посилаємося на це як на описаний операційний досвід, а не як на контрольоване вимірювання; ми не проводили бенчмарк такої платформи. [16] D. E. Knuth. The TeXbook. Addison-Wesley, 1984. [17] G. Lim, M. Ham, J. Moon and W. Song. LightSys: Lightweight and Efficient CI System for Improving Integration Speed of Software. arXiv:2101.07961 [cs.SE], 2021. Препринт. [18] G. Lim, M. Ham, J. Moon, W. Song, S. Woo and S. Oh. TAOS-CI: Lightweight & Modular Continuous Integration System for Edge Computing. arXiv:2101.08889 [cs.SE], 2021. Препринт. [19] S. Khan. Decomposing Docker Container Startup Performance: A Three-Tier Measurement Study on Heterogeneous Infrastructure. arXiv:2602.15214, 2026. Препринт. [20] R. Gupta and K. Nahrstedt. Performance Characterization of Containers in Edge Computing. arXiv:2505.02082, 2025. Препринт. [21] S. Checkoway, H. Shacham and E. Rescorla. Are Text-Only Data Formats Safe? Or, Use This LaTeX Class File to Pwn Your Computer. In Proc. USENIX Workshop on Large-Scale Exploits and Emergent Threats (LEET), 2010. [22] G. Lacombe, K. Masalygina, A. Tahiri, C. Adam and C. Lauradoux. Can You Accept LaTeX Files from Strangers? Ten Years Later. arXiv:2102.00856 [cs.CR], 2021. Препринт. [23] J. D. C. Little. A Proof for the Queuing Formula L=λWL=\lambda W. Operations Research, 9(3):383–387, 1961. [24] G. M. Amdahl. Validity of the Single Processor Approach to Achieving Large Scale Computing Capabilities. AFIPS, 1967.
Останнє оновлення 5 жовтня 2026 р.