Skip to main content

Overleaf-Benchmark.pdf

概要

セルフホストの Overleaf デプロイは、一般的に「同時ユーザー 5〜10 人あたり CPU 1 コアとメモリ 1 GB」という単一の経験則でサイジングされています。本稿では、この経験則が単に不正確なだけでなく構造的に誤っていることを示します。なぜなら、この経験則は単一のリソース次元が容量を支配すると仮定していますが、実際には独立した 2 つの壁が容量を支配しており、さらにハードウェアではない 2 つのソフトウェアパラメーターが最大 4 倍もの差で結果を左右するからです。 ホストのコアを 3.0 GHz にクロック固定した QEMU/KVM ゲスト内で、サンドボックスコンパイル(TeX Live 2025)を有効にした標準構成の Ayakaleaf Pro v6.2.2 デプロイを、21 通りの CPU/メモリ構成で測定しました。ワークロードは実際の 63 ページの XeLaTeX 論文で、最大数百の異なるユーザーアカウントから同時にコンパイルされます。その結果、ゲストメモリが 32 GiB 未満ではコア数はほぼ無関係であり(16 GiB では 4、8、16 vCPU のゲストの測定容量の差は 8% 未満)、代わりに TeX Live ツリーに対する共有ページキャッシュに起因する超線形のメモリの壁によって容量が支配されることがわかりました。 これらの法則が 1 桁異なるスケールでも成り立つかを確かめるため、64 コア、995 GiB の単一サーバーで同じスイープを繰り返しました。このサーバーは 1024 件の同時コールドコンパイルを成功率 100% で処理し(スレッド数の 8 倍)、上限に達することはありませんでした。有用な数値はその上限ではなく、その手前にある「ニー(屈曲点)」です。テールレイテンシは N=256N=256 までは倍増ごとに 20〜40% 増加しますが、N=512N=512 では 190% 増加します。したがって、「失敗しない最大の同時実行数」として報告される容量は、実際に使える動作点を 4 倍過大評価することになります。このマシンではメモリが制約となることは一度もなく、限界は CPU と、コンテナデーモンが新しいサンドボックスを受け入れられる速度であり、後者は要求されるコンパイル数にかかわらず 200 付近で飽和します。 さらに、容量計画からは見えない 2 つの実装レベルの影響を特定しました。第一に、CLSI には同時コンパイル数 65 というハードコードされた上限があり、どの環境変数からも設定できません。これを超えると、ユーザーはキューに入れられるのではなく即座に HTTP 503 を受け取ります。第二に、Docker ランナーのコンテナごとのメモリ制限は、2018 年の導入以来、その大きさと適用箇所の両面で効果がなく、メモリ不足が発生すると単一のコンパイルではなくホスト全体がダウンします。同時実行数の上限を解除し、デフォルトのコンパイルタイムアウトを 180 秒から 300 秒に引き上げると、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 の全範囲にわたるクロックスイープでは、30 件の測定値が k=27.9GHz⋅sk=27.9 GHz·s、残差のばらつき 5.1% で T=(k/f)max⁡(1,N/C)T=(k/f)\max(1,N/C) 上に収束します。5.5 倍のクロックは収穫逓減なしに 5.5 倍の高速化をもたらします。これが、クロックとコアがもたらすものが異なるという意味です。クロックはすべてのユーザーのコンパイルを速くし、コアはより多くのユーザーを受け入れるだけです。

1. はじめに

Overleaf は共同編集型 LaTeX エディターの主流であり、そのオンプレミス版は、未発表の原稿をサードパーティのクラウドに送信できない大学や研究グループに広く導入されています。こうしたデプロイのサイジングは、繰り返し問われる実務上の問題です。固定のハードウェア予算で、実際に何人が同時に「Recompile」を押せるのでしょうか。 公式のガイダンスは線形の規則、すなわち同時ユーザー 5〜10 人あたりおよそ 1 コアと 1 GB というもので、容量が両方のリソースに対して滑らかかつ連動してスケールすることを前提としています。私たちの測定結果は、これと 3 つの点で矛盾します。

1.1 容量を支配するのは 1 つではなく、独立した 2 つの壁

ある構成が失敗する原因は、メモリが枯渇して Overleaf スタック自体がダウンし HTTP 502 を返す場合か、コンパイルがサーバー側のタイムアウトを超え、何ギガバイトものメモリが未使用のまま CLSI が timedout を報告する場合のどちらかです。これら 2 つの領域は、スケーリングの挙動も対処法もまったく異なります。メモリが制約となっている構成にコアを追加することは非効率なだけでなく、逆効果になることさえあります。コア数を増やすと容量が 減少 する構成を実際に測定しました。コアが増えると同時コンパイルが歩調を合わせて進行するため、ピーク時のメモリ需要が分散せずに重なってしまうのです。

1.2 ソフトウェアパラメーターがハードウェアを上回る影響を持つ

コンパイルタイムアウトは MongoDB 内のユーザーごとのフィールドで、デフォルトの 180 秒が CPU 制約の構成の上限を暗黙のうちに抑えています。これを 300 秒に引き上げると、同じハードウェアで測定容量が最大 4.2 倍になります。それとは独立に、CLSI はハードコードされた定数によって 65 件を超える同時コンパイルを拒否します。この両方を考慮していない容量調査(およびデプロイ)は、マシンではなくソフトウェアを測定していることになります。

1.3 同時実行は並列化ではなくタイムシェアリング

LaTeX のコンパイルはシングルスレッドであるため、CC コアで NN 人の同時ユーザーに対応しても、システムが早く処理を終えるわけではありません。すべてのユーザーが比例して長く待つことになるだけです。したがって、「何人の同時ユーザーをサポートできるか」という問いは、ユーザーがどれだけ待てるかを決めない限り不適切な問いです。本稿ではこの依存関係を明示し、定量化します。

1.4 貢献

  • クロック固定かつ再試行で検証した条件下で測定した 21 通りの CPU/メモリ構成にわたる容量マトリクス。各構成について、失敗の特徴から制約となっている要因を特定しています。
  • 2 つのフィッティングモデル: 超線形のメモリの壁と CPU の上限を分離する容量モデルと、純粋なタイムシェアリングの挙動を確立するレイテンシモデル。
  • デプロイされたシステムにおける 2 つの実装上の問題の特定と実験的確認。2018 年以来機能していないコンテナのメモリ制限を含みます。
  • コンパイルタイムアウトと容量のトレードオフの定量化。同時実行数の数値には必ずこれを併記すべきだと私たちは主張します。

2. 背景

2.1 コンパイル経路

Overleaf のコンパイルリクエストは web →\rightarrow clsi →\rightarrow コンパイルコンテナの順に伝わります。サンドボックスコンパイルのデプロイ(SIBLING_CONTAINERS_ENABLED=true)では、CLSI は latexmk をプロセス内で実行しません。代わりに、バインドマウントされたソケット経由でアクセスできるホストの Docker デーモンに対し、プロジェクトディレクトリを /compile にバインドマウントした TeX Live イメージから新しいコンテナを起動するよう要求します。したがって、1 回のコンパイルは、1 つの latexmk プロセスを実行する 1 つの短命なコンテナです。 ここから 3 つの帰結が導かれ、そのすべてが本稿の測定結果を形作っています。第一に、作業単位はシングルスレッドのプロセスです。XeLaTeX は並列化しません。第二に、コンパイルごとのリソース分離は Docker ランナーが要求する内容次第であり、§6.2 で示すように、実質的に何も要求していません。第三に、ワーキングセットを支配するのは文書ではなく TeX Live ツリーです。これはおよそ 32 GiB の読み取り専用のコーパスで、すべての同時コンパイルがここから読み込むため、ホストのページキャッシュを通じて共有されます。この共有こそが、私たちが観測した超線形のメモリスケーリングの原因です。

2.2 サンドボックスコンパイルの有効化

コミュニティ版 Overleaf は、アプリケーションコンテナ自体の中で latexmk を実行します。Ayakaleaf Pro は、Overleaf Server Pro と同様に、各コンパイルを 兄弟 コンテナで実行できます。これは、アプリケーションコンテナ内にネストされるのではなく、アプリケーションによって ホストの Docker デーモン上で起動されるコンテナです。この機能は 2 つの Toolkit 設定で有効になります:
Toolkit はホストの Docker ソケットをアプリケーションコンテナにバインドマウントし、これらの設定を CLSI が読み取る環境変数 SANDBOXED_COMPILES=true、SANDBOXED_COMPILES_SIBLING_CONTAINERS=true、SANDBOXED_COMPILES_HOST_DIR に変換します。最後のものはコンパイルディレクトリの ホスト 上のパスです。このパスは重要です。コンパイルコンテナを起動するデーモンはホストのものであるため、そこに渡されるバインドマウントは、アプリケーションコンテナではなくホストの名前空間で解決できなければなりません。さらに Server Pro の config/env.sh は、コンパイルコンテナが書き込むファイルの所有者を一貫させるため、このモードでは TEXLIVE_IMAGE_USER=www-data を強制します。 確認は簡単です。コンパイル中、ホスト上には TeX Live イメージから latexmk を実行する project-{projectId}-{userId}-{hash} という名前のコンテナが表示され、終了コード 0 で終了します。これが本稿全体を通じてその多重度を測定する単位であり、§6.2 で報告するように、リソース制限がまったく存在しない単位でもあります。 兄弟コンテナによって測定はクリーンになります。各コンパイルは観測可能で独立にスケジュールされる OS エンティティだからです。しかしそれは同時に、コンパイル間の CPU とメモリの調停を行うのが Overleaf ではなくゲストのカーネルであることも意味します。したがって、本稿のすべてのスケーリング則は、NN 個のシングルスレッドプロセスに適用された Linux スケジューラーの性質であり、それゆえにこれほど規則的なのです。
図 1. コミュニティ版のマイクロサービスを通じてトレースした 1 件のコンパイルリクエスト。ステップ間の分岐は容量にとって重要です。文書のテキストはリクエストボディにコピーされる一方、バイナリアセットは参照で渡され clsi によって取得されます。どちらも支配的ではありません。プロジェクトのコンパイルコストは、すべての同時コンパイルが共有ページキャッシュを通じて読み込む 32 GiB の TeX Live ツリーによって決まります。
図 2. 3 つのデプロイトポロジーと、それぞれにおけるインスタンスごとのコンパイル上限の位置。重なったパネルはレプリケーションを表します。65 件のコンパイルという定数は 1 つの CLSI を保護するものなので、SaaS のフリートではインスタンス数とゾーン数を掛けた値になり(a)、Server Pro と Ayakaleaf Pro がサポートする水平スケーリングではインスタンス数を掛けた値になります(c)。ただしその代償として、中央の MongoDB、Redis、S3 互換ストレージ、Cookie によるセッションアフィニティを備えたロードバランサー(コンパイル出力はインスタンスローカルのディスクに書き込まれるため、コンパイルとその後の PDF ダウンロードは同じインスタンスで処理される必要があります)、そして単一の git-bridge が必要になります。私たちが測定した Toolkit のデフォルト(b)は乗数が 1 であるため、フリートの 1 メンバー向けにサイジングされた定数がインストール全体の上限になってしまいます。
図 3. clsi-cache におけるシャード選択。プロジェクトは crc32⁡(projectId-i) mod ∣shards∣\operatorname{crc32}(\text{projectId}\text{-}i)\bmod|\text{shards}| によってマッピングされます。つまり、ハッシュ空間はシャードの数と同じ数の等しいセクターに分割されます。これはリングベースのコンシステントハッシュではなく 剰余 ハッシュです。フリートを 3 シャードから 4 シャードに拡張すると空間全体が再分割され、実質的にすべてのプロジェクトが再マッピングされます(a、b)。だからこそ、この実装では、コンシステントハッシュリングがもたらす K/nK/n の移動ではなく、時間枠をかけて線形に増加する割合のプロジェクトを currentShards から desiredShards へ移す、明示的なオンラインのリシャーディング勾配が必要になるのです。シャードのサーキットブレーカーが作動すると、ソルト ii がインクリメントされてそのシャードが候補リストから除外されるため、ルックアップは失敗する代わりに次の候補を探索します(c)。

2.3 2 つの失敗モード

測定したすべての構成は、2 つの方法のうちどちらか一方でのみ失敗し、その区別は推測ではなくレスポンスのステータスから確認できます:
  • メモリ枯渇 — Overleaf スタック自体が応答しなくなり、リクエストは HTTP 502 を返します。失敗するレベルでのゲストの利用可能メモリは通常 500 MiB 未満です。
  • コンパイルタイムアウト — CLSI がユーザーごとのタイムアウトでコンパイルを終了し、ステータス timedout を報告します。失敗するレベルでの利用可能メモリは数ギガバイトあることが多いです。
各構成は、リソース比率に基づくヒューリスティックではなく、この特徴によって分類します。これにより、「どちらの壁に当たったか」という問いにデータそのものから答えられるようになります。

3. 方法論

3.1 テストベッドとクロック制御

すべてのゲストは、62 GiB の RAM と NVMe ストレージを備えた 1 台の Intel Core i9-14900K ホスト上の QEMU/KVM で動作します。ゲストは Docker 29.7 を搭載した Ubuntu 24.04 で、Overleaf Toolkit により、texlive-full:2025.1 に対するサンドボックスコンパイルを有効にした Ayakaleaf Pro v6.2.2 をデプロイしています。 一般向けのデスクトップ CPU は、クロックが制御されていない限り、サーバーの代用としては不適切です。KVM には仮想クロックを設定する仕組みがありません。vCPU はホストのスレッドであり、ホストのコアが動作している周波数で動作します。そこで私たちはホストを直接制約し、ターボを無効にしてすべてのコアで scaling_max_freq を 3.0 GHz に固定し、taskset でゲストの vCPU を物理 P コアに固定しました。ハイブリッドコア CPU ではこの区別が重要です。この CPU の E コアはベースクロックが 2.4 GHz で、ターボを無効にすると 3.0 GHz に 到達できない ため、実行が E コアに流れると、暗黙のうちにより遅いマシンを測定してしまいます。全負荷時に、固定した 16 スレッドすべてでちょうど 3000 MHz であることを確認しています。ガードスクリプトがすべてのベンチマークの前にこの不変条件を検証し、満たされない場合は開始を拒否します。調査中、このスクリプトはガバナーが暗黙のうちにリセットされた事例を 1 件検出しました。

3.2 2 つ目のテストベッド: 1 台の大型ランナー

QEMU マトリクスは一度に 1 つの変数を分離しますが、固定スレッド 16 が上限です。同じ法則が 1 桁上のスケールでも成り立つかを確かめるため、同時実行数のスイープを 1 台の大型サーバーで繰り返しました。995 GiB の RAM を搭載した AMD EPYC 7773X(Milan-X、64 コア / 128 スレッド、768 MiB の L3)1 基で、同じ texlive-full:2025.1 に対して同じ Ayakaleaf Pro v6.2.2 イメージを実行しています。QEMU ゲストとは異なり、このマシンはクロック固定されていません。本番クラスのサーバーであり、そのまま本番サーバーとして測定しています。 2 つの運用上の予防措置が必要であり、述べておく価値があります。これがなければ、実験はサーバーではなくハーネスを測定することになるからです。第一に、すべてのコンテナを MemoryMax=940 GiB の systemd スライスに閉じ込め、スイープが暴走してもホストではなく cgroup が枯渇するようにしました。第二に、サンドボックスコンパイルはホストのデーモンによって作成され、それぞれが自身のコピーオンライトレイヤーに書き込みます。20.6 GiB のベースイメージは共有されているにもかかわらず、コンテナあたり 116 MiB と測定されたため、Docker のデータルートを専用の NVMe デバイスに移動しました。N=1024N=1024 のスイープではおよそ 119 GiB のスクラッチレイヤーが書き込まれ、標準のルートファイルシステムには収まりません。

3.3 ワークロード

文書は実際の 63 ページの修士論文(SJTU テンプレート)で、latexmk 経由で XeLaTeX によってコンパイルされ、TikZ の図、biblatex による参考文献処理、埋め込み PDF アセットを含みます。つまり、合成的な負荷ではなく現実的な負荷です。負荷のないゲストでの単一コンパイルは、すべての構成で 8.6〜9.8 秒かかり、これを自由飛行ベースライン T1T_1 として使用します。

3.4 負荷生成

512 個の実ユーザーアカウントを作成し、それぞれにプロジェクトのコピーを与えました。これにより、同時コンパイルはプロジェクトのロックを共有するのではなく、独立したユーザーとまったく同じように競合します。リクエストはホストからゲストの転送ポートに対して発行されるため、負荷生成がゲストの CPU を消費することはありません。 同時実行は、時間をずらしたものではなく 同時 です。まずすべてのセッション(ログイン、CSRF トークン、コンパイラー選択)を確立し、その後で初めて各スレッドが、一度だけ計算されて共有される共通の壁時計時刻までスリープしてから POST /project/:id/compile を発行します。この区別は細かいことではありません。時間をずらしたランプは安定したキューでのスループットを測定し、同時バーストは、講義室の学生たちが同じ締め切りのアナウンスの後に一斉に同じボタンを押したときに何が起こるかを測定します。後者こそ運用者が実際に恐れているケースです。2 つは定数倍以上異なります。後者では、デーモンが処理できるよりも速くコンパイルキューが埋まるからです。 そのバーストを忠実に送り出す前に、4 つの実務上の障害を取り除く必要がありました。いずれも、実験を暗黙のうちにサーバーではなくハーネスの測定に劣化させてしまうため、記録しておく価値があります。

3.4.1 レートリミッターは 1 つではなく 2 つ

Overleaf は送信元アドレスごとにログインを制限しており(1 分あたり 20 回)、私たちのトラフィックはすべて 1 台のホストから発生します。シミュレートした各ユーザーに異なる X-Forwarded-For アドレスを割り当てるとこの制限は解除されますが、すぐにより粗い 2 つ目の制限、すなわちサブネットごとの 1 分あたりおよそ 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 しか尊重せず、Overleaf の trustedProxyIps のデフォルトは loopback です。負荷生成器はループバックインターフェースではなくコンテナのブリッジ経由でアプリケーションに到達するため、ヘッダーは解析された後に破棄され、シミュレートしたすべてのユーザーが 1 つのアドレスに戻ってしまいます。症状はちょうど 20 回目のログインで発生する HTTP 429 の波で、サーバーの過負荷と誤解しやすいものです。ゲートウェイネットワークを明示的に信頼チェーンに追加する必要があります。§4.3 のクラスター構成のデプロイでは、Pod と Service の CIDR も追加する必要があります。

3.4.3 ロードバランサーは保持するよう求められたヘッダーを上書きする

インスタンスがプロキシの背後にある場合、慣例的な option forwardfor は実際のクライアントアドレスをチェーンに 追加 します。これは本番環境では正しい動作ですが、ここではまさに誤りです。合成アドレスが負荷生成器自身のアドレスに置き換えられてしまうからです。このディレクティブは option forwardfor if-none と修飾し、クライアントが値を指定しなかった場合にのみプロキシが値を追加するようにする必要があります。

3.4.4 サーバーの容量が尽きる前にクライアントのファイルディスクリプタが尽きる

N=1024N=1024 では生成器が 1000 を超えるソケットを同時に保持し、デフォルトのソフトリミットである 1024 ディスクリプタに、測定中ではなくセッションのセットアップ中に到達します。この失敗は目立ちません。3 つのセッションが確立に失敗し、実行結果は 1024 ではなく 1021 と報告されます。一方、コンテナ数を取得するためにシェルを呼び出すサンプリングスレッドは EMFILE で終了し、テレメトリが暗黙のうちに途切れます。生成器のソフトリミットを引き上げ(私たちのホストのハードリミットはすでに 1048576 でした)、実行を繰り返す必要があります。§4.3 では両方の実行を報告します。修正後の実行は 1024 件中 1024 件を完了し、中央値は途切れた実行と 1.2 秒以内の差でした。これが、最初の実行を使用可能だが決定的ではないものとして扱う理由です。

3.5 測定プロトコル

再現性のために、いくつかの方法論上の選択が必要であることがわかりました。

3.5.1 ウォームアップ

起動直後のゲストではページキャッシュが空で、最初のコンパイルは定常状態の容量ではなくコールドスタートの I/O を測定します。同じ 2 vCPU / 2 GiB 構成で、コールドでは 36.5 秒、ウォームでは 9.8 秒となり、3.7 倍の差があります。そのため、各構成では起動後に、結果を破棄する単一コンパイルのウォームアップを 2 回実行します。

3.5.2 合格基準

同時実行レベルは、すべての コンパイルが成功し、かつ再試行でも成功した場合にのみ合格とします。これは成功率のしきい値よりも厳格であり、それが重要です。4 vCPU / 16 GiB では、レベル 32 が中央値 80.2 秒で一度合格した後、再試行で 32 件すべてのコンパイルがタイムアウトしたため、31 と報告しています。

3.5.3 探索

レベルは、モデルで予測したシードからの指数的なブラケッティングの後、厳密な整数二分探索によって特定します。基準は全か無かであるため、レベルは最初の失敗で確定します。そこで、1 件が失敗した時点で残りの処理中のリクエストを放棄します。ただし小さなレベルは例外で、放棄したコンパイルが小さなゲストを強く占有してしまい、二度と回復しなくなるためです。

3.5.4 レベル間の分離

次のレベルを開始する前に、コンパイルコンテナを排出し、Web アプリケーションが再び応答するまでポーリングします。これを行わないと、クラッシュ後のレベルで、セッション数ゼロという偽の失敗が記録されます。

3.5.5 ホストの衛生管理

ホスト上の無関係な仮想マシンはシャットダウンしました。ホストメモリのうち 24 GiB が他で使用されている状態では、同じゲスト構成で、同一の同時実行数にもかかわらずロードアベレージが 3.2 ではなく 11.7 と報告されました。ホストのメモリ圧迫はゲストに伝播し、測定を無効にします。

4. 結果

4.1 容量マトリクス

表 1 と図 4 に、すべての構成について測定した上限を示します。行に沿って読むと、まず驚くべきことがわかります。4 GiB では、2、4、8 vCPU のゲストがいずれもちょうど 9 に達します。コアを 4 倍にしてもまったく何も変わりません。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 の上限です。構成は、2 つのうち先に到達した方によって制約されます。 列に沿って読むと 2 つ目のことがわかります。コア数を固定すると、容量はメモリに対して超線形に、おおよそ R1.6R^{1.6} で増加します。その理由は §5.1 で展開するページキャッシュによるものです。 表 1. 正常に完了する同時コンパイルの最大数。コンパイルタイムアウト 300 秒、CLSI の同時実行数上限を解除した状態で測定。太字 は CPU 制約の構成(メモリに余裕がある状態でコンパイルがタイムアウト)を示し、それ以外はメモリ制約(スタックが HTTP 502 でダウン)です。2 GiB の行には §5.2 で説明する補正が含まれています。

4.2 同時実行はタイムシェアリング

図 5 は、8 vCPU / 16 GiB に固定したゲストで、すべての同時実行レベルをスイープしたものです。2 つの領域が、ちょうど 1 コアあたり 1 コンパイルの位置にある鋭いニーで分かれています。それより下では、平均コンパイル時間は平坦で、N=1N=1 の 8.7 秒から N=C=8N=C=8 の 9.1 秒へと 5% 変化するだけです。それより上では、時間は N/CN/C に厳密に比例して増加します。N=16,24N=16,24 では 18.5 秒と 27.1 秒を測定しており、理想的な 1:2:31:2:3 に対して 1:2.13:3.121:2.13:3.12 の比率です。
図 5. ハードウェアを固定した場合の、同時実行数に対するコンパイルレイテンシ。ニーは N=CN=C にあり、それを超えると測定された減速は 5〜7% 以内で N/CN/C に追従します。15 レベルすべてが完全に成功しました。
図 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)となります。1 と区別できない指数は、コンパイルがシングルスレッドで CPU 制約の作業単位であり、同時実行はコアを分割する以上には役にも立たず害にもならないということを定量的に述べたものです。容量計画にとって、その実務上の帰結は不都合なものです。ある構成は、失敗 することなく任意の数のユーザーを吸収できますが、その代わりにすべてのユーザーを比例して長く待たせます。このゲストで N=56N=56 のとき、すべてのコンパイルはまだ成功しますが、各ユーザーは 8.7 秒ではなく 64.8 秒待つことになります。

4.3 1024 件の同時コンパイルへの垂直スケーリング

表 2 と図 7 は、大型ランナーでのスイープの結果です。すべてのレベルは コールド コンパイルです。各レベルの前に、DELETE /project/:id/output によって参加するすべてのプロジェクトのコンパイルディレクトリと CLSI キャッシュをクリアしているため、どのレベルもその下のレベルで行われた作業の恩恵を受けません。このマシンの単一コンパイルのベースラインは 28.8 秒です。これはコールドの数値であり、先に使用した 8.6〜9.8 秒の定常状態のベースラインと比較すべきではありません。QEMU ゲストでのコールドベースラインは 28.3 秒なので、このワークロードにおいて、スレッドあたりでは 2 台のマシンの差は 2% 以内です。 表 2. EPYC 7773X 1 基(64 コア / 128 スレッド、995 GiB)での同時実行数スイープ。すべてのレベルはコールド、ベースラインは 28.8 秒。ピークコンテナ数は、同時に存在したサンドボックスの最大数です。
図 7. 1 台の大型ランナーでの垂直スケーリング。(a)提供した同時実行数に対するレイテンシ。網掛けの領域はニーを超えた領域を示します。(b)実際に存在するサンドボックスの数は要求された数に追従せず、200 付近で飽和します。一方、コンパイルの cgroup が上限の 5 分の 1 を超えて使用することはありません。

4.3.1 マシンは一度も失敗しない

N=1024N=1024(スレッド数の 8 倍)を含め、すべてのレベルが 100% で完了します。このマシンの容量の上限は見つかりませんでした。マシンの余力が尽きる前に、私たちの忍耐が尽きたのです。これは、制約となる要因がメモリではない、この調査で最初の構成です。N=1024N=1024 では、コンパイルの cgroup のピークは 184 GiB で、上限 940 GiB の 5 分の 1 にすぎません。一方、CPU は使用率 100%、ロードアベレージ 166 に達しています。

4.3.2 受け入れがレート制限されているため劣化は線形以下になる

単純なタイムシェアリングでは、スレッド数の 8×8\times でレイテンシも 8×8\times になると予測されます。測定されたコストは、単一コンパイルと比べると 9.7×9.7\times ですが、N=128N=128 と比べると、提供負荷が 8 倍になったにもかかわらず 3.8×3.8\times にすぎません。その理由は図 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 と報告されることになりますが、運用者にとっては役に立ちません。その時点でのテールの待ち時間は 8 分近くになるからです。

4.4 コンパイル時間はクロックに反比例する

ワークロードは CPU 制約なので、そのコストは 1/f1/f でスケールするはずです。それ以外は変更していないゲストで、ホストのクロックをマシンの全範囲である 1.0〜5.5 GHz にわたって 10 段階でスイープし、これを直接検証しました(図 8)。単一コンパイルの時間は 26.5 秒から 4.8 秒に変化します。5.5 倍のクロックで 5.5 倍の高速化が得られ、範囲内のどこにも収穫逓減はありません。積 T ⁣⋅ ⁣fT\!\cdot\!f は、10 通りのクロックすべてにわたって 2% 以内で一定です。 コアの配分で正規化すると、30 件の測定値すべて(10 通りのクロックにおける 3 つの同時実行レベル)が 1 つの定数に収束します: 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.5 倍変化する範囲で、残差のばらつきは 5.1% です。曲率がまったくないこと自体が結果です。ワークロードがメモリ帯域や I/O に制約されていたなら、CPU が他のリソースを追い越すため、高クロックで TT は平坦になったはずです。
図 8. クロックスイープ。(a)フィッティングした双曲線による T=k/fT=k/f。(b)max⁡(1,N/C)\max(1,N/C) で割ると、すべての点が 1 つの定数に収束し、式(1)が確認されます。 式(1)には、述べるのは簡単で間違えるのも簡単な、調達に直結する帰結があります。クロックはすべてのユーザー個人の体験を改善し、コア数はより多くのユーザーを受け入れるだけ だということです。クロックが 20% 高いマシンは、収穫逓減なしに全員のコンパイルを 20% 速くします。コアを 2 倍にしても、誰のコンパイルもまったく速くなりません。

5. 分析

5.1 2 つの壁を個別にフィッティング

各構成を失敗の特徴(§2.3)によって分類し、その後、メモリの壁と CPU の上限を、実際にそれに当たった構成のみでフィッティングします: Nmax⁡=min⁡(ARp,  kcC)N_{\max} = \min\left(A R^{p},\; k_c C\right) ここで RR はギビバイト、CC は vCPU 数です。 メモリの壁の指数は一貫して超線形で、p>1p>1 です。追加の同時コンパイル 1 件あたりの限界メモリコストは、総メモリが増えるにつれて 減少 し、3 GiB のゲストではコンパイルあたりおよそ 312 MiB、32 GiB のゲストでは約 194 MiB です。そのメカニズムは、§2.1 で説明した TeX Live ツリーに対する共有ページキャッシュです。同時コンパイルは重複するフォントファイルやマクロファイルを読み込むため、大きなキャッシュはより多くのコンパイルで償却されます。これが、「ユーザー 5 人あたり 1 ギガバイト」という単純な規則が大きなマシンを過小評価し、小さなマシンを過大評価する理由です。
図 9. 同じデータを (C,R)(C,R) 平面上の 2 つの曲面として表したもの。(a)容量: フィッティングした曲面は平面ではなく尾根状です。メモリに対しては急峻に上昇し、メモリが制約でなくなるまではコア軸に沿ってほぼ平坦です。これが、コア数によって構成間に差が出るのが 48 GiB の行だけである理由です。(b)すべての構成における同時実行数に対するレイテンシ。フィッティングした T=12.6 (N/C)0.91T=12.6\,(N/C)^{0.91} を破線で、180 秒のタイムアウトを平面で示しています。構成は、その実線の曲線がこの平面を突き抜けるところで失敗します。これにより、タイムアウト設定がいかに直接的に報告される容量を決定するかがわかります。

5.2 コアを増やすと悪化する場合

式(2)は 2 つの項の最小値であるため CC について単調ですが、測定値はそうではありません。コアを追加すると容量が 減少 する逆転を 2 件観測しました。16 GiB(54 対 45)と 32 GiB(145 対 135)です。どちらもメモリ制約の領域で発生しており、メカニズムも同じです。コアが多いと同時コンパイルが歩調を合わせて進行し、同じ瞬間にピークの常駐サイズに達します。一方、コアが少ないとスケジューラーがそれらをインターリーブし、ピークがずれます。メモリの余裕がすでにぎりぎりのゲストでは、このずれこそがゲストを生き延びさせているのです。平均的なリソース使用量に基づく容量モデルではこれを表現できません。これはピークの 一致 に関わる性質です。 2 GiB での 3 つ目の見かけ上の逆転は、現在は考慮から除外しています。探索では 2 vCPU で容量 2、4 vCPU と 8 vCPU で容量 1 と記録されており、同じ効果のように見えます。しかし生のスイープを再調査すると、もっと単純なことがわかりました。2 GiB では、レベル N=2N=2 は 3 つのコア数すべてで最初の試行に成功し、その後 3 つのうち 2 つで確認の実行に失敗していました。このレベルは容量ではなくコイントスであり、2 vCPU のエントリはたまたまうまくいったトスにすぎません。そのため、3 つのコア数すべてで再現可能な値である 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 秒で安定していました。 原因は CLSI 内の定数です:
比較が非厳密なので、実効的な上限は 64+1=6564+1=65 となり、測定値と正確に一致します。超過したリクエストは HTTP 503 を受け取ります。キューに入れられるのではなく 拒否 されるため、ユーザー側からはコンパイルボタンが単に失敗するように見えます。同じファイル内の他のすべての調整可能な値とは異なり、この値はどの環境変数も読み取りません。2024 年 8 月にアップストリームで導入されたもので、イメージを変更することでしか変えられません。上限を引き上げると、N=80N=80 で success=65, unavailable=15 と報告していた同じ 16 vCPU / 32 GiB のゲストが、代わりに success=80 と報告しました。

6.2 機能していないコンテナのメモリ制限

稼働中のコンパイルコンテナを調べると、リソース分離がまったくないことがわかります:
CPU クォータがないのは設計どおりであり、§4.2 のタイムシェアリングの指数がこれほどきれいな理由を説明しています。コンパイル間の競合を歪めるものが何もないのです。しかし、メモリ 制限がないのは設計どおりではありません。Docker ランナーは実際にメモリ制限を要求しています:
これは 2 重に誤っています。コメントは 102431024^3 を意図しているのに、値は 10244=1tebiB1024^4=1 tebiB です。さらに、このフィールドは Docker API が期待する HostConfig の中ではなく、作成オプションのトップレベルに置かれているため破棄されます。観測された Memory=0 がそれを裏付けています。どちらの誤りも、このファイルを導入したコミット(9a519f0d3d、2018 年 3 月)に存在し、CoffeeScript からの変換、リポジトリ全体の再フォーマット、CJS から ESM への移行を経ても残り続けました。いずれも意味論を見直すものではなかったからです。注目すべきは、同じコミット内の MAX_OUTPUT = 1024 * 1024 // 1MB は正しいことで、これは誤解ではなく単純なミスであることを示しています。 その結果は、低メモリでの測定に表れています。コンパイルに上限がないため、メモリ枯渇は Docker が問題のある 1 つのコンテナを終了させる形では現れず、ゲスト全体をダウンさせます。2 vCPU / 2 GiB の構成では、監視用の SSH セッションが 300 秒間ブロックされ、2 コアでロードアベレージが 68 に達し、最終的にゲストが自ら再起動するのを観測しました。コンテナごとの制限が機能していれば、はるかに穏やかに劣化したはずです。大きすぎるコンパイルが失敗し、サービスは生き残ったでしょう。 実際に効果を発揮する唯一の制限は RLIMIT_CPU で、timeout+5\text{timeout}+5 秒に設定されています。これは壁時計時間ではなく CPU 時間を制限するもので、1 回のコンパイルが消費する CPU は約 9 秒にすぎないため、どの同時実行数でも制約になることはありません。暴走するマクロのような異常な入力から保護するためのものです。ただし、これは有用な判定材料になります。Soft:305 が観測されれば、300 秒のタイムアウト設定が実際にコンテナまで伝播していることが確認できます。

6.3 コンパイルタイムアウトは最も影響の大きい調整項目

ユーザーごとのフィールド features.compileTimeout のデフォルトは 180 秒です。CPU 制約のあらゆる構成において、これは安全マージンではなく容量の設定です。まだ正しく計算を続けているマシンが失敗と判定されてしまうからです。これを 300 秒に引き上げると(MongoDB の更新 1 回で済みます)、測定容量は最大 4.2 倍変化します(表 3)。上限は RequestParser.MAX_TIMEOUT によって強制される 600 秒で、これを超える値は暗黙のうちに切り詰められます。 表 3. コンパイルタイムアウトが測定容量に与える影響。 最後の 2 行は、結果のうち直感に反する半分であり、すべての構成を単一のタイムアウトで再測定した理由です。メモリ 制約の構成では、タイムアウトを長くすると容量が 減少 します。各コンパイルが常駐セットをより長く保持し、より多くのコンパイルが重なるからです。したがって、容量の数値は、それを測定したときのタイムアウトを明記しなければ無意味であり、1 つの表の中で両者を混在させることはできません。

7. 関連研究

7.1 ベンダーのガイダンス

Overleaf 自身のハードウェアに関するドキュメントは、本稿で定量化した定性的な事実を述べています。LaTeX はシングルスレッドであり、したがってシングルコア性能がコンパイル時間を支配し、「コアを増やしても、空いている CPU コアよりも多くの文書をコンパイルしようとしている場合にしか役に立たない」というものです [1]。続いて、本調査の動機となった線形のサイジング規則、すなわち 2 コア / 3 GiB をベースに、同時ユーザー 5〜10 人あたり 1 コアと 1 ギガバイトを加えるという規則を示しています。私たちの貢献は、これらの記述を測定された法則(式(1)と(2))に変え、線形の規則がどこで破綻するかを示したことです。その規則には、メモリの壁を超線形にする共有ページキャッシュの項も、結果を支配する 2 つのソフトウェアパラメーターの項もありません。

7.2 ビルドおよび CI の容量に関する研究

同時実行下でのビルドシステムの測定は、LaTeX 以外の分野では確立されています。LightSys は、Docker コンテナ内でコンパイルする従来の CI システムが、プルリクエストの到着率の上昇に伴って I/O 性能が劣化し、同時リクエスト 11 件前後でボトルネックが現れると報告しています [17]。TAOS-CI は、コンパイルが CI の壁時計時間を支配し、大規模プロジェクトではパイプライン全体の所要時間の 60〜67% を占めることを観測しています [18]。私たちのシステムは、決定的であることが判明した 1 つの点で異なります。LaTeX のコンパイルはインタラクティブなのです。2 倍時間がかかる CI ジョブは不便なだけですが、2 倍時間がかかるコンパイルは、プレビューペインを待っているユーザーに直接観測されます。これが、タイムアウトを失敗のしきい値ではなく容量のパラメーターとして扱う理由です。

7.3 コンテナのオーバーヘッド

近年の研究では、Docker コンテナの起動レイテンシをストレージ階層ごとに分解したり [19]、エッジにおけるコンテナの性能を特徴づけたりしています [20]。私たちの設定では、コンパイルごとのコンテナ起動は償却されます。9 秒のコンパイルに対して小さな定数であり、フィッティングした自由飛行時間 T1T_1 に吸収されます。重要なのは、リソース制限が ない というコンテナの性質(§6.2)であり、これがコンパイル単位のメモリ超過をホスト全体の障害に変えてしまいます。

7.4 信頼できない入力としての LaTeX

サンドボックスコンパイルが存在するのは、TeX がプログラミング言語であり、文書が信頼できない入力だからです [21, 22]。この設計上の選択こそが本調査を可能にしています(各コンパイルは、リソースの挙動を観測できる分離されたコンテナです)。そしてそれは同時に、メモリ制限の欠如を重大なものにしています。デプロイする運用者は、分離されていることを前提にしているからです。

7.5 研究対象としてのコンパイラー

TeX 自体は言語として十分に文書化されています [16] が、ビルドターゲット としての挙動が注目されるようになったのは最近のことです。Tan と Rigger [8] は、arXiv の大規模なソースコーパスを複数のエンジンとディストリビューションのバージョンでコンパイルし、エンジンの選択は代替不可能であることを発見しました。XeTeX と pdfTeX でバイト単位で同一の出力を生成する文書は、ほんの 1% 未満にすぎません。この結果は私たちの方法論に直接関係します。容量は文書 と エンジンの性質であるため、その両方を固定しないベンチマークは再現可能ではありません。そこで、全体を通じて 1 つの文書、1 つのエンジン、1 つのディストリビューション(texlive-full:2025.1)に固定し、すべての図のキャプションにエンジンを記載しています。また、これは私たちの数値の一般性を制限するものであり、はっきり述べておく価値があります。これらの数値は、抽象的な TeX ではなく、この文書における XeLaTeX の特性を示すものです。 LaTeX のビルド システム に関する取り組みは、主に実務者主導です。LaTeX3 プロジェクトの l3build [13] は回帰テストとパッケージングを標準化しており、独立したベンチマークではラッパーツールが比較されています。26 のビルドシステムの調査では、プリコンパイルされたプリアンブルが、通常の実行に対しておよそ 20%、latexmk に対して 40% の効果があることがわかっています [14]。これらは 単一の コンパイルを最適化するものです。私たちが測定する内容とは直交しており、組み合わせることができます。プリアンブルキャッシュは T1T_1 を短縮し、本稿のすべての容量の数値は T1T_1 に比例してスケールします。

7.6 コンパイラーではなくエディターにおける同時実行制御

Overleaf の共同編集の部分は、確立された一連の研究に基づいています。操作変換(Operational Transformation)は Ellis と Gibbs [9] に始まり、Jupiter システム [10] によって高レイテンシのクライアントでも実用的になりました。その設計は document-updater に見て取れます。操作を順序付けるサーバーと、クライアントが同期する文書ごとのバッファです。CRDT(Conflict-free Replicated Data Types) [11] は、中央のシーケンサーなしで同じ問題を解決します。この区別こそが、§4.3 のトポロジーを機能させているものです。保留中の更新バッファはインスタンスのメモリではなく共有の Redis に存在するため、どのレプリカにルーティングされたコンパイルでも最新のキーストロークを観測でき、コンパイルのアフィニティは正確性のためではなくキャッシュの局所性のために選択できます。

7.7 容量モデル

アムダールの法則 [24] は並列化による高速化の上限を与え、リトルの法則 [23] は占有率を到着率とサービス時間に関連付けます。どちらも上で使用しています。Gunther の普遍的スケーラビリティ法則 [12] は、前者にコヒーレンシ遅延による逆行項を加えて拡張したもので、スループットがピークに達した後に低下すると予測します。私たちのシステムは、N=1024N=1024 までその逆行領域を示さ ない ことに注目してください。スループットは飽和しレイテンシは増加しますが、崩壊するものは何もありません。その理由は幸運ではなく構造的なものです。コンパイルは一貫性を保つべき状態を共有しないため、この法則が加える項はほぼゼロであり、§4.3 の受け入れのプラトーが、競合が問題になる前にそれを抑えています。

8. 運用者への推奨事項

1

ハードウェアを購入する前に 2 つのソフトウェアパラメーターを修正する

どちらも無料で、どちらも私たちが測定したどの単一のハードウェアアップグレードよりも価値があります。features.compileTimeout をユーザーが実際に許容できる値に引き上げてください(CLSI が受け付ける最大値は 600 秒です)。また、同時コンパイルが 65 件を超えると予想される場合は、派生イメージで compileConcurrencyLimit を引き上げるか、スケールアウトしてください。どちらも行わないということは、ソフトウェアが使用を拒否するコアにお金を払うことを意味します。
2

1 台のマシンは上限ではなくニーでサイジングする

大型ランナーのスイープ(§4.3)は、日常的に混同されている 2 つの数値を区別します。上限(すべての PDF を返せる最大の同時実行数)は 64 コアのサーバーで少なくとも 1024 であり、私たちは一度もそこに到達しませんでした。ニー(テールレイテンシの緩やかな増加が止まり、倍増し始める点)は 512 にあり、その手前で最後に快適な動作点は 256 です。N=256N=256 から N=512N=512 の間で、p95p_{95} の待ち時間は 2 分から 6 分近くになり、512 から 1024 の間では 8 分に達します。上限に合わせてサイジングする運用者は、技術的には動作するものの誰も使いたがらないシステムを提供することになります。したがって、このマシンとこの文書における推奨動作点は 同時コンパイル 256 件 です。これは物理コア数の 4×4\times、スレッド数の 2×2\times であり、p95p_{95} を 120 秒付近に保ちます。compileConcurrencyLimit は高いままにせず、この値に設定することをお勧めします。1024 件のコンパイルを一度に受け入れると全員が 8 分待つことになりますが、256 件を受け入れて残りをキューに入れれば、ほとんどのユーザーは 2 分で処理されます。キューイングは遅れて到着した人の体験を劣化させ、競合は全員の体験を劣化させます。
3

これらの数値は最悪ケースとして扱う

表 2 のすべてのレベルは、同時に発行されたコールドコンパイルです。本番環境ではどちらの条件も当てはまりません。同じ文書のウォームコンパイルはコールドの 28.3 秒に対して 8.6 秒で、3.33.3 倍の差があり、実際のユーザーが同じ秒にボタンを押すことはありません。したがって、2 分ごとに再コンパイルし、典型的なキャッシュヒット率を持つ定常状態のユーザー集団であれば、同時実行数の数値だけから想定されるよりもかなり多くの執筆者を支えられます。動作点 256 であれば、アクティブな執筆者 1000 人以上のオーダーです。同時実行数の数値は瞬間的なバーストの上限であり、席数ではありません。
4

まずレイテンシの予算を決め、そこからサイズを読み取る

式(1)は直接逆算できます。CC コア、クロック ff で目標待ち時間を TT とすると、収まる同時実行数は N≤C fT/kN \le C\,fT/k で、この文書では k≈28GHz⋅sk\approx28 GHz·s です。3 GHz の 8 コアで 60 秒の予算なら N≤51N\le51 となり、120 秒の予算ならその 2 倍になります。容量と予算を併記することが、どちらかを誠実に述べる唯一の方法です。
5

まずメモリ、次にコアを購入し、どちらの壁にいるかを確認する

32 GiB 未満では、コアを追加してもほとんど効果がないことが測定されました。診断は簡単です。ゲストのメモリが不足した状態で HTTP 502 として失敗が現れるならメモリを追加し、メモリに余裕がある状態で timedout として現れるならコアを追加するかタイムアウトを引き上げてください。運用者は、私たちが構成の分類に使用したのと同じ失敗の特徴からこれを読み取れます。
6

体験にはクロック、ユーザー数にはコアを優先する

T∝1/fT\propto 1/f は曲率なしで成り立つため(1.0〜5.5 GHz にわたって 2%)、クロックが速いほど、すべてのユーザーのすべてのコンパイルが速くなります。コアを増やしても、個々のコンパイルは速くなりません。より多くの同時コンパイルを受け入れられるようになるだけです。「コンパイルが遅い」という不満があるデプロイではクロックを購入し、「締め切り時にコンパイルが失敗する」という不満があるデプロイではメモリとコアを購入すべきです。
7

上限を超えたらスケールアップではなくスケールアウトする

同時コンパイル 65 件を超える場合にサポートされている方法は水平スケーリングです(図 2c と図 10、詳細は §9)。Cookie によるセッションアフィニティを備えたロードバランサーの背後に複数のアプリケーションインスタンスを配置し、中央の MongoDB、Redis、S3 互換ストレージを共有させ、git-bridge は単一のままにします。これによりインスタンスごとの上限がインスタンス数倍になります。これはまさに SaaS デプロイが自身の容量を実現している方法です。
8

コンパイルごとの分離に頼らない

Docker ランナーのメモリ制限が修正されるまで(§6.2)、1 つの異常な文書が、単独で強制終了されるのではなくホストを枯渇させる可能性があります。その保証が必要な運用者は、修正を待つのではなく自分で課すべきです。私たちが大型ホストで使用した仕組みは、ハードな上限を持つ systemd スライスで、Docker デーモンがこれを参照するよう設定することで、デーモンが作成するすべてのコンテナがその中で計上されるようにしています:
見落とすと午後を丸ごと失う詳細が 1 つあります。docker-capped.slice という名前のスライスは docker.slice の隣ではなく、その 中 に配置されます。ハイフンは名前の一部ではなく階層の区切り文字だからです。効果がないように見える上限は、たいていコンテナが実際に存在する場所から 1 階層ずれた場所に適用されています。設定ファイルを信頼するのではなく、実行後に memory.max_usage_in_bytes からピーク値を読み取って確認してください。私たちのホストでは、同時コンパイル 1024 件でもコンパイルの cgroup が上限の 5 分の 1 を超えることはありませんでした。これ自体が、制約要因がメモリではなくデーモンであったことの証拠です。
図 10. 水平スケーリングされたデプロイのリファレンストポロジー。私たちが検証した構成に基づいて描いたものです。アプリケーションレプリカは互換性があり、永続的なものを何も保持しないため、自由に追加・削除できます。そうではないコンポーネントが 3 つあります。Redis は、その文書バッファによって、どのレプリカにルーティングされたコンパイルでも最新のキーストロークを確認できるようにしています。オブジェクトストアは、レプリカが 2 つ以上になるとオプションではなく必須になります。そして git-bridge は、レプリケーションの経路を持たずにリポジトリをローカルディスクに保持するため、指定した 1 つのレプリカの隣で単一インスタンスとして実行する必要があります。

9. 複数マシン構成のリファレンスデプロイ

ここまでの内容はすべて 1 台のマシンを測定したものです。このセクションでは、構築できるだけの詳細さで分散構成を説明します。そして、運用者が実際に直面する問いは どのように ではなく そうすべきかどうか であるため、まずそれが手間に見合うようになる時点を述べます。

9.1 分散構成が正当化される場合

運用にとって重要なあらゆる点で、1 台のマシンの方が安価です。障害ドメインは 1 つ、一貫性を保つべき共有状態はなく、間違える可能性のあるルーティングもありません。私たちのデータは、1 台構成から離れるべき時期について 3 つのしきい値を示しています。

9.1.1 同時コンパイル 65 件未満では、分散しない

インスタンスごとの上限は、ハードウェアではなくソフトウェアの定数です(§6.1)。提供負荷がそれに近づくまでは、2 台目のマシンは障害モードを増やすだけで何ももたらしません。64 コアのホストが同時コンパイル 256 件を完全に成功させたのは、compileConcurrencyLimit を引き上げた後のことでした。この 1 つの値をまだ変更していない運用者は、ハードウェアに制約されているわけではなく、ハードウェアを買いに行くべきではありません。

9.1.2 65 件からおよそ 500 件の間では、まずスケールアップする

垂直スケーリングは私たちの測定範囲全体で線形のままで、逆行領域に入ることはありませんでした。1 台の大型ホストが同時コールドコンパイル 1024 件を成功率 100% で処理しました(§4.3)。レイテンシのニーは 512 で現れ、それより前には現れませんでした。この範囲内では、大きなマシン 1 台の方が小さなマシン数台よりも厳密に単純であり、§4.4 のとおり、より高速なマシンは、より多くのユーザーを受け入れるだけでなく、すべてのユーザーの体験を改善します。

9.1.3 スループットではなく可用性のために分散する

上限未満で複数のアプリケーションレプリカを実行する誠実な理由は、1 台のマシンは電源 1 つ、カーネル 1 つ、アップグレードの時間枠 1 つだということです。これは正当な理由であり、私たちもそう答えるでしょう。ただ、それは容量の議論ではないというだけです。両者を混同すると、運用者はメモリが必要なときにレプリカを購入してしまいます。

9.2 階層とそのサイジング

図 10 にトポロジーを示します。4 つの階層があり、それぞれ異なる量に応じてスケールします。それこそが、階層を分ける意味のすべてです。

9.2.1 エッジ

ロードバランサーを 1 台、可用性のためなら 2 台。TLS を終端するだけで、コストの高い処理は行いません。コンパイル数ではなく接続数に応じてスケールし、ここで調査した負荷であれば小さなインスタンスで十分です。重要なのはサイズではなく設定です(§9.3)。

9.2.2 アプリケーションレプリカ

これらがコンパイル負荷を担い、同時実行数に応じてスケールする唯一の階層です。§8 の規則(コアよりもメモリ、次にクロック)に従って各レプリカをサイジングし、ピーク時の同時実行数をレプリカごとの上限で割った値をカバーするようにレプリカ数を設定します。レプリカは永続的なものを何も保持しません。ローカルディスクにはコンパイルのスクラッチと出力キャッシュが置かれますが、どちらも再構築可能です。これが、レプリカを自由に追加・削除しても安全な理由です。ただし、前提にせず検証する価値があります。filestore のパスを 1 つ誤設定するだけで、この階層が暗黙のうちにステートフルなものに変わってしまうからです。

9.2.3 状態

Redis、MongoDB、S3 互換のオブジェクトストアを、それぞれ別のホストに配置します。荷重を支えているのは Redis であり、それは最もわかりにくい部分でもあります。Redis はセッションストアとライブの文書バッファを保持しており、これによって、どのレプリカにルーティングされたコンパイルでも、別のレプリカに対して入力されたキーストロークを観測できます。Redis をキャッシュとして扱い、エビクションを前提にサイジングする運用者は、古い文書をコンパイルしてしまう状況を生み出し、それは診断が極めて困難です。何も失敗せず、出力が単に間違っているだけだからです。MongoDB はコンパイル頻度ではなくプロジェクト数に応じてスケールします。オブジェクトストアは、レプリカが 1 つならオプションで、それを超えると必須です。

9.2.4 シングルトン

git-bridge はリポジトリをローカルディスクに保持し、ローカルのインデックスを管理し、レプリケーションの経路を持ちません。正確に 1 インスタンスとして実行し、指定した 1 つのレプリカの隣に固定する必要があります。これが、デプロイを完全にはステートレスでないものにしているコンポーネントです。そのホストは適切に計画してください。バックアップが必要なのはそのディスクです。 表 4. リファレンスの階層。同時実行数に応じてスケールするのはアプリケーション階層のみで、そのサイジングは §8 の主題です。

9.3 ルーティングは間違えやすい部分

3 種類のリクエストが 3 つの異なる場所に到達する必要があり、デフォルトの単一ルール構成では、そのうち最大でも 2 つしか満たせません。 /project/ 配下のコンパイルトラフィックは、プロジェクト識別子に対するコンシステントハッシュで分散し、プロジェクトのコンパイルキャッシュが 1 つのレプリカに留まるようにすべきです。私たちは HAProxy の balance hash path,field(3,/) を hash-type consistent および hash-balance-factor 150 とともに使用しています。この選択はスケールアウト時に重要になります。Cookie によるアフィニティでは、既存のセッションは元のレプリカに無期限に固定されたままになり、新しく追加されたレプリカは新規ユーザーしか受け取りません。そのため、運用者が購入したばかりのマシンは、購入の動機となった負荷をまったく吸収しません。私たちの構成では、スケールアウト時にコンシステントハッシュによってプロジェクトの 35% が再分散されましたが、Cookie では 0% でした。 セッションのトラフィックは異なります。WebSocket のアップグレードが失敗して socket.io が XHR ポーリングにフォールバックした場合、1 つのセッションの連続するポーリングは 1 つのレプリカに到達する必要がありますが、パスにはハッシュするためのプロジェクト識別子がありません。このトラフィックには、Cookie によるアフィニティを備えた別のバックエンドが必要です。この分割は設計しましたがデプロイはしていません。実現したと主張するのではなく、不足点として明記しておきます。 最後に、/git/ は git-bridge が隣で動作しているレプリカに到達する必要があります。git-bridge に直接ではなくそのレプリカにルーティングするのは、ブリッジがアプリケーションの OAuth エンドポイントに対してコールバックを認証し、アプリケーションを通じて blob の URL を解決するためです。レプリカを迂回すると、何も改善されず認証が壊れるだけです。

9.4 スケールインにはドレインのバッファが必要

レプリカの削除は追加と対称ではありません。処理中のコンパイルは失われ、ユーザーは自分が原因ではない失敗を目にします。実用的な手順は、まず新しいトラフィックを止め、待ってから、初めて終了させることです。私たちはこれを、バランサーがバックエンドをドレイン中としてマークしている間、Pod を設定可能な間隔だけ保持する pre-stop フックとして実装しました。テストでは数分で済むほど短く、本番環境ではセッションが自然に終了するのに十分な長さ、つまり数秒ではなく数時間です。この間隔こそが、弾力性がユーザーに気づかれないものになるか、苛立たしいものになるかを決める調整項目です。 設計ではなく測定によって見つけたもう 1 つの制約があります。このワークロードでは、CPU に基づくオートスケーリングは機能しません。アプリケーション Pod 自体の使用率は、ノード全体の 3997 m コアに対して 22 m コアでした。コンパイル作業は、Pod が計上しない兄弟コンテナ内で行われるからです。この階層のスケーリングに使用するシグナルは、Pod の CPU ではなく、実行中のコンパイルコンテナを数える必要があります。

10. Overleaf 以外への示唆

§4.2 と §4.3 には、Overleaf のコードに固有のものは何もありません。測定された法則は、あらゆるホスト型 LaTeX サービスに共通する 3 つの性質から導かれます。作業単位がシングルスレッドのプロセスであること、それがコンテナ内に分離されていること、そしてそのワーキングセットがページキャッシュに保持されなければならない大きな読み取り専用ツリーであることです。そうしたサービスを構築するすべての人に、3 つの帰結がそのまま当てはまります。

10.1 まずメモリ、次にコアを用意する

このマトリクスで最も強い結果は否定的なものです。16 GiB 未満ではコア数はほぼ無関係で、48 GiB になって初めて 4、8、16 vCPU の構成に差が出ます(143、268、331)。従来の規則を「ユーザー 5 人ごとにコアを 1 つ追加する」と読む運用者は、間違ったリソースを購入することになります。そのメカニズムはディストリビューションのツリーに対する共有ページキャッシュであり、特定のフロントエンドではなく TeX Live のサイズに由来する性質です。

10.2 受け入れ速度はリソースであり、たいてい忘れられている

N=1024N=1024 では、すべてのリクエストが一度に到着したにもかかわらず、サーバーが同時に保持した稼働中のサンドボックスは 205 を超えませんでした(図 7b)。制約となったのはコンパイルではなくコンテナの作成でした。これは、コンテナの起動コストをイメージサイズではなくランタイムのオーバーヘッドに帰する測定研究と一致しています [19, 20]。CPU とメモリだけをサイジングするサービスは、自身のバースト時の挙動が、一度も測定したことのない量によって支配されていることに気づくでしょう。その実践的な形が §8 の推奨事項です。受け入れを意図的に制限してください。自分で選んだキューは、後から発見するキューよりも優れています。

10.3 コンパイルより長く存続するサンドボックスはモデルを無効にする

ここでのすべての容量の数値は、コンテナが作成され、1 回コンパイルを行い、終了することを前提としています。寿命は数十秒で、稼働率がほぼ 1 になるのは実行中だけです。最近の 2 つの設計パターンはこの前提を崩し、しかも同じ形で崩します。 1 つ目は、ユーザーごとの永続的なサンドボックスです。各ユーザーに固定のプライベート環境を割り当てると、統計的に多重化されたプールが予約の集合に変わります。タイムシェアリングによって 64 コアで同時コンパイル 256 件を処理できるサービスも、各ユーザーに専用コアを 4 つ与えると 16 ユーザーしか処理できなくなり、同じハードウェアで 1 桁少なくなります。私たちのデータはその選択に反対するのではなく、そのコストを定量化するものです。予約は予測可能性をもたらし、その交換レートは、私たちが推奨する動作点でおよそ 16×16\times です。 2 つ目の、より新しいパターンは、コンパイラーとサンドボックスを共有する AI エージェントです。エージェント支援型の執筆プラットフォームでは、XeLaTeX を実行するのと同じコンテナで長時間実行されるコーディングエージェントもホストされることがあり、コンテナはバースト的ではなく継続的に占有されます。実務者たちは、そうしたデプロイについてモデルが予測するとおりの症状、つまり控えめなユーザー数でも持続的に動作が重くなるという症状を報告しています [15]。この相互作用は正確に述べておく価値があります。単に「負荷が増える」だけではないからです。私たちの発見のうち 3 つが複合的に作用します。占有がバースト的でなくなるため、§4.2 のタイムシェアリングの法則が、現在コンパイル中の一部ではなくユーザー全体に一度に適用されます。§4.1 の超線形のメモリリターンをもたらしているページキャッシュは、今やエージェント自身のワーキングセットと共有され、TeX にとってウォームな状態ではなくなります。そして、§6.2 のコンテナのメモリ制限の欠如ははるかに危険になります。終了しないコンテナは決してメモリを返却しないからです。 私たちはそのようなプラットフォームを測定しておらず、特定の製品について何も主張しません。言えるのは、私たちの数値が設計に対して何を意味するかです。各ユーザーに長期間存続するマルチコアのサンドボックスを与えるアーキテクチャは、ここで報告した同時実行数の数値ではなく予約システムとしてサイジングすべきであり、期待できる容量は、表 1 のどの値よりも、コア数をユーザーあたりのコア数で割った値に近くなります。

11. 妥当性への脅威

11.1 単一の文書

すべての測定では 63 ページの XeLaTeX 文書を 1 つだけ使用しています。他の文書では容量の絶対値は異なりますが、比率であるスケーリング則は変わらないはずです。常駐セットが大幅に大きい文書では、メモリの壁の位置は移動しますが、その超線形の性質は変わりません。

11.2 仮想化されたホスト

ゲストは 1 台の物理マシン上の KVM で動作するため、絶対値には仮想化のオーバーヘッドが含まれ、ゲストはホストのページキャッシュと NVMe デバイスを共有します。ホストのメモリ圧迫によって、同一の同時実行数でゲスト内のロードアベレージが 3×3\times 以上に膨らむことを観測した後、無関係なゲストをシャットダウンすることで最大の交絡要因を軽減しました。

11.3 同時到着

すべてのコンパイルは 1 つの瞬間に発行されます。これは最悪のケースです。実際のユーザーは確率過程として到着するため、私たちの数値でサイジングしたデプロイには不足ではなく余裕があります。ただし、提出締め切り直前のピークは、ポアソン過程よりも私たちのモデルに近いものです。

11.4 境界的な構成

2 GiB では、システムが崩壊寸前であるため、同じ構成を繰り返し実行するとコンパイル 1 件分の差が生じることがあります。保守的な値を報告し、その領域での ±1\pm 1 の差から結論は導きません。

12. 入手方法

テスト対象のシステム、デプロイツール、およびその派生元であるアップストリームプロジェクトはすべて公開されています: 引用したすべてのソースの場所は、Ayakaleaf Pro v6.2.2 に対するリポジトリ相対パスと行番号で示しています。また、日付を記載した 2 つのアップストリームのコミット(9a519f0d3d、5d472e9b38)は、Overleaf の履歴からたどることができます。

13. 貢献

Musicminion は調査を設計し、テストベッドを提供・運用し、調査の方向性を指揮し、ここで報告したすべての測定を検証しました。Claude Opus 5(Anthropic)はベンチマークハーネスを構築・運用し、デプロイを自動化し、ソースコードの考古学的調査を行い、図を作成し、原稿の草稿を書きました。両著者が最終稿をレビューしました。汚染されたものとして報告している実行(§4.3 の 1021 セッションのスイープと §4.1 の異常な N=256N=256 のレベル)については、欠陥はレビュー中に発見され、黙って除外するのではなく、公開前に実行をやり直しました。 なお、ACM、IEEE、ICMJE の著者資格に関する方針は現在、著者資格を作品に対して責任を負うことができる当事者に限定しており、第 2 著者の貢献は著者名としてではなく開示事項として記録することを求めるでしょう。いずれの慣例の下でも記録が正確であるよう、ここで役割分担を明示しています。

14. 結論

セルフホストの Overleaf の容量計画は、1 つのリソースをスケールさせるという問題ではありません。3 つの発見が、その進め方を変えるはずです。 第一に、ゲストメモリが 32 GiB 未満では、コア数はほとんど重要ではありません。16 GiB では、4、8、16 vCPU のゲストの容量の差は 8% 未満です。TeX Live ツリーに対する共有ページキャッシュを通じて、メモリが限界を決めます。コアが重要になり始めるのは、メモリが十分にある場合だけです。 第二に、2 つのソフトウェアパラメーターがハードウェアを上回ります。CLSI にハードコードされた 65 件のコンパイル上限を解除し、デフォルトの 180 秒のコンパイルタイムアウトを引き上げたことで、8 vCPU / 48 GiB のゲストは同時コンパイル 64 件から 268 件になりました。追加のハードウェアなしで 4.2 倍です。どちらも設定ドキュメントからは発見できず、一方はそもそも設定できません。 第三に、「このマシンは何人の同時ユーザーをサポートできるか」という問いは、条件が不十分です。このシステムにおける同時実行は純粋なタイムシェアリングであり、容量はタイムアウトが許容する範囲で決まります。誠実な回答は両方を述べるものです。このマシンは、ユーザーが TT 秒待てるなら NN 件の同時コンパイルを処理できる。ここで NN と TT は式(1)で関係づけられます。 また、潜在的な欠陥も報告します。Docker ランナーのコンテナごとのメモリ制限は、2018 年以来、その大きさと適用箇所の両面で機能していません。その実際の影響は、小規模なデプロイでメモリが枯渇すると、原因となった 1 件のコンパイルではなくサービス全体がダウンすることです。

参考文献

[1] Overleaf. Hardware requirements, On-premises documentation. https://docs.overleaf.com/on-premises/getting-started/requirements/hardware-requirements [2] Overleaf. Horizontal scaling, On-premises documentation. https://docs.overleaf.com/on-premises/maintenance/horizontal-scaling [3] Overleaf. Microservices, On-premises documentation. https://docs.overleaf.com/on-premises/getting-started/microservices [4] Overleaf. Source repository. 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. Preprint. [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. Preprint. [19] S. Khan. Decomposing Docker Container Startup Performance: A Three-Tier Measurement Study on Heterogeneous Infrastructure. arXiv:2602.15214, 2026. Preprint. [20] R. Gupta and K. Nahrstedt. Performance Characterization of Containers in Edge Computing. arXiv:2505.02082, 2025. Preprint. [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. Preprint. [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.
最終更新日 2026年10月5日