> For the complete documentation index, see [llms.txt](https://ayakaleaf-pro.ayaka.space/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://ayakaleaf-pro.ayaka.space/blog/de/2026/overleaf-benchmark.md).

# Overleaf-Benchmark: Eine tiefgehende Untersuchung der gleichzeitigen LaTeX-Kompilierung in Overleaf (SaaS und Self-host)

{% file src="/files/729198c66978717d86fe825bb3d0f04df816ca43" %}

## Zusammenfassung

Selbst gehostete Overleaf-Bereitstellungen werden üblicherweise mit einer einfachen Faustregel dimensioniert: ein CPU-Kern und ein Gigabyte Speicher pro fünf bis zehn gleichzeitige Nutzer. Wir zeigen, dass diese Regel nicht nur ungenau, sondern strukturell falsch ist, weil sie annimmt, dass eine einzige Ressourcendimension die Kapazität bestimmt, obwohl tatsächlich zwei unabhängige Grenzen dies tun, und weil zwei Softwareparameter — keiner davon Hardware — das Ergebnis um Faktoren von bis zu vier dominieren.

Wir messen eine unveränderte Ayakaleaf-Pro-v6.2.2-Bereitstellung mit sandboxed compilation (TeX Live 2025) über 21 CPU-/Speicherkonfigurationen hinweg innerhalb von QEMU/KVM-Gästen, deren Host-Kerne auf 3,0 GHz festgetaktet sind. Die Arbeitslast ist eine reale 63-seitige XeLaTeX-Dissertation, die gleichzeitig von bis zu mehreren hundert verschiedenen Benutzerkonten kompiliert wird. Wir finden, dass unterhalb von 32 GiB Gästen-Speicher die Kernanzahl fast irrelevant ist — bei 16 GiB unterscheidet sich die gemessene Kapazität von 4-, 8- und 16-vCPU-Gästen um weniger als 8 % — und dass die Kapazität stattdessen von einer überlinearen Speichergrenze bestimmt wird, die aus dem gemeinsam genutzten Page Cache über den TeX-Live-Baum entsteht.

Um zu untersuchen, ob diese Gesetze einen Größenordnungswechsel überstehen, wiederholen wir den Sweep auf einem einzelnen 64-Kern-, 995-GiB-Server. Er hält 1024 gleichzeitige kalte Kompilierungen mit 100 % Erfolg durch — achtmal seine Thread-Anzahl — und wir erreichen nie seine Obergrenze. Die nützliche Zahl ist nicht diese Obergrenze, sondern der Knick darunter: die Tail-Latenz wächst bis $$N=256$$ dann um 190 % bei $$N=512$$. Kapazität, angegeben als „die größte Nebenläufigkeit, die nicht fehlschlägt“, würde daher den nutzbaren Betriebspunkt um den Faktor vier überschätzen. Auf dieser Maschine ist Speicher nie die begrenzende Ressource; die Grenze ist die CPU zusammen mit der Rate, mit der der Container-Daemon neue Sandboxes zulassen kann, was sich unabhängig davon, wie viele Kompilierungen angefordert werden, bei etwa 200 sättigt.

Wir identifizieren außerdem zwei Implementierungseffekte auf der Ebene des Codes, die für die Kapazitätsplanung unsichtbar sind. Erstens erzwingt CLSI eine hart kodierte Obergrenze von 65 gleichzeitigen Kompilierungen, die über keine Umgebungsvariable zugänglich ist; darüber erhalten Benutzer sofort HTTP 503 statt in eine Warteschlange gestellt zu werden. Zweitens ist das pro-Container-Speicherlimit im Docker-Runner seit seiner Einführung im Jahr 2018 sowohl in der Größenordnung als auch in der Platzierung wirkungslos, sodass ein Out-of-Memory-Ereignis den gesamten Host und nicht nur eine einzelne Kompilierung außer Gefecht setzt. Das Anheben der Nebenläufigkeitsobergrenze und des Standard-Compile-Timeouts von 180 s auf 300 s erhöht die gemessene Kapazität eines 8-vCPU-/48-GiB-Gasts von 64 auf 268 gleichzeitige Kompilierungen — ein Faktor von 4,2 ohne Hardwarekosten.

Schließlich zeigen wir, dass Nebenläufigkeit in diesem System nichts anderes als Zeitscheibung bringt und dass die Arbeitslast allein durch die Taktfrequenz begrenzt ist. Ein angepasstes Degradationsgesetz $$T(N)=T\_1\max(1,N/C)^{b}$$ liefert $$b=0.914$$, nahe an einer perfekten proportionalen Verlangsamung, und ein Takt-Sweep über den gesamten 1,0–5,5-GHz-Bereich der Maschine bringt dreißig Messungen auf $$T=(k/f)\max(1,N/C)$$ mit $$k=27.9 GHz·s$$ und einer Reststreuung von 5,1 %. Ein 5,5×-Takt bringt eine 5,5×-Beschleunigung ohne abnehmenden Ertrag, und darin liegt der Sinn, in dem Takt und Kerne unterschiedliche Dinge kaufen: Der Takt macht die Kompilierung jedes Benutzers schneller, Kerne lassen nur mehr Benutzer zu.

## 1. Einleitung

Overleaf ist der dominierende kollaborative LaTeX-Editor, und seine On-Premises-Distribution wird häufig von Universitäten und Forschungsgruppen eingesetzt, die unveröffentlichte Manuskripte nicht an eine Cloud eines Drittanbieters senden können. Die Dimensionierung einer solchen Bereitstellung ist eine wiederkehrende praktische Frage: Wie viele Personen können bei einem festen Hardware-Budget tatsächlich gleichzeitig auf „Erneut kompilieren“ drücken?

Die offizielle Empfehlung ist eine lineare Regel — grob ein Kern und ein Gigabyte pro fünf bis zehn gleichzeitige Nutzer —, die voraussetzt, dass die Kapazität in beiden Ressourcen gleichmäßig und gemeinsam skaliert. Unsere Messungen widersprechen dem in drei Punkten.

### 1.1 Die Kapazität wird von zwei unabhängigen Grenzen bestimmt, nicht von einer

Eine Konfiguration schlägt entweder fehl, weil der Speicher erschöpft ist, wobei der Overleaf-Stack selbst abstürzt und HTTP 502 zurückgibt, oder weil Kompilierungen das serverseitige Timeout überschreiten, wobei CLSI `timedout`  meldet, während Gigabytes an Speicher ungenutzt bleiben. Diese beiden Regime haben völlig unterschiedliches Skalierungsverhalten und unterschiedliche Abhilfen. Einer speichergebundenen Konfiguration weitere Kerne hinzuzufügen ist nicht nur ineffizient, sondern gelegentlich kontraproduktiv: Wir messen Konfigurationen, bei denen eine Erhöhung der Kernzahl *die Kapazität* verringert, weil mehr Kerne dazu führen, dass gleichzeitige Kompilierungen im Gleichschritt voranschreiten, sodass ihre Spitzen-Speicheranforderungen zusammenfallen statt sich zu verschieben.

### 1.2 Softwareparameter dominieren die Hardware

Das Compile-Timeout ist ein pro Benutzer festgelegtes Feld in MongoDB, dessen Standardwert von 180 s CPU-gebundene Konfigurationen stillschweigend begrenzt. Das Anheben auf 300 s vervielfacht die gemessene Kapazität auf unveränderter Hardware um bis zu 4,2. Unabhängig davon verweigert CLSI mehr als 65 gleichzeitige Kompilierungen durch eine hart kodierte Konstante. Jede Kapazitätsstudie — und jede Bereitstellung —, die beide nicht berücksichtigt, misst die Software und nicht die Maschine.

### 1.3 Nebenläufigkeit ist Zeitscheibung, nicht Parallelität

Weil eine LaTeX-Kompilierung nur einen Thread verwendet, macht das Bereitstellen von $$N$$ gleichzeitigen Nutzern auf $$C$$ Kernen das System nicht schneller fertig; es lässt jeden Nutzer proportional länger warten. Die Frage „Wie viele gleichzeitige Nutzer werden unterstützt“ ist daher schlecht gestellt, solange nicht festgelegt ist, wie lange ein Nutzer bereit ist zu warten. Wir machen diese Abhängigkeit explizit und quantifizieren sie.

### 1.4 Beiträge

* Eine Kapazitätsmatrix über 21 CPU-/Speicherkonfigurationen, gemessen unter fest getakteten und wiederholungsgeprüften Bedingungen, wobei die begrenzende Ressource je Konfiguration anhand ihrer Fehlersignatur identifiziert wird.
* Zwei angepasste Modelle: ein Kapazitätsmodell, das eine überlineare Speichergrenze von einer CPU-Obergrenze trennt, und ein Latenzmodell, das reines Time-Sharing-Verhalten belegt.
* Die Identifizierung und experimentelle Bestätigung von zwei Implementierungsproblemen im eingesetzten System, einschließlich eines Container-Speicherlimits, das seit 2018 nicht mehr funktioniert.
* Eine Quantifizierung des Trade-offs zwischen Compile-Timeout und Kapazität, die nach unserer Auffassung zusammen mit jeder Nebenläufigkeitszahl angegeben werden muss.

## 2. Hintergrund

### 2.1 Kompilierungspfad

Eine Overleaf-Kompilierungsanfrage läuft über `web` $$\rightarrow$$ `clsi` $$\rightarrow$$  in einen Compile-Container. In einer Bereitstellung mit sandboxed compiles (`SIBLING_CONTAINERS_ENABLED=true`) führt CLSI `latexmk`  nicht im Prozess aus; es fordert den Host-Docker-Daemon, der über einen per Bind-Mount eingebundenen Socket erreichbar ist, auf, einen frischen Container aus einem TeX-Live-Image zu starten, wobei das Projektverzeichnis unter `/compile` eingehängt wird. Eine Kompilierung ist daher ein kurzlebiger Container, der einen `latexmk` Prozess

&#x20;ausführt. Daraus folgen drei Konsequenzen, und alle drei prägen die Messungen in diesem Papier. Erstens ist die Arbeitseinheit ein Single-Thread-Prozess: XeLaTeX parallelisiert nicht. Zweitens ist die pro Kompilierung gewährte Ressourcenisolierung genau das, was der Docker-Runner anfordert — wir zeigen in §6.2, dass er faktisch nichts anfordert. Drittens wird der Arbeitssatz nicht vom Dokument, sondern vom TeX-Live-Baum dominiert, einem schreibgeschützten Korpus von etwa 32 GiB, aus dem jede gleichzeitige Kompilierung liest und den sie daher über den Host-Page-Cache gemeinsam nutzt. Diese gemeinsame Nutzung ist die Ursache der überlinearen Speicher-Skalierung, die wir beobachten.

### 2.2 Sandbox-Kompilierungen aktivieren

Die Community-Edition von Overleaf läuft `latexmk`  im Anwendungscontainer selbst. Ayakaleaf Pro kann wie Overleaf Server Pro stattdessen jede Kompilierung in einem *Geschwister-* Container ausführen — einem Container, der von der Anwendung auf dem *Host-* Docker-Daemon gestartet wird, statt innerhalb des Anwendungscontainers verschachtelt zu sein. Zwei Toolkit-Einstellungen schalten dies ein:

```ini
# config/overleaf.rc
SERVER_PRO=true
SIBLING_CONTAINERS_ENABLED=true
DOCKER_SOCKET_PATH=/var/run/docker.sock

# config/variables.env
TEX_LIVE_DOCKER_IMAGE=ghcr.io/ayaka-notes/texlive-full:2025.1
ALL_TEX_LIVE_DOCKER_IMAGES=ghcr.io/ayaka-notes/texlive-full:2025.1
```

Das Toolkit bind-mountet den Docker-Socket des Hosts in den Anwendungscontainer und übersetzt dies in die Umgebungsvariablen, die CLSI liest: `SANDBOXED_COMPILES=true`, `SANDBOXED_COMPILES_SIBLING_CONTAINERS=true` und `SANDBOXED_COMPILES_HOST_DIR`, wobei letzteres der *Host-* Pfad des Compile-Verzeichnisses ist. Dieser Pfad ist wichtig: Weil der Daemon, der den Compile-Container startet, dem Host gehört, muss der ihm gegebene Bind-Mount im Namespace des Hosts und nicht im Namespace des Anwendungscontainers auflösbar sein. Die config/env.sh von Server Pro erzwingt in diesem Modus zusätzlich `config/env.sh` TEXLIVE\_IMAGE\_USER=www-data `sodass Dateien, die vom Compile-Container geschrieben werden, konsistent dem richtigen Benutzer gehören.` Die Verifikation ist direkt: Während einer Kompilierung zeigt der Host einen Container mit dem Namen

project-{projectId}-{userId}-{hash} `an` laufen `latexmk`  aus dem TeX-Live-Image, der mit Exit-Code 0 endet. Dies ist die Einheit, deren Vielfachheit wir im gesamten Papier messen, und deren vollständigen Mangel an Ressourcenlimits wir in §6.2 berichten.

**Warum das für die Studie wichtig ist.**

Geschwister-Container machen die Messung sauber — jede Kompilierung ist eine beobachtbare, unabhängig geplante Betriebssystem-Entität —, aber sie bedeuten auch, dass der Gastkernel, nicht Overleaf, die CPU und den Speicher zwischen den Kompilierungen aushandelt. Jedes Skalierungsgesetz in diesem Papier ist daher eine Eigenschaft des Linux-Schedulers angewendet auf $$N$$ Single-Thread-Prozesse, weshalb es so regelmäßig ist.

<figure><img src="/files/5d39043f0f32b1ec755c2edccc08de33b6150203" alt=""><figcaption></figcaption></figure>

**Abbildung 1.** Eine Kompilierungsanfrage, nachverfolgt durch die Microservices der Community-Edition. Die Trennung in Schritte ist für die Kapazität relevant: Der Dokumenttext wird in den Request-Body kopiert, während binäre Assets per Referenz übergeben und von `clsi` abgerufen werden. Keines von beiden dominiert — die Compile-Kosten eines Projekts werden durch den 32-GiB-TeX-Live-Baum bestimmt, den jede gleichzeitige Kompilierung über den gemeinsamen Page-Cache liest.

<figure><img src="/files/d71b0470e036b6c6b9389beadadf59845bf78b10" alt=""><figcaption></figcaption></figure>

**Abbildung 2.** Drei Bereitstellungs-Topologien und wo die pro Instanz geltende Compile-Obergrenze jeweils liegt; gestapelte Panels bezeichnen Replikation. Die 65-Compile-Konstante schützt *eine* CLSI, sodass die SaaS-Flotte sie mit Instanzen und Zonen multipliziert (a), und die von Server Pro und Ayakaleaf Pro unterstützte horizontale Skalierung multipliziert sie mit Instanzen (c) — um den Preis von zentralem MongoDB-, Redis- und S3-kompatiblem Speicher, einem Load Balancer mit Cookie-Session-Affinität (Compile-Ausgabe wird auf instanzlokalen Speicher geschrieben, daher müssen eine Kompilierung und der anschließende PDF-Download auf derselben Instanz landen) und einem Singleton `git-bridge`. Die Toolkit-Standardkonfiguration (b), die wir messen, hat einen Multiplikator von eins, sodass eine für ein Mitglied einer Flotte ausgelegte Konstante zur Obergrenze der gesamten Installation wird.

<figure><img src="/files/c1e98376da3f81bccb1241e7bfb43dd33eff5824" alt=""><figcaption></figcaption></figure>

**Abbildung 3.** Shard-Auswahl in `clsi-cache`. Ein Projekt wird durch $$\operatorname{crc32}(\text{projectId}\text{-}i)\bmod|\text{shards}|$$ abgebildet, d. h. der Hashraum wird in ebenso viele gleich große Sektoren unterteilt, wie es Shards gibt. Das ist *Modulo-* Hashing, nicht ringbasiertes konsistentes Hashing: Wenn die Flotte von drei Shards auf vier wächst, wird der gesamte Raum neu partitioniert und praktisch jedes Projekt neu zugeordnet (a, b). Genau deshalb braucht die Implementierung eine explizite Online-Resharding-Rampe, die einen linear wachsenden Anteil der Projekte von `currentShards` zu `desiredShards`  über ein Zeitfenster verschiebt, statt der $$K/n$$ Bewegung, die ein konsistenter Hashing-Ring liefern würde. Wenn der Leistungsschalter eines Shards auslöst, wird das Salt $$i$$  inkrementiert und der Shard aus der Kandidatenliste entfernt, sodass die Suche stattdessen weiterprüft, anstatt zu scheitern (c).

### 2.3 Die beiden Fehlermodi

Jede von uns gemessene Konfiguration schlägt genau auf eine von zwei Arten fehl, und der Unterschied ist im Antwortstatus sichtbar statt nur abgeleitet zu werden:

* **Speichererschöpfung**  — der Overleaf-Stack selbst reagiert nicht mehr und die Anfrage liefert **HTTP 502**. Der verfügbare Gästespeicher auf der fehlschlagenden Ebene liegt typischerweise unter 500 MiB.
* **Compile-Timeout**  — CLSI beendet die Kompilierung beim pro Benutzer gesetzten Timeout und meldet Status `timedout`. Der verfügbare Speicher auf der fehlschlagenden Ebene liegt oft bei mehreren Gigabyte.

Wir klassifizieren jede Konfiguration anhand dieser Signatur statt anhand einer Heuristik über Ressourcenverhältnisse, was die Frage „an welche Wand sind wir gestoßen“ direkt aus den Daten beantwortbar macht.

## 3. Methodik

### 3.1 Testumgebung und Taktsteuerung

Alle Gäste laufen unter QEMU/KVM auf einem einzelnen Intel-Core-i9-14900K-Host mit 62 GiB RAM und NVMe-Speicher. Der Gast ist Ubuntu 24.04 mit Docker 29.7 und dem Overleaf Toolkit, das Ayakaleaf Pro v6.2.2 mit sandboxed compiles gegen `texlive-full:2025.1`.

Eine handelsübliche Desktop-CPU ist nur dann ein schlechter Proxy für einen Server, wenn ihr Takt kontrolliert wird. KVM bietet keinen Mechanismus, um eine virtuelle Uhr einzustellen: Eine vCPU ist ein Host-Thread und läuft mit genau der Frequenz, mit der der Host-Kern läuft. Wir begrenzen daher den Host direkt, deaktivieren Turbo und setzen `scaling_max_freq`  auf 3,0 GHz auf jedem Kern, und pinnen die vCPUs des Gasts mit `taskset` an physische P-Kerne. Der Unterschied ist bei einer Hybrid-Core-CPU wichtig: Die E-Kerne dieses Modells haben eine Basis-Taktfrequenz von 2,4 GHz und *können*  3,0 GHz nicht erreichen, sobald Turbo deaktiviert ist, sodass ein Lauf, der auf sie gerät, stillschweigend eine langsamere Maschine misst. Unter Volllast verifizieren wir exakt 3000 MHz auf allen sechzehn gepinnten Threads. Ein Guard-Skript prüft diese Invariante vor jedem Benchmark und verweigert andernfalls den Start; es fing während der Studie einen stillen Reset des Governors auf.

### 3.2 Eine zweite Testumgebung: ein großer Runner

Die QEMU-Matrix isoliert jeweils eine Variable, aber sie endet bei sechzehn gepinnten Threads. Um zu fragen, ob dieselben Gesetze auch eine Größenordnung höher noch gelten, wiederholten wir den Nebenläufigkeits-Sweep auf einem einzelnen großen Server: einem AMD EPYC 7773X (Milan-X, 64 Kerne / 128 Threads, 768 MiB L3) mit 995 GiB RAM, auf dem dasselbe Ayakaleaf-Pro-v6.2.2-Image gegen dieselbe `texlive-full:2025.1` verwendet wird. Anders als die QEMU-Gäste ist diese Maschine nicht auf einen festen Takt gepinnt: Es handelt sich um einen Server der Produktionsklasse, und wir messen ihn als Ganzes.

Zwei betriebliche Vorsichtsmaßnahmen waren notwendig und sollten genannt werden, weil das Experiment ohne sie nicht den Server, sondern das Testgerüst misst. Erstens wurde jeder Container in einen `systemd` -Slice mit `MemoryMax=940 GiB` eingesperrt, sodass ein aus dem Ruder laufender Sweep ein cgroup-Limit und nicht den Host erschöpft. Zweitens werden sandboxed compiles vom Host-Daemon erstellt und verschmutzen jeweils ihre eigene Copy-on-Write-Schicht — gemessen mit 116 MiB pro Container, obwohl das 20,6-GiB-Basis-Image gemeinsam genutzt wird —, weshalb der Docker-Datenstamm auf ein dediziertes NVMe-Gerät verschoben wurde. Ein Sweep bei $$N=1024$$  schreibt ungefähr 119 GiB Scratch-Schichten, was nicht auf ein Standard-Root-Dateisystem passt.

### 3.3 Arbeitslast

Das Dokument ist eine reale 63-seitige Masterarbeit (SJTU-Vorlage), die mit XeLaTeX über `latexmk` kompiliert wird, mit TikZ-Abbildungen, `biblatex` -Bibliographie-Verarbeitung und eingebetteten PDF-Assets — also einer realistischen und nicht synthetischen Last. Eine einzelne Kompilierung auf einem unbelasteten Gast dauert über alle Konfigurationen hinweg 8,6–9,8 s, was wir als Freiflug-Baseline verwenden $$T\_1$$.

### 3.4 Lastgenerierung

Wir erstellen 512 echte Benutzerkonten und geben jedem seine eigene Kopie des Projekts, sodass gleichzeitige Kompilierungen exakt so konkurrieren wie unabhängige Nutzer und nicht einen Projektlock teilen. Anfragen werden vom Host an den weitergeleiteten Port des Gasts gesendet, sodass die Lastgenerierung keine CPU des Gasts verbraucht.

Nebenläufigkeit ist *gleichzeitig*, nicht versetzt. Jede Sitzung wird zuerst aufgebaut — Login, CSRF-Token, Compiler-Auswahl — und erst dann schläft jeder Thread bis zu einem gemeinsamen Echtzeitpunkt, der einmal berechnet und geteilt wird, bevor er sein `POST /project/:id/compile` absendet. Der Unterschied ist nicht kleinlich. Eine versetzte Rampe misst Durchsatz unter einer stabilen Warteschlange; ein gleichzeitiger Burst misst, was passiert, wenn ein Hörsaal voller Studierender nach derselben Deadline-Ankündigung denselben Knopf drückt, was die Betreiber tatsächlich fürchten. Die beiden unterscheiden sich um mehr als einen konstanten Faktor, weil der zweite die Compile-Warteschlange schneller füllt, als der Daemon sie abarbeiten kann.

Vor diesem Burst mussten vier praktische Hindernisse entfernt werden, damit er korrekt geliefert werden konnte. Jedes ist erwähnenswert, weil jedes das Experiment stillschweigend in eine Messung des Testgerüsts statt des Servers verwandelt.

#### 3.4.1 Zwei Rate-Limiter, nicht einer

Overleaf drosselt Anmeldungen pro Quelladresse — 20 Versuche pro Minute — und unser gesamter Verkehr stammt von einem einzigen Host. Weist man jedem simulierten Nutzer eine eigene `X-Forwarded-For` -Adresse zu, entfernt das diese Grenze, stößt aber sofort auf eine zweite, gröbere: ein Budget pro Subnetz von ungefähr 200 pro Minute. Das Verteilen der Nutzer auf einen zusammenhängenden Block scheitert daher beim 201. Konto. Stattdessen leiten wir die synthetische Adresse aus dem Benutzerindex ab, sodass aufeinanderfolgende Nutzer in unterschiedlichen `/24`Subnetzen,

$$
\texttt{203.};\big\lfloor i/250 \big\rfloor \bmod 100 + 1\texttt{.};
i \bmod 250 + 1\texttt{.}; i \bmod 200 + 10 ,
$$

&#x20;landen, wodurch beide Rate-Limiter für die gesamte Population von 1024 locker bleiben.

#### 3.4.2 Der injizierte Header wird standardmäßig verworfen

Das Setzen des Headers reicht nicht aus. Express berücksichtigt `X-Forwarded-For`  nur für Peers, denen es als vertrauenswürdig mitgeteilt wurde, und Overleafs `trustedProxyIps`  ist standardmäßig auf `loopback` gesetzt. Weil der Lastgenerator die Anwendung über die Bridge des Containers und nicht über die Loopback-Schnittstelle erreicht, wird der Header geparst und dann verworfen, und jeder simulierte Nutzer fällt wieder auf eine Adresse zusammen. Das Symptom ist eine Welle von `HTTP 429`  genau beim zwanzigsten Login, was leicht als Serverüberlastung missverstanden werden kann. Das Gateway-Netzwerk muss ausdrücklich in die Trust-Kette aufgenommen werden; in der geclusterten Bereitstellung von §4.3 müssen auch die Pod- und Service-CIDRs hinzugefügt werden.

#### 3.4.3 Ein Load Balancer überschreibt den Header, den er bewahren sollte

Wenn die Instanz hinter einem Proxy steht, hängt die herkömmliche `option forwardfor` *an* die echte Client-Adresse an die Kette an, was für die Produktion korrekt und hier genau falsch ist: Die synthetische Adresse wird durch die des Lastgenerators selbst verdrängt. Die Direktive muss als `option forwardfor if-none` qualifiziert werden, sodass der Proxy nur dann einen Wert hinzufügt, wenn der Client keinen geliefert hat.

#### 3.4.4 Dem Client gehen Dateideskriptoren aus, bevor dem Server die Kapazität ausgeht

Bei $$N=1024$$  hält der Generator mehr als tausend gleichzeitige Sockets, und das standardmäßige Soft-Limit von 1024 Deskriptoren wird bereits während des Sitzungsaufbaus und nicht erst während der Messung erreicht. Der Fehler ist still: Drei Sitzungen lassen sich nicht aufbauen und der Lauf meldet 1021 statt 1024, während ein Samplings-Thread, der für Container-Zählungen Shell-Aufrufe verwendet, mit `EMFILE`  stirbt und die Telemetrie stillschweigend abschneidet. Das Soft-Limit muss auf dem Generator erhöht werden — das Hard-Limit auf unserem Host lag bereits bei 1048576 — und der Lauf muss wiederholt werden. Wir berichten beide Läufe in §4.3: Der korrigierte Lauf erreicht 1024 von 1024 mit einem Median, der 1,2 s vom abgeschnittenen Lauf entfernt ist, weshalb wir den ersten als brauchbar, aber nicht maßgeblich behandeln.

### 3.5 Messprotokoll

Für die Reproduzierbarkeit erwiesen sich mehrere methodische Entscheidungen als notwendig.

#### 3.5.1 Aufwärmen

Auf einem frisch gestarteten Gast ist der Page Cache leer und die ersten Kompilierungen messen Cold-Start-I/O statt stationärer Kapazität: Dieselbe 2-vCPU-/2-GiB-Konfiguration ergibt 36,5 s kalt und 9,8 s warm, also einen Faktor von 3,7. Daher führt jede Konfiguration nach dem Start zwei verworfene Einzelkompilierungs-Aufwärmläufe durch.

#### 3.5.2 Erfolgskriterium

Ein Nebenläufigkeitsniveau besteht nur dann, wenn *jede*  Kompilierung erfolgreich ist und das Niveau eine Wiederholung übersteht. Das ist strenger als eine Erfolgsrate-Schwelle, und das ist wichtig: Bei 4 vCPU / 16 GiB bestand ein Niveau von 32 einmal mit einem Median von 80,2 s und lief dann bei allen 32 Kompilierungen beim Wiederholen in ein Timeout, sodass wir 31 berichten.

#### 3.5.3 Suche

Niveaus werden durch exponentielle Eingrenzung aus einem modellvorhergesagten Startwert und anschließende exakte ganzzahlige Bisektion lokalisiert. Da das Kriterium alles-oder-nichts ist, entscheidet der erste Fehler über ein Niveau; daher brechen wir die verbleibenden laufenden Anfragen ab, sobald eine fehlschlägt — außer bei kleinen Niveaus, wo die abgebrochenen Kompilierungen einen kleinen Gast so stark blockieren, dass er sich nie erholt.

#### 3.5.4 Isolation zwischen den Niveaus

Compile-Container werden geleert, und die Webanwendung wird abgefragt, bis sie wieder antwortet, bevor das nächste Niveau beginnt. Ohne dies protokolliert ein Niveau nach einem Absturz einen falschen Zero-Session-Fehler.

#### 3.5.5 Host-Hygiene

Nicht verwandte virtuelle Maschinen auf dem Host wurden heruntergefahren: Mit 24 GiB Host-Speicher, der anderswo gebunden war, meldete dieselbe Gastkonfiguration bei identischer Nebenläufigkeit eine Last Average von 11,7 statt 3,2. Host-Speicherdruck pflanzt sich in den Gast fort und verfälscht die Messung.

## 4. Ergebnisse

### 4.1 Die Kapazitätsmatrix

Tabelle 1 und Abbildung 4 geben die gemessene Obergrenze für jede Konfiguration an. Das Lesen zeilenweise ist die erste Überraschung. Bei 4 GiB erreichen die 2-, 4- und 8-vCPU-Gäste alle exakt 9 — eine Vervierfachung der Kerne ändert überhaupt nichts. Bei 16 GiB erreichen sie 54, 45 und 57: Von 4 auf 16 Kerne zu gehen bringt 6 %, und der 8-Kern-Gast ist tatsächlich *schlechter*  als der mit 4 Kernen (§5.2). Erst bei 48 GiB trennt die Kernanzahl die Konfigurationen eindeutig: 143, 268 und 331.

<figure><img src="/files/4a0a67ce933e05c677a8d8e41eeff8aeb125eba5" alt=""><figcaption></figcaption></figure>

**Abbildung 4.** Gemessene Kapazität über die Konfigurationsmatrix. (a) Jede Konfiguration als Balken, gruppiert nach Speicher und nach Kernanzahl eingefärbt; volle Balken sind speichergebunden (der Gast stirbt wegen erschöpften Speichers) und schraffierte Balken sind CPU-gebunden (Kompilierungen laufen mit Speichervorrat in ein Timeout). Das Lesen einer Gruppe von links nach rechts zeigt, wie wenig Kernzahl unterhalb von 16 GiB bringt; das Lesen über Gruppen hinweg zeigt den überlinearen Ertrag von Speicher. (b) Dieselben Punkte gegen das angepasste Modell $$N\_{\max}=\min(0.69R^{1.60},,26.4C)$$; die gestrichelte Linie ist die Speichergrenze und die gepunkteten Horizontalen sind die CPU-Obergrenzen je Kernzahl. Eine Konfiguration wird von der der beiden Grenzen begrenzt, die sie zuerst erreicht.

Das Lesen einer Spalte von oben nach unten ist die zweite: Bei fester Kernzahl wächst die Kapazität überlinear mit dem Speicher, ungefähr wie $$R^{1.6}$$), aus dem in §5.1 erläuterten Page-Cache-Grund.

| Speicher | 2 vCPU |  4 vCPU |  8 vCPU | 16 vCPU |
| -------: | -----: | ------: | ------: | ------: |
|    2 GiB |      1 |       1 |       1 |       — |
|    3 GiB |      4 |       5 |       6 |       — |
|    4 GiB |      9 |       9 |       9 |       — |
|    8 GiB |     21 |      22 |      26 |       — |
|   16 GiB |      — |      54 |  **45** |      57 |
|   32 GiB |      — | **145** |     135 |     141 |
|   48 GiB |      — | **143** | **268** | **331** |

**Tabelle 1.** Maximale gleichzeitige Kompilierungen, die erfolgreich abgeschlossen werden, gemessen bei einem Compile-Timeout von 300 s mit angehobener CLSI-Nebenläufigkeitsobergrenze. **Fett**  kennzeichnet eine CPU-gebundene Konfiguration (Kompilierungen laufen mit Speichervorrat in ein Timeout); der Rest ist speichergebunden (der Stack stirbt mit HTTP 502). Die 2-GiB-Zeile enthält die in §5.2 besprochene Korrektur.

### 4.2 Nebenläufigkeit ist Zeitscheibung

Abbildung 5 sweepte jedes Nebenläufigkeitsniveau auf einem festen 8-vCPU-/16-GiB-Gast. Zwei Regime werden durch einen scharfen Knick bei genau einer Kompilierung pro Kern getrennt. Darunter ist die mittlere Kompilierungszeit flach — sie bewegt sich von 8,7 s bei $$N=1$$  auf 9,1 s bei $$N=C=8$$ , eine Änderung von 5 %. Oberhalb davon wächst die Zeit strikt proportional zu $$N/C$$: bei $$N=16,24$$  messen wir 18,5 s und 27,1 s, also ein Verhältnis von $$1:2.13:3.12$$  gegenüber einem idealen $$1:2:3$$.

<figure><img src="/files/7746d763e0754912155d5fa8c2d490a750759e3d" alt=""><figcaption></figcaption></figure>

**Abbildung 5.** Kompilierungslatenz gegen Nebenläufigkeit auf fester Hardware. Der Knick liegt bei $$N=C$$; darüber folgt die gemessene Verlangsamung $$N/C$$  mit einer Genauigkeit von 5–7 %. Alle fünfzehn Niveaus waren vollständig erfolgreich.

<figure><img src="/files/ed4a54f866b9c1cb80c4373044545c08f8dc325f" alt=""><figcaption></figcaption></figure>

**Abbildung 6.** Kompilierungslatenz gegen Nebenläufigkeit für mehrere Konfigurationen. Jedes Panel hält die Hardware fest und sweeped die angebotene Last; die vertikale Linie markiert $$N=C$$ . Die Kurven sind links davon flach und rechts davon linear in $$N/C$$  , was das Kennzeichen von Zeitscheibung statt von Konkurrenz ist: Die Arbeit wird nicht teurer, sie wartet nur auf ihre Reihe.

Die Anpassung $$T(N)=T\_1\max(1,N/C)^{b}$$  über alle erfolgreichen Messungen der Studie ergibt $$b=0.914$$ ($$R^2\_{\log}=0.904$$, $$n=81$$ ). Ein von eins nicht zu unterscheidender Exponent ist die quantitative Aussage, dass eine Kompilierung eine single-threaded, CPU-gebundene Arbeitseinheit ist und dass Nebenläufigkeit außerhalb der Aufteilung der Kerne weder hilft noch schadet. Die praktische Konsequenz ist für die Kapazitätsplanung unbequem: Eine Konfiguration kann eine beliebige Anzahl von Nutzern aufnehmen, ohne *zu scheitern*  und dabei jeden einzelnen proportional länger warten zu lassen. Bei $$N=56$$  auf diesem Gast schlagen weiterhin alle Kompilierungen erfolgreich durch, aber jeder Nutzer wartet 64,8 s statt 8,7 s.

### 4.3 Vertikale Skalierung auf 1024 gleichzeitige Kompilierungen

Tabelle 2 und Abbildung 7 berichten den Sweep auf dem großen Runner. Jedes Niveau ist eine *kalte*  Kompilierung: Vor jedem Niveau leeren wir das Compile-Verzeichnis und den CLSI-Cache jedes teilnehmenden Projekts über `DELETE /project/:id/output`, sodass kein Niveau von Arbeit profitiert, die von dem darunterliegenden Niveau erledigt wurde. Die Einzelkompilierungs-Baseline auf dieser Maschine beträgt 28,8 s, was der kalte Wert ist und nicht mit der früher verwendeten stationären Baseline von 8,6–9,8 s verglichen werden sollte; die kalte Baseline auf den QEMU-Gästen beträgt 28,3 s, sodass die beiden Maschinen pro Thread für diese Arbeitslast innerhalb von zwei Prozent voneinander liegen.

| $$N$$ |    Erfolg | $$p\_{50}$$ | $$p\_{95}$$ | $$p\_{50}/T\_1$$ | Spitzen-Container |
| ----: | --------: | ----------: | ----------: | ---------------: | ----------------: |
|    64 |     64/64 |      55,0 s |      55,1 s |             1,9× |                 — |
|   128 |   128/128 |      73,6 s |      74,4 s |             2,6× |                 — |
|   192 |   192/192 |      80,2 s |      87,3 s |             2,8× |                 — |
|   256 |   256/256 |      69,8 s |     121,2 s |             2,4× |               151 |
|   512 |   512/512 |     179,2 s |     344,6 s |             6,2× |               205 |
|  1024 | 1024/1024 |     280,2 s |     468,9 s |             9,7× |               179 |

**Tabelle 2.** Nebenläufigkeits-Sweep auf einem EPYC 7773X (64 Kerne / 128 Threads, 995 GiB). Alle Niveaus kalt; Baseline 28,8 s. Spitzen-Container ist die maximale Anzahl gleichzeitig lebender Sandboxes.

<figure><img src="/files/a3be28822bfe7d3733ac747f62e641dc8692e2dd" alt=""><figcaption></figcaption></figure>

**Abbildung 7.** Vertikale Skalierung auf einem großen Runner. (a) Latenz gegen angebotene Nebenläufigkeit; der schattierte Bereich markiert das Regime jenseits des Knicks. (b) Die Anzahl der tatsächlich lebenden Sandboxes folgt nie der angeforderten Anzahl — sie sättigt bei etwa 200 —, während das Compile-cgroup nie mehr als ein Fünftel seines Limits nutzt.

#### 4.3.1 Die Maschine schlägt nie fehl

Jedes Niveau erreicht 100 %, einschließlich $$N=1024$$  — achtmal der Thread-Anzahl. Wir haben die Kapazitätsgrenze dieser Maschine nicht gefunden; uns ging die Geduld aus, bevor ihr Spielraum erschöpft war. Dies ist die erste Konfiguration in der Studie, bei der die begrenzende Ressource nicht der Speicher ist: Bei $$N=1024$$  erreicht das Compile-cgroup 184 GiB, also ein Fünftel seines 940-GiB-Limits, während die CPU bei 100 % Auslastung mit einem Load Average von 166 läuft.

#### 4.3.2 Die Verlangsamung ist unterlinear, weil die Zulassung rate-begrenzt ist

Naives Time-Sharing sagt voraus, dass $$8\times$$  die Threads kostet $$8\times$$  die Latenz. Die gemessenen Kosten betragen $$9.7\times$$  relativ zu einer einzelnen Kompilierung, aber nur $$3.8\times$$  relativ zu $$N=128$$  — für eine achtfache Erhöhung der angebotenen Last. Der Grund ist in Abbildung 7(b) und in der letzten Spalte von Tabelle 2 sichtbar: Obwohl 1024 Anfragen gleichzeitig gestellt werden, übersteigt die Zahl der tatsächlich lebenden Sandboxes nie 205. Der Daemon kann Container nicht so schnell erstellen, wie die Clients sie anfordern, also werden Anfragen bei der Zulassung in die Warteschlange gestellt und nicht innerhalb der CPU konkurrierend bearbeitet. Das Warten in der Warteschlange ist hier das, was den Tail-Wert rettet, und zwar zufällig.

#### 4.3.3 Der Knick liegt bei 512, nicht am Fehlpunkt

Zwischen $$N=256$$  und $$N=512$$  der $$p\_{95}$$  Latenz steigt um $$2.9\times$$  bei einer Verdopplung der Last; jede frühere Verdopplung kostete zwischen $$1.2\times$$  und $$1.4\times$$. Kapazität, angegeben als „die größte $$N$$  die nicht fehlschlägt“, würde 1024 melden und wäre für einen Betreiber nutzlos: Zu diesem Zeitpunkt beträgt die Tail-Wartezeit fast acht Minuten.

### 4.4 Die Compile-Zeit ist umgekehrt proportional zum Takt

Da die Arbeitslast CPU-gebunden ist, sollte ihre Kostenkurve wie $$1/f$$ skalieren. Wir testen dies direkt, indem wir den Host-Takt über den gesamten Bereich der Maschine, 1,0–5,5 GHz in zehn Schritten, auf einem ansonsten unveränderten Gast sweepen (Abbildung 8). Die Zeit einer Einzelkompilierung verändert sich von 26,5 s auf 4,8 s: Ein 5,5×-Takt bringt überall im Bereich eine 5,5×-Beschleunigung ohne abnehmenden Ertrag. Das Produkt $$T!\cdot!f$$  ist über alle zehn Taktraten hinweg bis auf 2 % konstant.

Durch Normierung auf den Kernanteil fallen alle dreißig Messungen — drei Nebenläufigkeitsniveaus bei zehn Taktraten — auf eine einzige Konstante zusammen:

$$
T(N,f) ;=; \frac{k}{f},\max!\left(1,\frac{N}{C}\right),
\qquad k = 27.9\ \mathrm{GHz\cdot s}
$$

&#x20;mit einer Reststreuung von 5,1 % über einen Bereich, in dem der Takt selbst um das 5,5-Fache variiert. Das Fehlen jeglicher Krümmung ist selbst das Ergebnis: Wäre die Arbeitslast durch Speicherbandbreite oder I/O begrenzt, $$T$$  würde sie bei hohem Takt abflachen, sobald die CPU die andere Ressource überholt.

<figure><img src="/files/b4e2da89ff4d60d789d3d9416d01782db40c2f7a" alt=""><figcaption></figcaption></figure>

**Abbildung 8.** Takt-Sweep. (a) $$T=k/f$$  mit der angepassten Hyperbel. (b) Nach Division durch $$\max(1,N/C)$$  fallen alle Punkte auf eine einzige Konstante zusammen, was Gleichung (1) bestätigt.

Gleichung (1) hat eine direkte Beschaffungsfolge, die leicht zu formulieren und leicht falsch zu machen ist: *Der Takt verbessert die Erfahrung jedes einzelnen Nutzers, die Kernzahl lässt nur mehr von ihnen zu*. Eine Maschine mit 20 % höherem Takt kompiliert für alle 20 % schneller, ohne abnehmenden Ertrag; doppelt so viele Kerne machen niemandes Kompilierung überhaupt schneller.

## 5. Analyse

### 5.1 Zwei Grenzen, getrennt angepasst

Jede Konfiguration wird anhand ihrer Fehlersignatur klassifiziert (§2.3), und die Speichergrenze sowie die CPU-Obergrenze werden dann nur auf die Konfigurationen angepasst, die sie tatsächlich erreichen:

$$
N\_{\max} = \min\left(A R^{p},; k\_c C\right)
$$

mit $$R$$ in Gibibyte und $$C$$ in vCPUs.

Der Exponent der Speicherwand ist durchgängig überlinear, $$p>1$$: die marginalen Speicherkosten einer zusätzlichen gleichzeitigen Kompilierung *fallen* mit wachsendem Gesamtspeicher von ungefähr 312 MiB pro Kompilierung auf einem 3-GiB-Gast auf etwa 194 MiB auf einem mit 32 GiB. Der Mechanismus ist der gemeinsame Page Cache über den in §2.1 beschriebenen TeX-Live-Baum: Gleichzeitige Kompilierungen lesen überlappende Font- und Makrodateien, sodass ein größerer Cache auf mehr von ihnen umgelegt wird. Deshalb unterschätzt die naive Regel „ein Gigabyte pro fünf Benutzer“ große Maschinen und überschätzt kleine.

<figure><img src="/files/d84fe306bdf511e770ba022dc59e2d184e755542" alt=""><figcaption></figcaption></figure>

**Abbildung 9.** Dieselben Daten als zwei Flächen über der $$(C,R)$$ Ebene. (a) Kapazität: Die angepasste Fläche ist ein Grat, keine Ebene — sie steigt steil mit dem Speicher an und ist entlang der Kernachse nahezu flach, bis der Speicher nicht mehr bindet; deshalb ist die 48-GiB-Zeile die einzige, in der die Kernzahl die Konfigurationen voneinander trennt. (b) Latenz gegenüber Parallelität für jede Konfiguration, wobei die angepasste $$T=12.6,(N/C)^{0.91}$$ gestrichelt dargestellt ist und das 180-s-Timeout als Ebene eingezeichnet ist. Eine Konfiguration scheitert dort, wo ihre durchgezogene Kurve diese Ebene durchstößt, wodurch sichtbar wird, wie direkt die Timeout-Einstellung die gemeldete Kapazität bestimmt.

### 5.2 Wo mehr Kerne die Sache verschlimmern

Gleichung (2) ist das Minimum aus zwei Termen und daher monoton in $$C$$, aber die Messungen sind es nicht. Wir beobachten zwei Umkehrungen, bei denen das Hinzufügen von Kernen *verringerte* Kapazität: bei 16 GiB (54 gegenüber 45) und bei 32 GiB (145 gegenüber 135). Beide treten im speichergebundenen Regime auf, und der Mechanismus ist in beiden Fällen derselbe: Mit mehr Kernen schreiten gleichzeitige Kompilierungen im Gleichschritt fort und erreichen ihren maximalen Resident-Set-Umfang zum selben Zeitpunkt, während der Scheduler sie bei weniger Kernen ineinander verzahnt und die Spitzen zeitlich versetzt sind. Auf einem Gast, dessen Speicherpuffer bereits knapp ist, ist genau dieses Versetzen das, was ihn am Leben hält. Ein Kapazitätsmodell, das auf dem durchschnittlichen Ressourcenverbrauch basiert, kann das nicht ausdrücken; es ist eine Eigenschaft der *Koinzidenz* von Spitzen.

Eine dritte scheinbare Umkehrung bei 2 GiB verwerfen wir nun. Die Suchprotokolle verzeichnen eine Kapazität von 2 bei 2 vCPU, aber 1 bei 4 und 8 vCPU, was wie derselbe Effekt wirkt. Eine erneute Auswertung der Rohdurchläufe zeigt etwas Einfacheres: Bei 2 GiB $$N=2$$ war die Ebene beim ersten Versuch bei allen drei Kernzahlen erfolgreich und scheiterte dann bei zwei von drei Bestätigungsdurchläufen. Die Ebene ist keine Kapazität, sondern ein Münzwurf, und der 2-vCPU-Eintrag ist der Wurf, der zufällig gelandet ist. Wir berichten daher den reproduzierbaren Wert 1 bei allen drei Kernzahlen und ziehen aus dem Unterschied keinen Schluss. Wir halten die Korrektur hier fest, statt die Tabelle stillschweigend neu zu formulieren, weil die verworfene Lesart genau diejenige wäre, die eine interessante Behauptung gestützt hätte.

## 6. Implementierungsergebnisse

### 6.1 Eine hart codierte Obergrenze für Parallelität

Auf ausreichend großen Gästen endete die Kapazität bei genau 65 gleichzeitigen Kompilierungen, unabhängig von der angeforderten Parallelität: bei $$N=66,80,96,128$$ haben wir $$65$$ Erfolge und $$1,15,31,63$$ sofortige `nicht verfügbare` Antworten gemessen, wobei die Containerzahl bei 65 festgenagelt war, mehrere Gigabyte Speicher ungenutzt blieben und die mediane Kompilierzeit bei 77 s stabil blieb — weit unter jedem Timeout.

Die Ursache ist eine Konstante in CLSI:

```javascript
// services/clsi/config/settings.defaults.cjs:110
compileConcurrencyLimit: isSpotInstance ? 32 : 64,

// services/clsi/app/js/LockManager.js
if (LOCKS.size <= Settings.compileConcurrencyLimit) return   // <= admits 64+1
throw new Errors.TooManyCompileRequestsError(...)
```

Der Vergleich ist nicht strikt, also ist die wirksame Obergrenze $$64+1=65$$, was der Messung exakt entspricht. Die überschüssigen Anfragen erhalten **HTTP 503** — sie werden *abgewiesen*, nicht in eine Warteschlange gestellt, sodass aus Sicht des Benutzers der Compile-Button einfach fehlschlägt. Anders als jeder andere Parameter in derselben Datei liest dieser keine Umgebungsvariable; er wurde upstream im August 2024 eingeführt und kann nur durch Ändern des Images angepasst werden. Mit erhöhter Grenze meldete derselbe 16-vCPU-/32-GiB-Gast, der `Erfolg=65, nicht verfügbar=15` bei $$N=80$$ stattdessen meldete `Erfolg=80`.

### 6.2 Eine funktionslose Container-Speichergrenze

Die Inspektion eines laufenden Compile-Containers zeigt keinerlei Ressourcenisolation:

```
Memory=0  NanoCpus=0  CpuShares=0  CpuQuota=0  CpusetCpus=[]
Ulimits=[{Name:cpu Soft:305 Hard:310}]
```

Das Fehlen jeglicher CPU-Quote ist beabsichtigt und erklärt, warum der Zeitanteils-Exponent von §4.2 so sauber ist: Nichts verzerrt den Wettbewerb zwischen den Kompilierungen. Das Fehlen einer *Speicher* Speichergrenze ist jedoch nicht beabsichtigt. Der Docker-Runner fordert eine an:

```javascript
// services/clsi/app/js/DockerRunner.mjs:262
Memory: 1024 * 1024 * 1024 * 1024, // 1 GB
```

Das ist doppelt falsch. Der Wert ist $$1024^4=1 tebiB$$ wo der Kommentar $$1024^3$$; und das Feld steht auf der obersten Ebene der Create-Optionen statt innerhalb von `HostConfig`, wo die Docker-API ihn erwartet, also wird er verworfen — was das beobachtete `Memory=0` bestätigt. Beide Fehler sind in dem Commit enthalten, der die Datei einführte (`9a519f0d3d`, März 2018) und überstanden die Konvertierung von CoffeeScript, eine repositoryweite Neufomatierung und eine CJS-zu-ESM-Migration, von denen keine die Semantik erneut prüfte. Bemerkenswert `MAX_OUTPUT = 1024 * 1024 // 1MB` ist im selben Commit korrekt, was auf einen Versprecher statt auf ein Missverständnis hindeutet.

Die Folge ist in unseren Messungen mit wenig Speicher sichtbar. Weil Kompilierungen nicht begrenzt sind, äußert sich Speicherausschöpfung nicht so, dass Docker einen einzelnen problematischen Container beendet; sie legt den gesamten Gast lahm. In der 2-vCPU-/2-GiB-Konfiguration beobachteten wir, dass die Monitoring-SSH-Sitzung 300 s lang blockiert war, eine Lastdurchschnitt von 68 auf zwei Kernen und dass der Gast sich schließlich selbst neu startete. Eine funktionierende Limitierung pro Container würde viel sanfter degradieren: Die zu große Kompilierung würde fehlschlagen und der Dienst würde überleben.

Die einzige Grenze, die tatsächlich greift, ist `RLIMIT_CPU`, gesetzt auf $$\text{timeout}+5$$ Sekunden. Sie begrenzt *CPU* Zeit, nicht die Wandzeit, und eine einzelne Kompilierung verbraucht nur etwa 9 s CPU-Zeit, sodass sie bei keiner Parallelität jemals bindet; sie schützt gegen pathologische Eingaben wie ein außer Kontrolle geratendes Makro. Sie ist jedoch ein nützliches Orakel: Die Beobachtung von `Soft:305` bestätigt, dass eine 300-s-Timeout-Einstellung tatsächlich in den Container propagiert wurde.

### 6.3 Das Compile-Timeout ist der dominierende Parameter

Das benutzerspezifische Feld `features.compileTimeout` ist standardmäßig auf 180 s gesetzt. Für jede CPU-gebundene Konfiguration ist das keine Sicherheitsmarge, sondern eine Kapazitätseinstellung, denn ein Rechner, der noch korrekt rechnet, wird als fehlgeschlagen erklärt. Das Anheben auf 300 s — eine einzige MongoDB-Aktualisierung — verändert die gemessene Kapazität um bis zu den Faktor 4,2 (Tabelle 3). Die Obergrenze liegt bei 600 s und wird durch `RequestParser.MAX_TIMEOUT`erzwungen, oberhalb dessen der Wert stillschweigend abgeschnitten wird.

| Konfiguration    | 180 s | 300 s |     Verhältnis | Bindende Einschränkung                   |
| ---------------- | ----: | ----: | -------------: | ---------------------------------------- |
| 8 vCPU / 48 GiB  |    64 |   268 | $$4.19\times$$ | CPU, 28 GiB frei                         |
| 4 vCPU / 32 GiB  |    47 |   145 | $$3.09\times$$ | CPU, 30 GiB frei                         |
| 4 vCPU / 48 GiB  |    63 |   143 | $$2.27\times$$ | CPU                                      |
| 4 vCPU / 16 GiB  |    31 |    54 | $$1.74\times$$ | CPU                                      |
| 2 vCPU / 8 GiB   |    15 |    21 | $$1.40\times$$ | CPU                                      |
| 8 vCPU / 32 GiB  |   127 |   135 | $$1.06\times$$ | trifft als Nächstes auf die Speicherwand |
| 8 vCPU / 16 GiB  |    56 |    45 | $$0.80\times$$ | Speicher                                 |
| 16 vCPU / 32 GiB |   159 |   141 | $$0.89\times$$ | Speicher                                 |

**Tabelle 3.** Einfluss des Compile-Timeouts auf die gemessene Kapazität.

Die letzten beiden Zeilen sind der kontraintuitive Teil des Ergebnisses und der Grund, warum wir jede Konfiguration unter einem Timeout neu gemessen haben. Für *Speicher*-gebundene Konfigurationen erhöht ein längeres Timeout *die Kapazität* Kapazität, weil jede Kompilierung ihr Resident Set länger hält und sich mehr von ihnen überlappen. Eine Kapazitätsangabe ist daher sinnlos, wenn nicht das Timeout genannt wird, unter dem sie gemessen wurde; und beide lassen sich nicht in derselben Tabelle mischen.

## 7. Verwandte Arbeiten

### 7.1 Herstellerempfehlungen

Overleafs eigene Hardware-Dokumentation nennt die qualitativen Fakten, die wir hier quantifizieren: dass LaTeX ein Ein-Thread-System ist, dass daher die Single-Core-Leistung die Kompilierzeit bestimmt und dass „mehr Kerne nur dann helfen, wenn Sie mehr Dokumente kompilieren wollen, als Sie freie CPU-Kerne haben“ \[1]. Anschließend gibt sie die lineare Dimensionierungsregel an — eine Basis von 2 Kernen/3 GiB plus ein Kern und ein Gigabyte pro fünf bis zehn gleichzeitige Benutzer —, die diese Studie motivierte. Unser Beitrag besteht darin, diese Aussagen in gemessene Gesetze zu übersetzen (Gleichungen (1) und (2)) und zu zeigen, wo die lineare Regel bricht: Sie hat keinen Term für den gemeinsamen Page Cache, der die Speicherwand überlinear macht, und keinen Term für die beiden Softwareparameter, die das Ergebnis dominieren.

### 7.2 Studien zur Build- und CI-Kapazität

Die Messung von Builds unter Parallelität ist außerhalb des LaTeX-Umfelds gut etabliert. LightSys berichtet, dass herkömmliche CI-Systeme, die in Docker-Containern kompilieren, bei steigendem Eintrefftempo von Pull Requests bei der I/O-Leistung abbauen, wobei sich um elf gleichzeitige Anfragen herum ein Engpass zeigt \[17]; TAOS-CI beobachtet, dass das Kompilieren die Wandzeit von CI dominiert und auf großen Projekten 60–67 % der gesamten Pipeline-Dauer ausmacht \[18]. Unser System unterscheidet sich in einem Punkt, der sich als entscheidend erweist: Ein LaTeX-Compile ist interaktiv. Ein CI-Job, der doppelt so lange dauert, ist eine Unannehmlichkeit; eine Kompilierung, die doppelt so lange dauert, wird direkt von einem Benutzer wahrgenommen, der auf ein Vorschaufenster wartet, weshalb wir das Timeout nicht als Fehlerschwelle, sondern als Kapazitätsparameter behandeln.

### 7.3 Container-Overheads

Neuere Arbeiten zerlegen die Startlatenz von Docker-Containern nach Speicherstufen \[19] und charakterisieren die Container-Performance am Rand \[20]. In unserem Setting wird der Containerstart pro Kompilierung amortisiert: Er ist eine kleine Konstante im Vergleich zu einer 9-s-Kompilierung, und die Free-Flight-Zeit $$T\_1$$ die wir anpassen, nimmt ihn auf. Die Container-Eigenschaft, die tatsächlich zählt, ist die *Abwesenheit* von Ressourcenlimits (§6.2), die einen Speicherüberschuss pro Kompilierung in einen Ausfall des gesamten Hosts verwandelt.

### 7.4 LaTeX als nicht vertrauenswürdige Eingabe

Sandboxed-Compilation existiert, weil TeX eine Programmiersprache ist und Dokumente nicht vertrauenswürdige Eingaben sind \[21, 22]. Diese Designentscheidung macht diese Studie überhaupt erst möglich — jede Kompilierung ist ein isolierter Container mit beobachtbarem Ressourcenverhalten — und macht auch das fehlende Speicherlimit folgenschwer, da die Isolation von den Betreibern, die sie einsetzen, als gegeben angenommen wird.

### 7.5 Der Compiler als Untersuchungsgegenstand

TeX selbst ist als Sprache gut dokumentiert \[16], aber sein Verhalten als *Build-Ziel* hat erst kürzlich Aufmerksamkeit erhalten. Tan und Rigger \[8] kompilieren einen großen Korpus von arXiv-Quellen über verschiedene Engines und Distributionsversionen hinweg und stellen fest, dass die Wahl der Engine nicht austauschbar ist: Nur ein Bruchteil eines Prozents der Dokumente erzeugt unter XeTeX und pdfTeX byteidentische Ausgaben. Dieses Ergebnis ist für unsere Methodik direkt relevant. Kapazität ist eine Eigenschaft eines Dokuments *und* einer Engine, sodass ein Benchmark, der nicht beide festlegt, nicht reproduzierbar ist; wir fixieren daher durchgehend ein Dokument, eine Engine und eine Distribution (`texlive-full:2025.1`) und geben die Engine in jeder Bildunterschrift an. Es begrenzt auch die Allgemeingültigkeit unserer Zahlen in einer Weise, die man klar benennen sollte: Sie charakterisieren XeLaTeX auf diesem Dokument, nicht TeX im Abstrakten.

Arbeiten an LaTeX-Build- *Systemen* sind weitgehend praxisgetrieben. Das Projekt LaTeX3s `l3build` \[13] standardisiert Regressionstests und Paketierung, und unabhängige Benchmarks vergleichen Wrapper-Tools — eine Untersuchung von 26 Build-Systemen findet, dass ein vorkompiliertes Preamble gegenüber einem einfachen Durchlauf rund 20 % und 40 % gegenüber `latexmk` \[14] wert ist. Diese optimieren die *einzelne* Kompilierung. Sie sind orthogonal zu dem, was wir messen, und lassen sich damit kombinieren: Ein Preamble-Cache verkürzt $$T\_1$$, und jede Kapazitätsangabe in diesem Papier skaliert mit $$T\_1$$.

### 7.6 Parallelitätskontrolle im Editor, nicht im Compiler

Die kollaborative Hälfte von Overleaf beruht auf einer gut etablierten Arbeitstradition. Operationale Transformation stammt von Ellis und Gibbs \[9] und wurde durch das Jupiter-System \[10] für Latenz-arme Clients praktikabel gemacht, dessen Design in `document-updater`erkennbar ist: ein Server, der Operationen ordnet, und ein Puffer pro Dokument, gegen den die Clients synchronisieren. Konfliktfreie replizierte Datentypen \[11] lösen dasselbe Problem ohne zentralen Sequencer. Dieser Unterschied ist es, der die Topologie von §4.3 überhaupt funktionieren lässt: Da der ausstehende Aktualisierungspuffer in gemeinsam genutztem Redis statt im Speicher einer Instanz liegt, sieht eine an irgendeine Replik geroutete Kompilierung die neuesten Tasteneingaben, und die Kompilierungsaffinität kann aus Cache-Lokalität statt aus Korrektheit gewählt werden.

### 7.7 Kapazitätsmodelle

Das Gesetz von Amdahl \[24] begrenzt den Geschwindigkeitsgewinn durch Parallelität und das Gesetz von Little \[23] setzt Belegung mit Ankunftsrate und Servicezeit in Beziehung; beide werden oben verwendet. Gunthers universelles Skaliergesetz \[12] erweitert das erste um einen Rückwärts-Term für Kohärenzverzögerung und sagt voraus, dass der Durchsatz zuerst seinen Höhepunkt erreicht und dann sinkt. Wir stellen fest, dass unser System dieses *nicht* Regime zeigt bis $$N=1024$$: Der Durchsatz sättigt sich und die Latenz steigt, aber nichts bricht zusammen. Der Grund ist struktureller und nicht zufällig — Kompilierungen teilen keinen Zustand, der kohärent gehalten werden müsste, sodass der durch das Gesetz hinzugefügte Term nahe null liegt, und das Aufnahmepflateau von §4.3 begrenzt die Konkurrenz, bevor sie relevant werden kann.

## 8. Empfehlungen für Betreiber

{% stepper %}
{% step %}

## Beheben Sie die beiden Softwareparameter, bevor Sie Hardware kaufen

Beide sind kostenlos und beide sind wertvoller als jedes einzelne Hardware-Upgrade, das wir gemessen haben. Erhöhen Sie `features.compileTimeout` auf einen Wert, den Ihre Benutzer tatsächlich tolerieren — das Maximum, das CLSI akzeptiert, sind 600 s — und, wenn Sie erwarten, mehr als 65 gleichzeitige Kompilierungen zu überschreiten, heben Sie entweder `compileConcurrencyLimit` in einem abgeleiteten Image an oder skalieren Sie horizontal. Keines von beidem zu tun bedeutet, für Kerne zu zahlen, die die Software sich weigert zu nutzen.
{% endstep %}

{% step %}

## Größen Sie eine Maschine nach ihrem Knie, nicht nach ihrer Obergrenze

Der Sweep des Großservers (§4.3) trennt zwei Zahlen, die routinemäßig gleichgesetzt werden. Die *Obergrenze* — die größte Parallelität, die noch jedes PDF zurückliefert — liegt auf einem 64-Kern-Server bei mindestens 1024, und wir haben sie nie erreicht. Die *Knie* — der Punkt, ab dem die Tail-Latenz nicht mehr sanft wächst, sondern sich zu verdoppeln beginnt — liegt bei 512, und der letzte bequeme Betriebspunkt darunter ist 256. Zwischen $$N=256$$  und $$N=512$$  der $$p\_{95}$$ steigt die Wartezeit von zwei Minuten auf fast sechs; zwischen 512 und 1024 erreicht sie acht. Ein Betreiber, der auf die Obergrenze dimensioniert, liefert ein System aus, das technisch funktioniert und das niemand benutzen möchte.

Für diese Maschine und dieses Dokument ist der empfohlene Betriebspunkt daher **256 gleichzeitige Kompilierungen**, was $$4\times$$ der physischen Kernzahl und $$2\times$$ der Thread-Zahl entspricht und $$p\_{95}$$ bei `compileConcurrencyLimit` etwa 120 s liegt. Wir empfehlen,
{% endstep %}

{% step %}

## auf diesen Wert zu setzen, statt ihn hoch zu lassen: 1024 Kompilierungen auf einmal zuzulassen, lässt alle acht Minuten warten, während 256 zuzulassen und den Rest in eine Warteschlange zu stellen den meisten Benutzern in zwei Minuten dient. Eine Warteschlange benachteiligt die später Ankommenden; Konkurrenz belastet alle.

Jede Zeile in Tabelle 2 ist eine kalte Kompilierung, die gleichzeitig ausgelöst wird. Keine der beiden Bedingungen trifft in der Produktion zu: Eine warme Kompilierung desselben Dokuments dauert 8,6 s gegenüber 28,3 s kalt, eine Verhältnismäßigkeit von $$3.3$$und echte Benutzer drücken den Knopf nicht in derselben Sekunde. Eine Population im stationären Zustand, die alle zwei Minuten neu kompiliert und eine typische Cache-Hit-Rate hat, kann daher erheblich mehr Schreibende tragen, als die reine Parallelitätszahl vermuten lässt — in der Größenordnung von tausend oder mehr aktiven Autoren bei dem Betriebspunkt 256. Die Parallelitätszahl ist eine Grenze für den momentanen Burst, keine Sitzplatzanzahl.
{% endstep %}

{% step %}

## Entscheiden Sie zuerst das Latenzbudget, dann lesen Sie die Größe ab

Gleichung (1) lässt sich direkt invertieren. Für eine Zielwartezeit $$T$$ bei Takt $$f$$ auf $$C$$ Kernen ist die passende Parallelität $$N \le C,fT/k$$ mit $$k\approx28 GHz·s$$ für dieses Dokument. $$N\le51$$Ein 60-s-Budget auf 8 Kernen bei 3 GHz ergibt
{% endstep %}

{% step %}

## Kaufen Sie zuerst Speicher, dann Kerne, und prüfen Sie, auf welcher Wand Sie stehen

Unter 32 GiB sahen wir kaum Nutzen durch zusätzliche Kerne. Die Diagnose ist einfach: Treten Fehler als HTTP 502 auf und fehlt dem Gast Speicher, fügen Sie Speicher hinzu; treten sie als `timedout` bei reichlich Speicher auf, fügen Sie Kerne hinzu oder erhöhen Sie das Timeout. Betreiber können dies an derselben Fehlersignatur ablesen, die wir zur Klassifikation von Konfigurationen verwendet haben.
{% endstep %}

{% step %}

## Bevorzugen Sie Takt für die Erfahrung, Kerne für die Population

Weil $$T\propto 1/f$$ ohne Krümmung gilt (2 % über 1,0–5,5 GHz), macht ein schnellerer Takt jede Kompilierung für jeden Benutzer schneller. Mehr Kerne machen keine einzelne Kompilierung schneller; sie lassen nur mehr gleichzeitige zu. Bereitstellungen, deren Beschwerde lautet „Kompilierungen sind langsam“, sollten Takt kaufen; Bereitstellungen, deren Beschwerde lautet „Kompilierungen scheitern zum Termin“, sollten Speicher und Kerne kaufen.
{% endstep %}

{% step %}

## Horizontal statt vertikal über die Obergrenze hinaus skalieren

Jenseits von 65 gleichzeitigen Kompilierungen ist der unterstützte Weg horizontale Skalierung (Abbildungen 2c und 10, in §9 erläutert): mehrere Anwendungsinstanzen hinter einem Load Balancer mit Cookie-Sitzungsaffinität, die zentrale MongoDB, Redis und S3-kompatiblen Speicher gemeinsam nutzen, wobei `git-bridge` als Singleton verbleibt. Das vervielfacht die Obergrenze pro Instanz mit der Anzahl der Instanzen, und genau so erreicht die SaaS-Bereitstellung ihre eigene Kapazität.
{% endstep %}

{% step %}

## Nicht auf Isolation pro Kompilierung verlassen

Bis die Speichergrenze des Docker-Runners korrigiert ist (§6.2), kann ein einziges pathologisches Dokument den Host erschöpfen, statt allein beendet zu werden. Betreiber, die diese Garantie brauchen, sollten sie selbst erzwingen statt darauf zu warten. Der Mechanismus, den wir auf dem großen Host verwendet haben, ist eine systemd-Slice mit harter Obergrenze, auf die dann der Docker-Daemon ausgerichtet wird, sodass jeder von ihm erstellte Container darin erfasst wird:

```ini
[Slice]
MemoryMax=940G
```

Ein Detail daran kostet einen Nachmittag, wenn man es übersieht. Eine Slice mit dem Namen `docker-capped.slice` steht nicht neben `docker.slice`; sie steht *innerhalb* davon, weil der Bindestrich der Hierarchie-Trenner und nicht Teil des Namens ist. Eine Obergrenze, die keine Wirkung zu haben scheint, wurde gewöhnlich eine Ebene entfernt von dem Ort gesetzt, an dem die Container tatsächlich leben. Prüfen Sie dies, indem Sie den Spitzenwert aus `memory.max_usage_in_bytes` nach einem Lauf auslesen, statt der Konfigurationsdatei zu vertrauen — auf unserem Host überschritt die Compile-cgroup nie ein Fünftel ihrer Obergrenze, selbst bei 1024 gleichzeitigen Kompilierungen, was wiederum der Beweis dafür ist, dass der bindende Engpass der Daemon und nicht der Speicher war.
{% endstep %}
{% endstepper %}

<figure><img src="/files/f869333bf415c3188b0dcbbd22303d9658e48a9e" alt=""><figcaption></figcaption></figure>

**Abbildung 10.** Referenz-Topologie für eine horizontal skalierte Bereitstellung, gezeichnet nach der von uns verifizierten Konfiguration. Anwendungsreplikate sind austauschbar und enthalten nichts Dauerhaftes, daher können sie frei hinzugefügt und entfernt werden. Drei Komponenten sind es nicht: Redis, dessen Dokumentpuffer es einer an irgendeine Replik gerouteten Kompilierung ermöglicht, die neuesten Tasteneingaben zu sehen; der Objektspeicher, der ab mehr als einer Replik nicht mehr optional, sondern verpflichtend ist; und `git-bridge`, die Repositories auf lokaler Festplatte ohne Replikationspfad hält und als Singleton neben einer bestimmten Replik laufen muss.

## 9. Eine referenzielle Mehrmaschinen-Bereitstellung

Alles oben misst eine einzelne Maschine. Dieser Abschnitt beschreibt die verteilte Form so detailliert, dass man sie bauen kann, und — weil die Frage, vor der ein Betreiber tatsächlich steht, nicht *wie* sondern *ob* ist — nennt zuerst den Punkt, an dem sich der Aufwand lohnt.

### 9.1 Wann die verteilte Form gerechtfertigt ist

Eine einzelne Maschine ist in jeder relevanten Hinsicht billiger zu betreiben: eine Fehlerdomäne, kein gemeinsam genutzter Zustand, der konsistent gehalten werden muss, kein Routing, das falsch laufen kann. Unsere Daten setzen drei Schwellen dafür, wann man davon abweichen sollte.

#### 9.1.1 Unter 65 gleichzeitigen Kompilierungen nicht

Die Obergrenze pro Instanz ist eine Softwarekonstante, keine Hardwarekonstante (§6.1). Solange die angebotene Last sich ihr nicht nähert, bringt eine zweite Maschine nur zusätzliche Fehlermodi und keinen Nutzen. Der 64-Kern-Host bediente 256 gleichzeitige Kompilierungen nur mit voller Erfolgsquote, nachdem `compileConcurrencyLimit` angehoben worden war; ein Betreiber, der diesen einen Wert noch nicht geändert hat, ist nicht hardwaregebunden und sollte keine Hardware kaufen.

#### 9.1.2 Zwischen 65 und ungefähr 500 zuerst vertikal skalieren

Vertikale Skalierung blieb über unseren gesamten Bereich linear und trat nie in ein Rückwärtsregime ein. Ein großer Host erreichte 1024 gleichzeitige kalte Kompilierungen mit 100 % Erfolgsquote (§4.3); das Knie in der Latenz erschien bei 512, nicht früher. Innerhalb dieses Bands ist eine größere Maschine eindeutig einfacher als mehrere kleinere und verbessert nach §4.4 die Erfahrung jedes Benutzers, statt nur mehr von ihnen zuzulassen.

#### 9.1.3 Für Verfügbarkeit verteilt gehen, nicht für Durchsatz

Der ehrliche Grund, unterhalb der Obergrenze mehr als ein Anwendungsreplikat zu betreiben, ist, dass eine Maschine ein Netzteil, ein Kernel, ein Upgradefenster ist. Das ist ein legitimer Grund, und genau den würden wir nennen; es ist einfach kein Kapazitätsargument, und wer beides vermischt, bringt Betreiber dazu, Replikate zu kaufen, obwohl sie Speicher brauchten.

### 9.2 Ebenen und ihre Dimensionierung

Abbildung 10 zeigt die Topologie. Sie hat vier Ebenen, und sie skalieren nach unterschiedlichen Größen — genau deshalb trennt man sie.

#### 9.2.1 Edge

Ein Load Balancer, oder zwei für Verfügbarkeit. Er terminiert TLS und tut nichts Teures; er skaliert mit der Verbindungsanzahl, nicht mit der Kompilierungsanzahl, und eine kleine Instanz reicht für die hier untersuchten Lasten aus. Seine Konfiguration, nicht seine Größe, ist entscheidend (§9.3).

#### 9.2.2 Anwendungsreplikate

Diese tragen die Compile-Last und sind die einzige Ebene, die mit Parallelität skaliert. Dimensionieren Sie jede nach den Regeln von §8 — zuerst Speicher, dann Kerne, dann Takt — und setzen Sie dann die Replikanzahl so, dass die Spitzenparallelität durch die Obergrenze pro Replik gedeckt ist. Replikate halten nichts Dauerhaftes: Ihre lokale Festplatte trägt Kompilier-Temp und einen Ausgabe-Cache, beides rekonstruierbar. Das macht sie sicher frei hinzuzufügen und zu entfernen, und es lohnt sich, dies zu verifizieren statt anzunehmen, weil ein einziges fehlkonfiguriertes `filestore` Pfad die Ebene stillschweigend in eine zustandsbehaftete verwandelt.

#### 9.2.3 Zustand

Redis, MongoDB und ein S3-kompatibler Objektspeicher, auf getrennten Hosts. Redis ist der tragende und am wenigsten offensichtliche Teil: Er hält den Session-Speicher und den Live-Dokumentpuffer, was es einer an irgendeine Replik gerouteten Kompilierung ermöglicht, Tastatureingaben zu sehen, die gegen eine andere Replik gemacht wurden. Ein Betreiber, der Redis als Cache behandelt und für Eviction dimensioniert, produziert Kompilierungen veralteter Dokumente, die extrem schwer zu diagnostizieren sind, weil nichts fehlschlägt — die Ausgabe ist lediglich falsch. MongoDB skaliert mit der Projektanzahl statt mit der Kompilierungsrate. Der Objektspeicher ist bei einer Replik optional und darüber hinaus obligatorisch.

#### 9.2.4 Der Singleton

`git-bridge` hält Repositories auf lokaler Festplatte, pflegt einen lokalen Index und hat keinen Replikationspfad. Er muss als genau eine Instanz laufen, fest neben einer bestimmten Replik verankert, und ist die Komponente, die die Bereitstellung nicht ganz zustandslos macht. Planen Sie seinen Host entsprechend: Seine Festplatte ist diejenige, die gesichert werden muss.

| **Ebene**      | **Anzahl**           | **Skaliert mit**                          |
| -------------- | -------------------- | ----------------------------------------- |
| Load Balancer  | 1–2                  | gleichzeitigen Verbindungen               |
| Anwendung      | $$\lceil N/C\rceil$$ | Spitzenzahl gleichzeitiger Kompilierungen |
| Redis          | 1 (+Replikat)        | aktive Bearbeitungssitzungen              |
| MongoDB        | 1 (+Replikat)        | gespeicherte Projekte                     |
| Objektspeicher | 1 Cluster            | gesamte Projektbytes                      |
| `git-bridge`   | genau 1              | Repositories auf der Festplatte           |

**Tabelle 4.** Referenzebenen. Nur die Anwendungsebene skaliert mit Parallelität; ihre Dimensionierung ist Gegenstand von §8.

### 9.3 Routing ist der Teil, bei dem man sich leicht vertut

Drei Arten von Anfragen müssen drei verschiedene Ziele erreichen, und die Standardkonfiguration mit einer einzigen Regel erfüllt höchstens zwei davon.

Compile-Verkehr unter `/project/` sollte mittels konsistentem Hashing über den Projektbezeichner verteilt werden, damit der Compile-Cache eines Projekts bei einer Replik bleibt. Wir verwenden Haproxys `balance hash path,field(3,/)` mit `hash-type consistent`  und `hash-balance-factor 150`. Die Wahl ist bei der Skalierung wichtig: Mit Cookie-Affinität bleiben bestehende Sitzungen auf unbestimmte Zeit an ihre ursprüngliche Replik gebunden, und eine neu hinzugefügte Replik erhält nur neue Benutzer, sodass die Maschine, für die der Betreiber gerade bezahlt hat, von der Last, die ihren Kauf motivierte, nichts abbekommt. Konsistentes Hashing verteilte in unserer Konfiguration 35 % der Projekte bei der Skalierung, gegenüber 0 % bei Cookies.

Sitzungsverkehr ist anders. Wenn das WebSocket-Upgrade fehlschlägt und `socket.io` auf XHR-Polling zurückfällt, müssen aufeinanderfolgende Polls einer Sitzung dieselbe Replik erreichen, und es gibt im Pfad keinen Projektbezeichner zum Hashen. Dieser Verkehr braucht ein separates Backend mit Cookie-Affinität. Wir haben diese Aufteilung entworfen, aber nicht ausgerollt; wir kennzeichnen sie als Lücke, statt sie zu behaupten.

Schließlich, `/git/` muss die Replik erreichen, neben der `git-bridge` läuft. Es wird zu dieser Replik und nicht direkt zu `git-bridge` geroutet, weil die Bridge ihre Rückrufe gegen die OAuth-Endpunkte der Anwendung authentifiziert und Blob-URLs über sie auflöst; das Umgehen der Replik bricht die Authentifizierung, statt irgendetwas zu verbessern.

### 9.4 Scale-in braucht einen Drain-Puffer

Das Entfernen einer Replik ist nicht symmetrisch zum Hinzufügen einer: Eine laufende Kompilierung geht verloren, und der Benutzer sieht einen von ihm nicht verursachten Fehler. Die praktikable Abfolge ist, zuerst neuen Verkehr zu stoppen, zu warten und erst dann zu beenden. Wir haben dies als Pre-Stop-Hook implementiert, der das Pod für ein konfigurierbares Intervall hält, während der Balancer das Backend als draining markiert — kurz genug, um es in Minuten zu testen, und in Produktion lang genug für ein natürliches Sitzungsende, also Stunden statt Sekunden. Das Intervall ist der Stellknopf, der entscheidet, ob Elastizität unsichtbar oder verärgernd ist.

Eine weitere Einschränkung, die wir durch Messung und nicht durch Design gefunden haben: Autoskalierung nach CPU funktioniert für diese Arbeitslast nicht. Die eigene Auslastung des Anwendungspods lag bei 22 m core gegenüber einer Node-Gesamtsumme von 3997 m core, weil die Kompilierarbeit in Schwester-Containern geschieht, die der Pod nicht mitrechnet. Jedes Signal, das zur Skalierung dieser Ebene verwendet wird, muss laufende Compile-Container zählen, nicht die CPU des Pods.

## 10. Implikationen jenseits von Overleaf

Nichts in §4.2 oder §4.3 ist spezifisch für Overleafs Code. Die gemessenen Gesetze folgen aus drei Eigenschaften, die jeder gehostete LaTeX-Dienst teilt: Die Arbeitseinheit ist ein ein-threadiger Prozess, sie ist in einem Container isoliert, und ihr Arbeitsset ist ein großer schreibgeschützter Baum, den der Page Cache halten muss. Drei Konsequenzen übertragen sich direkt auf jeden, der einen solchen Dienst baut.

### 10.1 Speicher bereitstellen, dann Kerne

Das stärkste Ergebnis der Matrix ist negativ: Unter 16 GiB ist die Kernzahl nahezu irrelevant, und erst bei 48 GiB trennen sich 4-, 8- und 16-vCPU-Konfigurationen überhaupt (143, 268, 331). Ein Betreiber, der die herkömmliche Regel als „einen Kern pro fünf Benutzer“ liest, kauft die falsche Ressource. Der Mechanismus ist der gemeinsame Page Cache über den Distributionsbaum, und er ist eine Eigenschaft der Größe von TeX Live und nicht eines bestimmten Front-Ends.

### 10.2 Die Aufnahmerate ist eine Ressource und wird meist vergessen

Bei $$N=1024$$ Unser Server hielt nie mehr als 205 aktive Sandboxes (Abbildung 7b), obwohl jede Anfrage gleichzeitig eintraf. Das Erzeugen von Containern, nicht das Kompilieren, war der begrenzende Faktor — im Einklang mit Messstudien, die die Startkosten von Containern eher dem Laufzeit-Overhead als der Imagegröße zuschreiben \[19, 20]. Ein Dienst, der nur CPU und Speicher dimensioniert, wird feststellen, dass sein Burst-Verhalten von einer Größe bestimmt wird, die er nie gemessen hat. Die praktische Form davon ist die Empfehlung von §8: Begrenzen Sie die Aufnahmerate bewusst, denn eine Warteschlange, die Sie wählen, ist besser als eine, die Sie entdecken.

### 10.3 Eine Sandbox, die ihre Kompilierung überlebt, entwertet das Modell

Jede Kapazitätsangabe hier setzt voraus, dass der Container erstellt wird, eine Kompilierung ausführt und beendet wird — eine Lebensdauer von zig Sekunden und ein Duty Cycle nahe eins nur während des Laufs. Zwei jüngere Designmuster brechen diese Annahme, und zwar auf dieselbe Weise.

Das erste ist die persistente Sandbox pro Benutzer. Wenn jedem Benutzer eine feste private Umgebung zugewiesen wird, verwandelt sich ein statistisch multiplexierter Pool in eine Menge von Reservierungen: Ein Dienst, der mit 64 Kernen durch Zeitmultiplexing 256 gleichzeitige Kompilierungen bedienen könnte, kann nur noch 16 Benutzer bedienen, wenn jedem vier dedizierte Kerne gegeben werden — also um eine Größenordnung weniger bei derselben Hardware. Unsere Daten quantifizieren die Kosten dieser Entscheidung, statt sich dagegen zu wenden — Reservierungen kaufen Vorhersagbarkeit, und der Wechselkurs beträgt ungefähr $$16\times$$ bei dem von uns empfohlenen Betriebspunkt.

Das zweite und neuere Muster ist der KI-Agent, der die Sandbox mit dem Compiler teilt. In agentenunterstützten Autoring-Plattformen kann derselbe Container, der XeLaTeX ausführt, auch einen langlaufenden Coding-Agenten beherbergen, sodass er kontinuierlich statt in Bursts belegt ist. Praktiker berichten genau das Symptom, das das Modell für solche Bereitstellungen vorhersagt — anhaltende Trägheit bei moderater Benutzerzahl \[15]. Die Interaktion lohnt sich präzise zu benennen, denn es ist nicht einfach „mehr Last“. Drei unserer Befunde verstärken sich. Die Belegung ist nicht mehr burstartig, sodass das Zeitmultiplex-Gesetz von §4.2 auf die gesamte Population zugleich statt nur auf den aktuell kompilierenden Anteil angewendet wird. Der Page Cache, der den überlinearen Speichereffekt von §4.1 ermöglicht, wird nun mit dem eigenen Arbeitsset eines Agenten geteilt und ist für TeX nicht mehr warm. Und das fehlende Container-Speicherlimit von §6.2 wird weit gefährlicher, weil ein Container, der nie endet, seinen Speicher nie zurückgibt.

Wir haben eine solche Plattform nicht gemessen und erheben keinen Anspruch auf ein bestimmtes Produkt. Was wir sagen können, ist, was unsere Zahlen für das Design implizieren: Eine Architektur, die jedem Benutzer eine langlebige Multi-Core-Sandbox gibt, sollte als Reservierungssystem dimensioniert werden, nicht nach den hier berichteten Parallelitätszahlen, und die Kapazität, die sie erwarten kann, liegt näher bei Kernzahl geteilt durch Kerne pro Benutzer als bei irgendetwas in Tabelle 1.

## 11. Bedrohungen der Validität

### 11.1 Einzelnes Dokument

Alle Messungen verwenden ein 63-seitiges XeLaTeX-Dokument. Absolute Kapazitäten werden für andere Dokumente anders ausfallen; die Skalierungsgesetze, die Verhältnisse sind, sollten es nicht. Ein Dokument mit deutlich größerem Resident Set würde die Speicherwand verschieben, ohne seinen überlinearen Charakter zu ändern.

### 11.2 Virtualisierter Host

Die Gäste laufen unter KVM auf einer physischen Maschine, sodass die absoluten Zahlen Virtualisierungs-Overhead enthalten und die Gäste sich einen Host-Page-Cache und ein NVMe-Gerät teilen. Den größten Störfaktor haben wir dadurch gemildert, dass wir nicht verwandte Gäste heruntergefahren haben, nachdem wir beobachtet hatten, dass Host-Speicherdruck die Lastdurchschnitte im Gast um mehr als $$3\times$$ bei identischer Parallelität anhebt.

### 11.3 Gleichzeitige Ankunft

Jede Kompilierung wird zu einem einzigen Zeitpunkt ausgelöst, was der Worst Case ist. Echte Benutzer treffen stochastisch ein, sodass eine durch unsere Zahlen dimensionierte Bereitstellung eher einen Puffer als ein Defizit hat — aber der Peak am Ende einer Einreichungsfrist liegt näher an unserem Modell als an einem Poisson-Modell.

### 11.4 Randkonfigurationen

Bei 2 GiB ist das System nahe genug am Zusammenbruch, dass wiederholte Läufe derselben Konfiguration um eine Kompilierung abweichen können. Wir berichten den konservativen Wert und ziehen aus Unterschieden von $$\pm 1$$ in diesem Regime keine Schlüsse.

## 12. Verfügbarkeit

Das untersuchte System, die Bereitstellungswerkzeuge und das Upstream-Projekt, von dem es abgeleitet ist, sind alle öffentlich:

* Ayakaleaf Pro — <https://github.com/ayaka-notes/ayakaleaf-pro>
* Deployment-Toolkit — <https://github.com/ayaka-notes/toolkit>
* Dokumentation — [https://ayakaleaf-pro.ayaka.space](https://ayakaleaf-pro.ayaka.space/)
* Upstream Overleaf — <https://github.com/overleaf/overleaf>
* TeX-Live-Compile-Images — `ghcr.io/ayaka-notes/texlive-full:2025.1`

Jede von uns zitierte Quellstelle ist als repository-relativer Pfad mit einer Zeilennummer gegen Ayakaleaf Pro v6.2.2 angegeben, und die beiden Upstream-Commits, die wir datieren (`9a519f0d3d`, `5d472e9b38`), sind in der Overleaf-Historie erreichbar.

## 13. Beiträge

Musicminion entwarf die Studie, stellte die Testumgebungen bereit und betrieb sie, leitete die Untersuchung und verifizierte jede hier berichtete Messung. Claude Opus 5 (Anthropic) baute und betrieb das Benchmark-Harness, automatisierte die Bereitstellungen, führte die Quellcode-Archäologie durch, erstellte die Abbildungen und verfasste das Manuskript. Beide Autoren prüften den endgültigen Text. Wo ein Lauf als kontaminiert berichtet wird — der Sweep mit 1021 Sitzungen von §4.3 und das anomale $$N=256$$ Niveau von §4.1 — wurde der Defekt während der Begutachtung gefunden und der Lauf vor der Veröffentlichung wiederholt, statt stillschweigend verworfen zu werden.

Leser sollten beachten, dass die Autorschaftsrichtlinien von ACM, IEEE und ICMJE derzeit die Autorschaft für Parteien reservieren, die Verantwortung für eine Arbeit übernehmen können, und die Beitrag des zweiten Autors als Offenlegung statt als Byline erfordern würden. Wir nennen die Arbeitsteilung hier ausdrücklich, damit der Eintrag unter beiden Konventionen korrekt ist.

## 14. Schluss

Kapazitätsplanung für selbst gehostetes Overleaf ist keine Frage des Hochskalierens einer einzelnen Ressource. Drei Befunde sollten ändern, wie sie erfolgt.

Erstens: Unter 32 GiB Gastspeicher spielt die Kernzahl kaum eine Rolle: Bei 16 GiB unterscheiden sich die Kapazitäten von Gästen mit 4, 8 und 16 vCPU um weniger als 8 %. Der Speicher setzt über den gemeinsamen Page Cache über den TeX-Live-Baum die Grenze; Kerne werden erst relevant, wenn der Speicher großzügig ist.

Zweitens überwiegen zwei Softwareparameter die Hardware. Das Anheben von CLSIs fest codierter Obergrenze von 65 Kompilierungen und das Erhöhen des standardmäßigen Compile-Timeouts von 180 s hob einen 8-vCPU-/48-GiB-Gast von 64 auf 268 gleichzeitige Kompilierungen — ein Faktor von 4,2 ohne zusätzliche Hardware. Keines von beiden ist aus der Konfigurationsdokumentation ersichtlich; eines ist überhaupt nicht konfigurierbar.

Drittens ist die Frage „wie viele gleichzeitige Benutzer dieses Gerät unterstützt“ unterbestimmt. Gleichzeitigkeit in diesem System ist reines Time-Sharing, und die Kapazität ist das, was das Zeitlimit zulässt. Die ehrliche Form der Antwort nennt beides: *dieser Rechner bedient* $$N$$ *gleichzeitige Kompilierungen, wenn die Benutzer warten* $$T$$ *Sekunden*, mit $$N$$  und $$T$$ verknüpft durch Gleichung (1).

Wir berichten auch einen latenten Defekt: Die Speichergrenze pro Container im Docker-Runner ist seit 2018 unwirksam, sowohl in ihrer Höhe als auch in ihrer Platzierung. Die praktische Folge ist, dass Speichererschöpfung bei einer kleinen Bereitstellung den gesamten Dienst außer Betrieb setzt, statt nur die eine verantwortliche Kompilierung.

## Literatur

\[1] Overleaf. *Hardwareanforderungen*, On-Premises-Dokumentation. <https://docs.overleaf.com/on-premises/getting-started/requirements/hardware-requirements>

\[2] Overleaf. *Horizontale Skalierung*, On-Premises-Dokumentation. <https://docs.overleaf.com/on-premises/maintenance/horizontal-scaling>

\[3] Overleaf. *Microservices*, On-Premises-Dokumentation. <https://docs.overleaf.com/on-premises/getting-started/microservices>

\[4] Overleaf. *Quellrepository*. <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 und D. Lewin. *Konsistentes Hashing und zufällige Bäume: Verteilte Caching-Protokolle zur Entlastung von Hotspots im World Wide Web*. STOC, 1997.

\[8] J. Tan und M. Rigger. *Inkonsistenzen in von TeX erzeugten Dokumenten*. In *Proc. 33rd ACM SIGSOFT International Symposium on Software Testing and Analysis (ISSTA)*, Wien, 2024. doi: <https://doi.org/10.1145/3650212.3680370>

\[9] C. A. Ellis und S. J. Gibbs. *Nebenläufigkeitskontrolle in Groupware-Systemen*. In *Proc. ACM SIGMOD*, S. 399–407, 1989.

\[10] D. A. Nichols, P. Curtis, M. Dixon und J. Lamping. *Fensterung mit hoher Latenz und geringer Bandbreite im Jupiter-Kollaborationssystem*. In *Proc. ACM UIST*, S. 111–120, 1995.

\[11] M. Shapiro, N. Preguiça, C. Baquero und M. Zawirski. *Konfliktfreie replizierte Datentypen*. In *Proc. SSS*, S. 386–400, 2011.

\[12] N. J. Gunther. *Partisanische Kapazitätsplanung: Ein taktischer Ansatz zur Planung hoch skalierbarer Anwendungen und Dienste*. Springer, 2007.

\[13] The LaTeX3 Project. *l3build — Ein Test- und Build-System für (La)TeX*. CTAN.

\[14] M. Isaksson. *Welches LaTeX-Build-System ist am schnellsten? Ein Benchmark*. <https://blog.martisak.se/latex-build-systems-comparison/>

\[15] Berichte von Praktikern über anhaltende Latenz in agentenunterstützten Autorenplattformen, die einen persistenten Coding-Agenten mit dem LaTeX-Compiler in einer Sandbox pro Benutzer zusammenführen. Wir zitieren dies als berichtete Betriebserfahrung, nicht als kontrollierte Messung; wir haben eine solche Plattform nicht benchmarkiert.

\[16] D. E. Knuth. *Das TeX-Buch*. Addison-Wesley, 1984.

\[17] G. Lim, M. Ham, J. Moon und W. Song. *LightSys: Leichtgewichtiges und effizientes CI-System zur Verbesserung der Integrationsgeschwindigkeit von Software*. arXiv:2101.07961 \[cs.SE], 2021. Vorabveröffentlichung.

\[18] G. Lim, M. Ham, J. Moon, W. Song, S. Woo und S. Oh. *TAOS-CI: Leichtgewichtiges & modulares Continuous-Integration-System für Edge Computing*. arXiv:2101.08889 \[cs.SE], 2021. Vorabveröffentlichung.

\[19] S. Khan. *Zerlegung der Startleistung von Docker-Containern: Eine dreistufige Messstudie auf heterogener Infrastruktur*. arXiv:2602.15214, 2026. Vorabveröffentlichung.

\[20] R. Gupta und K. Nahrstedt. *Leistungscharakterisierung von Containern im Edge Computing*. arXiv:2505.02082, 2025. Vorabveröffentlichung.

\[21] S. Checkoway, H. Shacham und E. Rescorla. *Sind textbasierte Datenformate sicher? Oder: Verwenden Sie diese LaTeX-Klassendatei, um Ihren Computer zu kompromittieren*. In *Tagungsband des USENIX Workshop on Large-Scale Exploits and Emergent Threats (LEET)*, 2010.

\[22] G. Lacombe, K. Masalygina, A. Tahiri, C. Adam und C. Lauradoux. *Können Sie LaTeX-Dateien von Fremden annehmen? Zehn Jahre später*. arXiv:2102.00856 \[cs.CR], 2021. Vorabveröffentlichung.

\[23] J. D. C. Little. *Ein Beweis für die Warteschlangenformel* $$L=\lambda W$$. Operations Research, 9(3):383–387, 1961.

\[24] G. M. Amdahl. *Gültigkeit des Single-Processor-Ansatzes zur Erreichung von Rechenkapazitäten im großen Maßstab*. AFIPS, 1967.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://ayakaleaf-pro.ayaka.space/blog/de/2026/overleaf-benchmark.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
