Skip to main content

Overleaf-Benchmark.pdf

Tóm tắt

Các bản triển khai Overleaf tự lưu trữ thường được định cỡ theo một quy tắc kinh nghiệm duy nhất: một lõi CPU và một gigabyte bộ nhớ cho mỗi năm đến mười người dùng đồng thời. Chúng tôi chỉ ra rằng quy tắc này không chỉ thiếu chính xác mà còn sai về mặt cấu trúc, vì nó giả định rằng chỉ một chiều tài nguyên duy nhất quyết định năng lực, trong khi thực tế có hai bức tường độc lập, và vì hai tham số phần mềm — không phải phần cứng — chi phối kết quả với hệ số lên tới bốn lần. Chúng tôi đo đạc một bản triển khai Ayakaleaf Pro v6.2.2 nguyên bản với biên dịch trong sandbox (TeX Live 2025) trên 21 cấu hình CPU/bộ nhớ bên trong các máy khách QEMU/KVM có lõi máy chủ được khóa xung nhịp ở 3.0 GHz. Khối lượng công việc là một luận văn XeLaTeX thực tế dài 63 trang, được biên dịch đồng thời bởi tới vài trăm tài khoản người dùng riêng biệt. Chúng tôi nhận thấy rằng dưới 32 GiB bộ nhớ máy khách, số lõi gần như không liên quan — ở 16 GiB, năng lực đo được của các máy khách 4, 8 và 16 vCPU chênh lệch nhau dưới 8% — và năng lực thay vào đó bị chi phối bởi một bức tường bộ nhớ siêu tuyến tính phát sinh từ page cache dùng chung trên cây TeX Live. Để kiểm tra liệu các quy luật này có còn đúng khi quy mô thay đổi một bậc độ lớn, chúng tôi lặp lại phép quét trên một máy chủ duy nhất 64 lõi, 995 GiB. Máy chủ này duy trì được 1024 lần biên dịch nguội (cold) đồng thời với tỷ lệ thành công 100% — gấp tám lần số luồng của nó — và chúng tôi chưa bao giờ chạm tới giới hạn của nó. Con số hữu ích không phải là giới hạn đó mà là điểm gãy (knee) bên dưới nó: độ trễ đuôi (tail latency) tăng 20–40% mỗi lần tải tăng gấp đôi cho tới N=256N=256, rồi tăng 190% tại N=512N=512. Do đó, năng lực được báo cáo dưới dạng “mức đồng thời lớn nhất không gây lỗi” sẽ phóng đại điểm vận hành hữu dụng lên bốn lần. Trên máy đó, bộ nhớ không bao giờ là tài nguyên giới hạn; giới hạn là CPU cùng với tốc độ mà container daemon có thể tiếp nhận các sandbox mới, vốn bão hòa ở gần 200 bất kể có bao nhiêu lần biên dịch được yêu cầu. Chúng tôi còn xác định hai hiệu ứng ở cấp độ triển khai mà việc lập kế hoạch năng lực không nhìn thấy được. Thứ nhất, CLSI áp đặt một giới hạn cứng được mã hóa sẵn là 65 lần biên dịch đồng thời mà không được công khai qua bất kỳ biến môi trường nào; vượt quá giới hạn này, người dùng nhận HTTP 503 ngay lập tức thay vì được đưa vào hàng đợi. Thứ hai, giới hạn bộ nhớ cho mỗi container trong Docker runner đã không có hiệu lực kể từ khi được giới thiệu năm 2018, cả về độ lớn lẫn vị trí đặt, nên một sự kiện hết bộ nhớ sẽ đánh sập toàn bộ máy chủ thay vì chỉ một lần biên dịch. Việc gỡ bỏ giới hạn đồng thời và tăng thời gian chờ biên dịch mặc định từ 180 s lên 300 s làm tăng năng lực đo được của một máy khách 8 vCPU / 48 GiB từ 64 lên 268 lần biên dịch đồng thời — gấp 4.2 lần mà không tốn chi phí phần cứng. Cuối cùng, chúng tôi chỉ ra rằng tính đồng thời trong hệ thống này không mang lại gì ngoài chia sẻ thời gian (time-sharing), và khối lượng công việc chỉ bị giới hạn bởi xung nhịp. Một quy luật suy giảm được khớp T(N)=T1max⁡(1,N/C)bT(N)=T_1\max(1,N/C)^{b} cho b=0.914b=0.914, gần với mức chậm lại tỷ lệ thuận hoàn hảo, và một phép quét xung nhịp trên toàn dải 1.0–5.5 GHz của máy gom ba mươi phép đo về T=(k/f)max⁡(1,N/C)T=(k/f)\max(1,N/C) với k=27.9GHz⋅sk=27.9 GHz·s và độ phân tán phần dư 5.1%. Xung nhịp gấp 5.5 lần mang lại tốc độ nhanh gấp 5.5 lần mà không có hiện tượng lợi ích giảm dần, và đó chính là ý nghĩa của việc xung nhịp và số lõi mang lại những thứ khác nhau: xung nhịp làm cho lần biên dịch của mọi người dùng nhanh hơn, còn số lõi chỉ cho phép tiếp nhận thêm người dùng.

1. Giới thiệu

Overleaf là trình soạn thảo LaTeX cộng tác thống trị, và bản phân phối on-premises của nó được triển khai rộng rãi bởi các trường đại học và nhóm nghiên cứu không thể gửi các bản thảo chưa công bố lên đám mây của bên thứ ba. Định cỡ một bản triển khai như vậy là một câu hỏi thực tiễn lặp đi lặp lại: với một ngân sách phần cứng cố định, thực sự có bao nhiêu người có thể nhấn “Recompile” cùng một lúc? Hướng dẫn chính thức là một quy tắc tuyến tính — khoảng một lõi và một gigabyte cho mỗi năm đến mười người dùng đồng thời — vốn giả định rằng năng lực tăng trơn tru và đồng thời theo cả hai tài nguyên. Các phép đo của chúng tôi mâu thuẫn với điều này theo ba cách.

1.1 Năng lực bị chi phối bởi hai bức tường độc lập, không phải một

Một cấu hình thất bại hoặc vì bộ nhớ cạn kiệt, khi đó chính stack Overleaf sẽ chết và trả về HTTP 502, hoặc vì các lần biên dịch vượt quá thời gian chờ phía máy chủ, khi đó CLSI báo timedout trong khi hàng gigabyte bộ nhớ vẫn chưa được sử dụng. Hai chế độ này có hành vi mở rộng hoàn toàn khác nhau và cách khắc phục khác nhau. Thêm lõi vào một cấu hình bị giới hạn bởi bộ nhớ không chỉ kém hiệu quả mà đôi khi còn phản tác dụng: chúng tôi đo được các cấu hình mà việc tăng số lõi lại làm giảm năng lực, vì nhiều lõi hơn khiến các lần biên dịch đồng thời tiến triển đồng bộ, nên nhu cầu bộ nhớ đỉnh của chúng trùng nhau thay vì xen kẽ.

1.2 Tham số phần mềm lấn át phần cứng

Thời gian chờ biên dịch là một trường theo từng người dùng trong MongoDB với giá trị mặc định 180 s, âm thầm giới hạn các cấu hình bị giới hạn bởi CPU. Tăng nó lên 300 s nhân năng lực đo được lên tới 4.2 lần trên cùng phần cứng. Độc lập với điều đó, CLSI từ chối hơn 65 lần biên dịch đồng thời bởi một hằng số được mã hóa cứng. Bất kỳ nghiên cứu năng lực nào — và bất kỳ bản triển khai nào — không tính đến cả hai yếu tố này thì đang đo phần mềm chứ không phải máy.

1.3 Đồng thời là chia sẻ thời gian, không phải song song

Vì một lần biên dịch LaTeX là đơn luồng, việc phục vụ NN người dùng đồng thời trên CC lõi không làm hệ thống hoàn thành sớm hơn; nó khiến mọi người dùng phải chờ lâu hơn theo tỷ lệ. Do đó, câu hỏi “hỗ trợ bao nhiêu người dùng đồng thời” là đặt sai cho đến khi người ta xác định được người dùng sẵn sàng chờ bao lâu. Chúng tôi làm rõ sự phụ thuộc này và định lượng nó.

1.4 Đóng góp

  • Một ma trận năng lực trên 21 cấu hình CPU/bộ nhớ được đo trong điều kiện khóa xung nhịp và được xác minh lặp lại, với ràng buộc giới hạn được xác định cho từng cấu hình dựa trên dấu hiệu thất bại của nó.
  • Hai mô hình được khớp: một mô hình năng lực tách biệt bức tường bộ nhớ siêu tuyến tính với trần CPU, và một mô hình độ trễ xác lập hành vi chia sẻ thời gian thuần túy.
  • Xác định và xác nhận bằng thực nghiệm hai vấn đề triển khai trong hệ thống đã triển khai, bao gồm một giới hạn bộ nhớ container không hoạt động từ năm 2018.
  • Định lượng sự đánh đổi giữa thời gian chờ biên dịch và năng lực, điều mà chúng tôi cho rằng phải được nêu kèm với bất kỳ con số đồng thời nào.

2. Bối cảnh

2.1 Đường đi của quá trình biên dịch

Một yêu cầu biên dịch Overleaf đi qua web →\rightarrow clsi →\rightarrow một container biên dịch. Trong một bản triển khai sandboxed-compiles (SIBLING_CONTAINERS_ENABLED=true), CLSI không chạy latexmk trong cùng tiến trình; nó yêu cầu Docker daemon của máy chủ, có thể truy cập qua một socket được bind-mount, khởi động một container mới từ image TeX Live với thư mục dự án được bind-mount tại /compile. Do đó, một lần biên dịch là một container ngắn hạn chạy một tiến trình latexmk. Ba hệ quả sau đó, và cả ba đều định hình các phép đo trong bài viết này. Thứ nhất, đơn vị công việc là một tiến trình đơn luồng: XeLaTeX không song song hóa. Thứ hai, sự cô lập tài nguyên cho mỗi lần biên dịch là bất cứ thứ gì Docker runner yêu cầu — chúng tôi chỉ ra ở §6.2 rằng thực tế nó không yêu cầu gì cả. Thứ ba, tập làm việc (working set) bị chi phối không phải bởi tài liệu mà bởi cây TeX Live, một kho dữ liệu chỉ đọc khoảng 32 GiB mà mọi lần biên dịch đồng thời đều đọc từ đó và do đó chia sẻ qua page cache của máy chủ. Sự chia sẻ này là nguồn gốc của việc mở rộng bộ nhớ siêu tuyến tính mà chúng tôi quan sát được.

2.2 Bật sandboxed compiles

Overleaf phiên bản cộng đồng chạy latexmk ngay bên trong container ứng dụng. Ayakaleaf Pro, giống như Overleaf Server Pro, có thể chạy mỗi lần biên dịch trong một container anh em (sibling) — một container được ứng dụng khởi động trên Docker daemon của máy chủ thay vì lồng bên trong container ứng dụng. Hai thiết lập của toolkit bật tính năng này:
Toolkit bind-mount Docker socket của máy chủ vào container ứng dụng và chuyển các thiết lập này thành môi trường mà CLSI đọc: SANDBOXED_COMPILES=true, SANDBOXED_COMPILES_SIBLING_CONTAINERS=true và SANDBOXED_COMPILES_HOST_DIR, trong đó biến cuối cùng là đường dẫn trên máy chủ của thư mục biên dịch. Đường dẫn đó quan trọng: vì daemon khởi động container biên dịch là của máy chủ, bind mount được cung cấp cho nó phải phân giải được trong namespace của máy chủ, chứ không phải của container ứng dụng. Tệp config/env.sh của Server-Pro còn buộc TEXLIVE_IMAGE_USER=www-data trong chế độ này để các tệp do container biên dịch ghi ra có chủ sở hữu nhất quán. Việc xác minh rất trực tiếp: trong khi biên dịch, máy chủ hiển thị một container tên project-{projectId}-{userId}-{hash} chạy latexmk từ image TeX Live và thoát với mã 0. Đây là đơn vị mà chúng tôi đo số lượng xuyên suốt bài viết, và việc nó hoàn toàn không có giới hạn tài nguyên được chúng tôi báo cáo ở §6.2. Các container anh em giúp phép đo trở nên rõ ràng — mỗi lần biên dịch là một thực thể hệ điều hành có thể quan sát và được lập lịch độc lập — nhưng chúng cũng có nghĩa là kernel của máy khách, chứ không phải Overleaf, phân xử CPU và bộ nhớ giữa các lần biên dịch. Do đó, mọi quy luật mở rộng trong bài viết này là một thuộc tính của bộ lập lịch Linux áp dụng cho NN tiến trình đơn luồng, và đó là lý do chúng đều đặn đến vậy.
Hình 1. Một yêu cầu biên dịch, được theo dõi qua các microservice của phiên bản cộng đồng. Sự phân tách ở các bước này quan trọng đối với năng lực: văn bản tài liệu được sao chép vào thân yêu cầu, trong khi các tài nguyên nhị phân được truyền theo tham chiếu và được clsi kéo về. Không cái nào chiếm ưu thế — chi phí biên dịch của một dự án được quyết định bởi cây TeX Live 32 GiB mà mọi lần biên dịch đồng thời đều đọc qua page cache dùng chung.
Hình 2. Ba kiến trúc triển khai và vị trí của giới hạn biên dịch theo từng instance trong mỗi kiến trúc; các bảng xếp chồng biểu thị sự nhân bản. Hằng số 65 lần biên dịch bảo vệ một CLSI, vì vậy hạ tầng SaaS nhân nó theo số instance và zone (a), và việc mở rộng theo chiều ngang được Server Pro và Ayakaleaf Pro hỗ trợ nhân nó theo số instance (c) — với cái giá là MongoDB, Redis và lưu trữ tương thích S3 tập trung, một bộ cân bằng tải có session affinity bằng cookie (đầu ra biên dịch được ghi vào đĩa cục bộ của instance, nên một lần biên dịch và lần tải PDF sau đó phải đến cùng một instance), và một git-bridge duy nhất. Mặc định của toolkit (b), vốn là thứ chúng tôi đo, có hệ số nhân bằng một, nên một hằng số được định cỡ cho một thành viên của hạ tầng trở thành giới hạn của toàn bộ bản cài đặt.
Hình 3. Lựa chọn shard trong clsi-cache. Một dự án được ánh xạ bởi crc32⁡(projectId-i) mod ∣shards∣\operatorname{crc32}(\text{projectId}\text{-}i)\bmod|\text{shards}|, tức là không gian băm được cắt thành số cung bằng nhau đúng bằng số shard. Đây là băm modulo, không phải băm nhất quán dạng vòng: tăng hạ tầng từ ba shard lên bốn sẽ phân vùng lại toàn bộ không gian và ánh xạ lại về cơ bản mọi dự án (a, b). Đó chính xác là lý do việc triển khai cần một đoạn chuyển đổi resharding trực tuyến tường minh, di chuyển một tỷ lệ dự án tăng tuyến tính từ currentShards sang desiredShards trong một khoảng thời gian, thay vì mức di chuyển K/nK/n mà một vòng băm nhất quán sẽ mang lại. Khi circuit breaker của một shard bị ngắt, salt ii được tăng lên và shard bị loại khỏi danh sách ứng viên, nên việc tra cứu tiếp tục dò sang shard khác thay vì thất bại (c).

2.3 Hai chế độ thất bại

Mọi cấu hình chúng tôi đo đều thất bại theo đúng một trong hai cách, và sự phân biệt này có thể thấy trực tiếp qua trạng thái phản hồi chứ không phải suy luận:
  • Cạn kiệt bộ nhớ — chính stack Overleaf trở nên không phản hồi và yêu cầu trả về HTTP 502. Bộ nhớ khả dụng của máy khách ở mức thất bại thường dưới 500 MiB.
  • Hết thời gian biên dịch — CLSI chấm dứt lần biên dịch khi đạt thời gian chờ theo người dùng và báo trạng thái timedout. Bộ nhớ khả dụng ở mức thất bại thường còn vài gigabyte.
Chúng tôi phân loại mỗi cấu hình theo dấu hiệu này thay vì theo một phép suy đoán dựa trên tỷ lệ tài nguyên, giúp câu hỏi “chúng ta đã chạm vào bức tường nào” có thể được trả lời từ chính dữ liệu.

3. Phương pháp

3.1 Môi trường thử nghiệm và kiểm soát xung nhịp

Tất cả các máy khách chạy dưới QEMU/KVM trên một máy chủ Intel Core i9-14900K duy nhất với 62 GiB RAM và lưu trữ NVMe. Máy khách là Ubuntu 24.04 với Docker 29.7 và Overleaf Toolkit triển khai Ayakaleaf Pro v6.2.2 với sandboxed compiles sử dụng texlive-full:2025.1. Một CPU máy tính để bàn thông dụng là một đại diện kém cho máy chủ trừ khi xung nhịp của nó được kiểm soát. KVM không cung cấp cơ chế nào để đặt xung nhịp ảo: một vCPU là một luồng của máy chủ và chạy ở bất kỳ tần số nào mà lõi máy chủ đang chạy. Do đó chúng tôi ràng buộc trực tiếp máy chủ, tắt turbo và cố định scaling_max_freq ở 3.0 GHz trên mọi lõi, đồng thời ghim các vCPU của máy khách vào các P-core vật lý bằng taskset. Sự phân biệt này quan trọng trên CPU lõi lai: các E-core của chip này có xung cơ bản 2.4 GHz và không thể đạt 3.0 GHz khi turbo bị tắt, nên một lần chạy lạc sang chúng sẽ âm thầm đo một cỗ máy chậm hơn. Dưới tải đầy đủ, chúng tôi xác minh chính xác 3000 MHz trên cả mười sáu luồng được ghim. Một script bảo vệ kiểm tra điều kiện bất biến này trước mỗi lần benchmark và từ chối khởi động nếu không thỏa; nó đã bắt được một lần governor bị reset âm thầm trong quá trình nghiên cứu.

3.2 Môi trường thử nghiệm thứ hai: một runner lớn

Ma trận QEMU cô lập từng biến một, nhưng bị giới hạn ở mười sáu luồng được ghim. Để kiểm tra liệu các quy luật tương tự có còn đúng ở quy mô cao hơn một bậc độ lớn hay không, chúng tôi lặp lại phép quét đồng thời trên một máy chủ lớn duy nhất: một AMD EPYC 7773X (Milan-X, 64 lõi / 128 luồng, 768 MiB L3) với 995 GiB RAM, chạy cùng image Ayakaleaf Pro v6.2.2 với cùng texlive-full:2025.1. Không giống các máy khách QEMU, máy này không được ghim xung nhịp: đây là một máy chủ cấp production và chúng tôi đo nó đúng như vậy. Hai biện pháp phòng ngừa vận hành là cần thiết và đáng được nêu ra, vì nếu thiếu chúng, thí nghiệm sẽ đo bộ công cụ kiểm thử thay vì máy chủ. Thứ nhất, mọi container được giới hạn trong một systemd slice với MemoryMax=940 GiB, để một phép quét mất kiểm soát chỉ làm cạn một cgroup chứ không phải máy chủ. Thứ hai, các sandboxed compile được tạo bởi daemon của máy chủ và mỗi cái làm bẩn lớp copy-on-write riêng của nó — đo được 116 MiB mỗi container dù image cơ sở 20.6 GiB được chia sẻ — nên thư mục dữ liệu gốc của Docker đã được chuyển sang một thiết bị NVMe chuyên dụng. Một phép quét tại N=1024N=1024 ghi khoảng 119 GiB lớp tạm, không thể chứa vừa trên một hệ thống tệp gốc thông thường.

3.3 Khối lượng công việc

Tài liệu là một luận văn thạc sĩ thực tế dài 63 trang (mẫu SJTU) được biên dịch bằng XeLaTeX qua latexmk, chứa các hình TikZ, xử lý thư mục tham khảo bằng biblatex và các tài nguyên PDF nhúng — tức là một tải thực tế chứ không phải tổng hợp. Một lần biên dịch đơn lẻ trên một máy khách không tải mất 8.6–9.8 s trên tất cả các cấu hình, giá trị mà chúng tôi dùng làm đường cơ sở bay tự do T1T_1.

3.4 Tạo tải

Chúng tôi tạo 512 tài khoản người dùng thực và cấp cho mỗi tài khoản một bản sao dự án riêng, để các lần biên dịch đồng thời tranh chấp đúng như những người dùng độc lập thay vì chia sẻ khóa dự án. Các yêu cầu được gửi từ máy chủ tới cổng được chuyển tiếp của máy khách, nên việc tạo tải không tiêu tốn CPU của máy khách. Tính đồng thời ở đây là cùng lúc, không phải so le. Mọi phiên được thiết lập trước — đăng nhập, CSRF token, chọn trình biên dịch — và chỉ sau đó mỗi luồng mới ngủ cho đến một thời điểm đồng hồ chung, được tính một lần và chia sẻ, trước khi gửi POST /project/:id/compile. Sự phân biệt này không phải là bắt bẻ. Một đoạn tăng tải so le đo thông lượng dưới một hàng đợi ổn định; một đợt bùng phát cùng lúc đo điều xảy ra khi cả giảng đường sinh viên nhấn cùng một nút sau cùng một thông báo hạn chót, vốn là tình huống mà người vận hành thực sự lo sợ. Hai trường hợp khác nhau nhiều hơn một hệ số hằng, vì trường hợp thứ hai lấp đầy hàng đợi biên dịch nhanh hơn tốc độ daemon có thể xử lý. Bốn trở ngại thực tế phải được loại bỏ trước khi có thể tạo ra đợt bùng phát đó một cách trung thực. Mỗi trở ngại đều đáng được ghi lại, vì mỗi cái đều âm thầm biến thí nghiệm thành phép đo bộ công cụ kiểm thử thay vì máy chủ.

3.4.1 Hai bộ giới hạn tốc độ, không phải một

Overleaf giới hạn số lần đăng nhập theo địa chỉ nguồn — 20 lần thử mỗi phút — và toàn bộ lưu lượng của chúng tôi xuất phát từ một máy chủ duy nhất. Gán cho mỗi người dùng mô phỏng một địa chỉ X-Forwarded-For riêng biệt sẽ loại bỏ giới hạn đó nhưng ngay lập tức gặp phải một giới hạn thứ hai, thô hơn: ngân sách theo subnet khoảng 200 mỗi phút. Do đó, việc trải người dùng trên một khối liền kề sẽ thất bại ở tài khoản thứ 201. Thay vào đó, chúng tôi suy ra địa chỉ tổng hợp từ chỉ số người dùng để các người dùng liên tiếp rơi vào các /24 khác nhau, 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 , giúp cả hai bộ giới hạn đều còn dư cho toàn bộ 1024 người dùng.

3.4.2 Header được chèn vào bị loại bỏ theo mặc định

Chỉ đặt header thôi là chưa đủ. Express chỉ chấp nhận X-Forwarded-For từ các peer mà nó được chỉ định là tin cậy, và trustedProxyIps của Overleaf mặc định là loopback. Vì bộ tạo tải đến ứng dụng qua bridge của container chứ không qua giao diện loopback, header được phân tích rồi bị vứt bỏ, và mọi người dùng mô phỏng lại dồn về một địa chỉ. Triệu chứng là một loạt HTTP 429 đúng ở lần đăng nhập thứ hai mươi, dễ bị hiểu nhầm là máy chủ quá tải. Mạng gateway phải được thêm vào chuỗi tin cậy một cách tường minh; trong bản triển khai dạng cụm ở §4.3, các CIDR của pod và service cũng phải được thêm vào.

3.4.3 Bộ cân bằng tải sẽ ghi đè header mà nó được yêu cầu giữ nguyên

Khi instance nằm sau một proxy, chỉ thị thông thường option forwardfor sẽ nối thêm địa chỉ client thực vào chuỗi, đây là hành vi đúng cho production nhưng lại sai hoàn toàn trong trường hợp này: địa chỉ tổng hợp bị thay thế bởi chính địa chỉ của bộ tạo tải. Chỉ thị phải được bổ sung thành option forwardfor if-none, để proxy chỉ thêm giá trị khi client không cung cấp.

3.4.4 Client hết file descriptor trước khi máy chủ hết năng lực

Tại N=1024N=1024, bộ tạo tải giữ hơn một nghìn socket đồng thời, và giới hạn mềm mặc định 1024 descriptor bị chạm tới trong lúc thiết lập phiên chứ không phải trong lúc đo. Lỗi này diễn ra lặng lẽ: ba phiên không thiết lập được và lần chạy báo 1021 thay vì 1024, trong khi một luồng lấy mẫu gọi shell để đếm container chết với EMFILE và âm thầm cắt cụt dữ liệu đo từ xa. Giới hạn mềm phải được nâng lên trên bộ tạo tải — giới hạn cứng trên máy chủ của chúng tôi vốn đã là 1048576 — và lần chạy phải được lặp lại. Chúng tôi báo cáo cả hai lần chạy ở §4.3: lần đã sửa hoàn thành 1024 trên 1024 với trung vị chênh lệch trong vòng 1.2 s so với lần bị cắt cụt, đó là lý do chúng tôi coi lần đầu là dùng được nhưng không có tính quyết định.

3.5 Giao thức đo

Một số lựa chọn phương pháp tỏ ra cần thiết để đảm bảo khả năng tái lập.

3.5.1 Khởi động làm nóng

Trên một máy khách vừa khởi động, page cache trống và những lần biên dịch đầu tiên đo I/O khởi động nguội thay vì năng lực ở trạng thái ổn định: cùng cấu hình 2 vCPU / 2 GiB cho 36.5 s khi nguội và 9.8 s khi đã nóng, chênh lệch 3.7 lần. Do đó, mỗi cấu hình thực hiện hai lần biên dịch đơn lẻ làm nóng (bị loại bỏ) sau khi khởi động.

3.5.2 Tiêu chí đạt

Một mức đồng thời chỉ đạt nếu mọi lần biên dịch đều thành công và mức đó vượt qua một lần lặp lại. Điều này nghiêm ngặt hơn một ngưỡng tỷ lệ thành công và nó quan trọng: tại 4 vCPU / 16 GiB, mức 32 đã đạt một lần với trung vị 80.2 s rồi hết thời gian trên cả 32 lần biên dịch khi lặp lại, nên chúng tôi báo cáo 31.

3.5.3 Tìm kiếm

Các mức được xác định bằng cách khoanh vùng theo hàm mũ từ một giá trị khởi đầu do mô hình dự đoán, sau đó chia đôi chính xác trên số nguyên. Vì tiêu chí là tất cả hoặc không gì cả, một mức được xác định ngay ở lần thất bại đầu tiên, nên chúng tôi bỏ các yêu cầu còn đang chạy khi có một yêu cầu thất bại — trừ ở các mức nhỏ, nơi các lần biên dịch bị bỏ dở chiếm giữ một máy khách nhỏ nặng đến mức nó không bao giờ phục hồi.

3.5.4 Cô lập giữa các mức

Các container biên dịch được dọn sạch, và ứng dụng web được thăm dò cho đến khi phản hồi trở lại, trước khi mức tiếp theo bắt đầu. Nếu không, một mức đến sau một sự cố sập sẽ ghi nhận một lỗi giả với không phiên nào.

3.5.5 Vệ sinh máy chủ

Các máy ảo không liên quan trên máy chủ đã được tắt: với 24 GiB bộ nhớ máy chủ bị chiếm dụng ở nơi khác, cùng một cấu hình máy khách báo load average 11.7 thay vì 3.2 ở cùng mức đồng thời. Áp lực bộ nhớ của máy chủ lan vào máy khách và làm mất hiệu lực phép đo.

4. Kết quả

4.1 Ma trận năng lực

Bảng 1 và Hình 4 cho biết giới hạn đo được của mọi cấu hình. Đọc theo hàng ngang là điều bất ngờ đầu tiên. Ở 4 GiB, các máy khách 2, 4 và 8 vCPU đều đạt đúng 9 — tăng gấp bốn số lõi không thay đổi gì cả. Ở 16 GiB, chúng đạt 54, 45 và 57: đi từ 4 lên 16 lõi chỉ mang lại 6%, và máy khách 8 lõi thậm chí còn tệ hơn máy 4 lõi (§5.2). Chỉ ở 48 GiB, số lõi mới phân tách các cấu hình một cách rõ rệt: 143, 268 và 331.
Hình 4. Năng lực đo được trên ma trận cấu hình. (a) Mỗi cấu hình là một cột, được nhóm theo bộ nhớ và tô màu theo số lõi; cột tô đặc là bị giới hạn bởi bộ nhớ (máy khách chết do cạn bộ nhớ) và cột gạch chéo là bị giới hạn bởi CPU (các lần biên dịch hết thời gian trong khi bộ nhớ vẫn dư). Đọc một nhóm từ trái sang phải cho thấy số lõi mang lại ít lợi ích thế nào dưới 16 GiB; đọc qua các nhóm cho thấy lợi ích siêu tuyến tính của bộ nhớ. (b) Cùng các điểm đó so với mô hình được khớp Nmax⁡=min⁡(0.69R1.60, 26.4C)N_{\max}=\min(0.69R^{1.60},\,26.4C); đường gạch đứt là bức tường bộ nhớ và các đường ngang chấm là trần CPU theo từng số lõi. Một cấu hình bị giới hạn bởi bức tường nào mà nó gặp trước. Đọc theo cột dọc là điều bất ngờ thứ hai: với số lõi cố định, năng lực tăng siêu tuyến tính theo bộ nhớ, xấp xỉ R1.6R^{1.6}, vì lý do page cache được phân tích ở §5.1. Bảng 1. Số lần biên dịch đồng thời tối đa hoàn thành thành công, được đo với thời gian chờ biên dịch 300 s và đã gỡ bỏ giới hạn đồng thời của CLSI. In đậm đánh dấu cấu hình bị giới hạn bởi CPU (các lần biên dịch hết thời gian trong khi bộ nhớ vẫn dư); các cấu hình còn lại bị giới hạn bởi bộ nhớ (stack chết với HTTP 502). Hàng 2 GiB mang giá trị đã hiệu chỉnh được thảo luận ở §5.2.

4.2 Đồng thời là chia sẻ thời gian

Hình 5 quét mọi mức đồng thời trên một máy khách cố định 8 vCPU / 16 GiB. Hai chế độ được phân tách bởi một điểm gãy sắc nét đúng tại một lần biên dịch mỗi lõi. Bên dưới điểm đó, thời gian biên dịch trung bình gần như không đổi — đi từ 8.7 s tại N=1N=1 lên 9.1 s tại N=C=8N=C=8, thay đổi 5%. Bên trên điểm đó, thời gian tăng đúng tỷ lệ với N/CN/C: tại N=16,24N=16,24 chúng tôi đo được 18.5 s và 27.1 s, tức tỷ lệ 1:2.13:3.121:2.13:3.12 so với mức lý tưởng 1:2:31:2:3.
Hình 5. Độ trễ biên dịch theo mức đồng thời trên phần cứng cố định. Điểm gãy nằm tại N=CN=C; vượt qua đó, mức chậm lại đo được bám theo N/CN/C trong phạm vi 5–7%. Tất cả mười lăm mức đều thành công hoàn toàn.
Hình 6. Độ trễ biên dịch theo mức đồng thời cho một số cấu hình. Mỗi bảng giữ cố định phần cứng và quét tải đưa vào; đường thẳng đứng đánh dấu N=CN=C. Các đường cong phẳng ở bên trái và tuyến tính theo N/CN/C ở bên phải, đó là dấu hiệu của chia sẻ thời gian chứ không phải tranh chấp: công việc không trở nên tốn kém hơn, nó chỉ chờ đến lượt. Khớp T(N)=T1max⁡(1,N/C)bT(N)=T_1\max(1,N/C)^{b} trên tất cả các phép đo thành công trong nghiên cứu cho b=0.914b=0.914 (Rlog⁡2=0.904R^2_{\log}=0.904, n=81n=81). Một số mũ không thể phân biệt với một là phát biểu định lượng rằng một lần biên dịch là một đơn vị công việc đơn luồng, bị giới hạn bởi CPU, và tính đồng thời không giúp ích cũng không gây hại gì ngoài việc chia các lõi. Hệ quả thực tế gây khó chịu cho việc lập kế hoạch năng lực: một cấu hình có thể tiếp nhận số lượng người dùng tùy ý mà không thất bại, trong khi khiến mỗi người phải chờ lâu hơn theo tỷ lệ. Tại N=56N=56 trên máy khách này, mọi lần biên dịch vẫn thành công, nhưng mỗi người dùng phải chờ 64.8 s thay vì 8.7 s.

4.3 Mở rộng theo chiều dọc tới 1024 lần biên dịch đồng thời

Bảng 2 và Hình 7 báo cáo phép quét trên runner lớn. Mỗi mức là một lần biên dịch nguội: trước mỗi mức, chúng tôi xóa thư mục biên dịch và cache CLSI của mọi dự án tham gia qua DELETE /project/:id/output, để không mức nào được hưởng lợi từ công việc của mức trước. Đường cơ sở biên dịch đơn lẻ trên máy này là 28.8 s, là con số nguội và không nên so sánh với đường cơ sở ổn định 8.6–9.8 s dùng trước đó; đường cơ sở nguội trên các máy khách QEMU là 28.3 s, nên tính theo từng luồng, hai máy chênh nhau trong vòng hai phần trăm đối với khối lượng công việc này. Bảng 2. Phép quét đồng thời trên một EPYC 7773X (64 lõi / 128 luồng, 995 GiB). Mọi mức đều nguội; đường cơ sở 28.8 s. Container đỉnh là số sandbox tối đa tồn tại đồng thời.
Hình 7. Mở rộng theo chiều dọc trên một runner lớn. (a) Độ trễ theo mức đồng thời đưa vào; vùng tô bóng đánh dấu chế độ sau điểm gãy. (b) Số sandbox thực sự tồn tại không bao giờ bám theo số được yêu cầu — nó bão hòa ở gần 200 — trong khi cgroup biên dịch không bao giờ dùng quá một phần năm hạn mức của nó.

4.3.1 Máy không bao giờ thất bại

Mọi mức đều hoàn thành 100%, kể cả N=1024N=1024 — gấp tám lần số luồng. Chúng tôi không tìm thấy giới hạn năng lực của máy này; chúng tôi hết kiên nhẫn trước khi nó hết dư địa. Đây là cấu hình đầu tiên trong nghiên cứu mà ràng buộc giới hạn không phải là bộ nhớ: tại N=1024N=1024, cgroup biên dịch đạt đỉnh 184 GiB, một phần năm hạn mức 940 GiB, trong khi CPU ở mức sử dụng 100% với load average 166.

4.3.2 Mức suy giảm dưới tuyến tính vì việc tiếp nhận bị giới hạn tốc độ

Chia sẻ thời gian ngây thơ dự đoán rằng gấp 8×8\times số luồng sẽ tốn gấp 8×8\times độ trễ. Chi phí đo được là 9.7×9.7\times so với một lần biên dịch đơn lẻ, nhưng chỉ 3.8×3.8\times so với N=128N=128 — trong khi tải đưa vào tăng gấp tám. Lý do thể hiện ở Hình 7(b) và cột cuối của Bảng 2: mặc dù 1024 yêu cầu được gửi cùng lúc, số sandbox thực sự tồn tại không bao giờ vượt quá 205. Daemon không thể tạo container nhanh bằng tốc độ client yêu cầu, nên các yêu cầu xếp hàng ở khâu tiếp nhận thay vì tranh chấp bên trong CPU. Việc xếp hàng là thứ cứu phần đuôi ở đây, và nó làm vậy một cách tình cờ.

4.3.3 Điểm gãy nằm ở 512, không phải ở điểm thất bại

Giữa N=256N=256 và N=512N=512, độ trễ p95p_{95} tăng 2.9×2.9\times khi tải tăng gấp đôi; mọi lần tăng gấp đôi trước đó chỉ tốn từ 1.2×1.2\times đến 1.4×1.4\times. Năng lực được nêu là ”NN lớn nhất không thất bại” sẽ báo cáo 1024 và vô dụng với người vận hành: tại điểm đó, thời gian chờ ở phần đuôi gần tám phút.

4.4 Thời gian biên dịch tỷ lệ nghịch với xung nhịp

Vì khối lượng công việc bị giới hạn bởi CPU, chi phí của nó nên tỷ lệ với 1/f1/f. Chúng tôi kiểm tra trực tiếp điều này bằng cách quét xung nhịp của máy chủ trên toàn dải của máy, 1.0–5.5 GHz trong mười bước, trên một máy khách không thay đổi gì khác (Hình 8). Thời gian biên dịch đơn lẻ đi từ 26.5 s xuống 4.8 s: xung nhịp gấp 5.5 lần mang lại tốc độ nhanh gấp 5.5 lần mà không có lợi ích giảm dần ở bất kỳ đâu trong dải. Tích T ⁣⋅ ⁣fT\!\cdot\!f không đổi trong phạm vi 2% trên cả mười mức xung nhịp. Chuẩn hóa theo phần lõi được chia gom tất cả ba mươi phép đo — ba mức đồng thời ở mười mức xung nhịp — về một hằng số duy nhất: 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} với độ phân tán phần dư 5.1% trên một dải mà bản thân xung nhịp thay đổi 5.5 lần. Việc không có bất kỳ độ cong nào chính là kết quả: nếu khối lượng công việc bị giới hạn bởi băng thông bộ nhớ hoặc I/O, TT sẽ phẳng ra ở xung nhịp cao khi CPU vượt quá tài nguyên kia.
Hình 8. Quét xung nhịp. (a) T=k/fT=k/f với hyperbol được khớp. (b) Sau khi chia cho max⁡(1,N/C)\max(1,N/C), mọi điểm gom về một hằng số, xác nhận Phương trình (1). Phương trình (1) có một hệ quả trực tiếp đối với việc mua sắm, dễ phát biểu và dễ hiểu sai: xung nhịp cải thiện trải nghiệm của từng người dùng, còn số lõi chỉ cho phép tiếp nhận thêm người dùng. Một máy có xung nhịp cao hơn 20% biên dịch nhanh hơn 20% cho tất cả mọi người, không có lợi ích giảm dần; gấp đôi số lõi không làm lần biên dịch của bất kỳ ai nhanh hơn.

5. Phân tích

5.1 Hai bức tường, được khớp riêng biệt

Mỗi cấu hình được phân loại theo dấu hiệu thất bại của nó (§2.3), và bức tường bộ nhớ cùng trần CPU sau đó chỉ được khớp trên các cấu hình thực sự chạm vào chúng: Nmax⁡=min⁡(ARp,  kcC)N_{\max} = \min\left(A R^{p},\; k_c C\right) với RR tính bằng gibibyte và CC tính bằng vCPU. Số mũ của bức tường bộ nhớ luôn siêu tuyến tính, p>1p>1: chi phí bộ nhớ biên của thêm một lần biên dịch đồng thời giảm khi tổng bộ nhớ tăng, từ khoảng 312 MiB mỗi lần biên dịch trên máy khách 3 GiB xuống khoảng 194 MiB trên máy 32 GiB. Cơ chế là page cache dùng chung trên cây TeX Live được mô tả ở §2.1: các lần biên dịch đồng thời đọc các tệp font và macro chồng lấn nhau, nên một cache lớn hơn được khấu hao trên nhiều lần biên dịch hơn. Đó là lý do quy tắc ngây thơ “một gigabyte cho năm người dùng” dự đoán thấp cho các máy lớn và dự đoán cao cho các máy nhỏ.
Hình 9. Cùng dữ liệu được biểu diễn dưới dạng hai bề mặt trên mặt phẳng (C,R)(C,R). (a) Năng lực: bề mặt được khớp là một sống núi, không phải một mặt phẳng — nó dốc lên mạnh theo bộ nhớ và gần như phẳng dọc theo trục lõi cho đến khi bộ nhớ không còn là giới hạn, đó là lý do hàng 48 GiB là hàng duy nhất mà số lõi phân tách các cấu hình. (b) Độ trễ theo mức đồng thời cho mọi cấu hình, với T=12.6 (N/C)0.91T=12.6\,(N/C)^{0.91} được khớp hiển thị bằng nét đứt và thời gian chờ 180 s được vẽ thành một mặt phẳng. Một cấu hình thất bại ở nơi đường cong liền của nó xuyên qua mặt phẳng đó, cho thấy rõ thiết lập thời gian chờ quyết định trực tiếp năng lực được báo cáo như thế nào.

5.2 Khi nhiều lõi hơn lại làm mọi thứ tệ hơn

Phương trình (2) là giá trị nhỏ nhất của hai số hạng và do đó đơn điệu theo CC, nhưng các phép đo thì không. Chúng tôi quan sát hai trường hợp đảo ngược trong đó việc thêm lõi làm giảm năng lực: ở 16 GiB (54 so với 45) và ở 32 GiB (145 so với 135). Cả hai đều xảy ra trong chế độ bị giới hạn bởi bộ nhớ, và cơ chế giống nhau: với nhiều lõi hơn, các lần biên dịch đồng thời tiến triển đồng bộ và đạt kích thước bộ nhớ thường trú đỉnh cùng một lúc, trong khi với ít lõi hơn, bộ lập lịch xen kẽ chúng và các đỉnh lệch nhau. Trên một máy khách có dư địa bộ nhớ vốn đã sát giới hạn, sự lệch nhau đó là thứ giữ cho nó sống sót. Một mô hình năng lực dựa trên mức sử dụng tài nguyên trung bình không thể biểu diễn điều này; đó là thuộc tính của sự trùng khớp các đỉnh. Một trường hợp đảo ngược biểu kiến thứ ba, ở 2 GiB, chúng tôi hiện không tính đến. Phép tìm kiếm ghi nhận năng lực 2 ở 2 vCPU nhưng 1 ở 4 và 8 vCPU, trông như cùng một hiệu ứng. Xem xét lại các phép quét thô cho thấy điều đơn giản hơn: ở 2 GiB, mức N=2N=2 thành công ở lần thử đầu tiên với cả ba số lõi rồi thất bại ở lần chạy xác nhận với hai trong ba. Mức đó không phải là năng lực mà là một lần tung đồng xu, và mục 2 vCPU là lần tung tình cờ rơi đúng. Do đó chúng tôi báo cáo giá trị có thể tái lập, 1, ở cả ba số lõi và không rút ra kết luận gì từ sự khác biệt. Chúng tôi ghi lại sự hiệu chỉnh ở đây thay vì âm thầm sửa lại bảng, vì kết quả bị loại bỏ thuộc loại có thể đã hỗ trợ một tuyên bố thú vị.

6. Các phát hiện về triển khai

6.1 Giới hạn đồng thời được mã hóa cứng

Trên các máy khách đủ lớn, năng lực dừng ở đúng 65 lần biên dịch đồng thời bất kể mức đồng thời được yêu cầu: tại N=66,80,96,128N=66,80,96,128 chúng tôi đo được 6565 lần thành công và 1,15,31,631,15,31,63 phản hồi unavailable tức thì, với số container cố định ở 65, vài gigabyte bộ nhớ chưa dùng, và thời gian biên dịch trung vị ổn định ở 77 s — thấp hơn nhiều so với bất kỳ thời gian chờ nào. Nguyên nhân là một hằng số trong CLSI:
Phép so sánh không nghiêm ngặt, nên giới hạn thực tế là 64+1=6564+1=65, khớp chính xác với phép đo. Các yêu cầu vượt quá nhận HTTP 503 — chúng bị từ chối, không được xếp hàng, nên từ phía người dùng, nút biên dịch đơn giản là thất bại. Không giống mọi tham số có thể điều chỉnh khác trong cùng tệp, tham số này không đọc biến môi trường nào; nó được giới thiệu ở upstream vào tháng 8 năm 2024 và chỉ có thể thay đổi bằng cách sửa image. Khi giới hạn được nâng lên, cùng máy khách 16 vCPU / 32 GiB từng báo success=65, unavailable=15 tại N=80N=80 đã báo success=80.

6.2 Giới hạn bộ nhớ container không hoạt động

Kiểm tra một container biên dịch đang chạy cho thấy không có bất kỳ sự cô lập tài nguyên nào:
Việc không có hạn mức CPU là có chủ đích và giải thích tại sao số mũ chia sẻ thời gian ở §4.2 lại rõ ràng như vậy: không có gì làm méo mó sự cạnh tranh giữa các lần biên dịch. Tuy nhiên, việc không có giới hạn bộ nhớ thì không phải có chủ đích. Docker runner có yêu cầu một giới hạn:
Điều này sai đến hai lần. Giá trị là 10244=1tebiB1024^4=1 tebiB trong khi chú thích muốn nói 102431024^3; và trường này được đặt ở cấp cao nhất của các tùy chọn tạo container thay vì bên trong HostConfig, nơi Docker API mong đợi, nên nó bị bỏ qua — điều mà giá trị Memory=0 quan sát được xác nhận. Cả hai lỗi đều có mặt trong commit giới thiệu tệp này (9a519f0d3d, tháng 3 năm 2018) và tồn tại qua việc chuyển đổi từ CoffeeScript, một lần định dạng lại toàn repository, và một lần chuyển từ CJS sang ESM, không lần nào xem xét lại ngữ nghĩa. Đáng chú ý, MAX_OUTPUT = 1024 * 1024 // 1MB trong cùng commit lại đúng, cho thấy đây là một sơ suất chứ không phải hiểu nhầm. Hệ quả thể hiện rõ trong các phép đo bộ nhớ thấp của chúng tôi. Vì các lần biên dịch không bị giới hạn, việc cạn kiệt bộ nhớ không biểu hiện bằng việc Docker chấm dứt một container vi phạm; nó đánh sập toàn bộ máy khách. Trên cấu hình 2 vCPU / 2 GiB, chúng tôi quan sát thấy phiên SSH giám sát bị chặn trong 300 s, load average 68 trên hai lõi, và cuối cùng máy khách tự khởi động lại. Một giới hạn theo container hoạt động đúng sẽ suy giảm nhẹ nhàng hơn nhiều: lần biên dịch quá lớn sẽ thất bại và dịch vụ sẽ sống sót. Giới hạn duy nhất thực sự có hiệu lực là RLIMIT_CPU, được đặt thành timeout+5\text{timeout}+5 giây. Nó giới hạn thời gian CPU, không phải thời gian thực, và một lần biên dịch đơn lẻ chỉ tiêu tốn khoảng 9 s CPU, nên nó không bao giờ là giới hạn ở bất kỳ mức đồng thời nào; nó bảo vệ trước các đầu vào bất thường như một macro chạy mất kiểm soát. Tuy nhiên, nó là một công cụ kiểm chứng hữu ích: quan sát thấy Soft:305 xác nhận rằng thiết lập thời gian chờ 300 s đã thực sự được truyền tới container.

6.3 Thời gian chờ biên dịch là tham số điều chỉnh chi phối

Trường theo người dùng features.compileTimeout mặc định là 180 s. Với bất kỳ cấu hình nào bị giới hạn bởi CPU, đây không phải là biên an toàn mà là một thiết lập năng lực, vì một máy vẫn đang tính toán đúng lại bị tuyên bố là thất bại. Tăng nó lên 300 s — chỉ với một lệnh cập nhật MongoDB — thay đổi năng lực đo được tới 4.2 lần (Bảng 3). Giới hạn trên là 600 s, được áp đặt bởi RequestParser.MAX_TIMEOUT, vượt quá giá trị này sẽ bị cắt bớt một cách âm thầm. Bảng 3. Ảnh hưởng của thời gian chờ biên dịch lên năng lực đo được. Hai hàng cuối là nửa phản trực giác của kết quả và là lý do chúng tôi đo lại mọi cấu hình với cùng một thời gian chờ. Với các cấu hình bị giới hạn bởi bộ nhớ, thời gian chờ dài hơn lại làm giảm năng lực, vì mỗi lần biên dịch giữ bộ nhớ thường trú lâu hơn và nhiều lần biên dịch chồng lấn nhau hơn. Do đó, một con số năng lực là vô nghĩa nếu không nêu thời gian chờ mà nó được đo, và hai giá trị này không thể trộn lẫn trong cùng một bảng.

7. Các công trình liên quan

7.1 Hướng dẫn của nhà cung cấp

Tài liệu phần cứng của chính Overleaf nêu các sự thật định tính mà chúng tôi định lượng ở đây: rằng LaTeX là đơn luồng, rằng do đó hiệu năng đơn lõi quyết định thời gian biên dịch, và rằng “nhiều lõi hơn chỉ giúp ích nếu bạn đang cố biên dịch nhiều tài liệu hơn số lõi CPU còn trống” [1]. Sau đó nó đưa ra quy tắc định cỡ tuyến tính — cơ sở 2 lõi/3 GiB cộng thêm một lõi và một gigabyte cho mỗi năm đến mười người dùng đồng thời — vốn là động lực cho nghiên cứu này. Đóng góp của chúng tôi là biến những phát biểu này thành các quy luật đo được (Phương trình (1) và (2)), và chỉ ra nơi quy tắc tuyến tính bị phá vỡ: nó không có số hạng nào cho page cache dùng chung khiến bức tường bộ nhớ trở nên siêu tuyến tính, và không có số hạng nào cho hai tham số phần mềm chi phối kết quả.

7.2 Các nghiên cứu năng lực về build và CI

Việc đo đạc các hệ thống build dưới tải đồng thời đã được nghiên cứu kỹ bên ngoài bối cảnh LaTeX. LightSys báo cáo rằng các hệ thống CI thông thường biên dịch bên trong container Docker bị suy giảm I/O khi tốc độ đến của pull request tăng lên, với nút thắt cổ chai xuất hiện quanh mười một yêu cầu đồng thời [17]; TAOS-CI quan sát thấy biên dịch chiếm phần lớn thời gian thực của CI, chiếm 60–67% tổng thời lượng pipeline trên các dự án lớn [18]. Hệ thống của chúng tôi khác ở một điểm hóa ra mang tính quyết định: một lần biên dịch LaTeX mang tính tương tác. Một job CI mất gấp đôi thời gian chỉ là bất tiện; một lần biên dịch mất gấp đôi thời gian được người dùng đang chờ ở khung xem trước quan sát trực tiếp, đó là lý do chúng tôi coi thời gian chờ không phải là ngưỡng thất bại mà là một tham số năng lực.

7.3 Chi phí phụ trội của container

Các công trình gần đây phân tách độ trễ khởi động container Docker trên các tầng lưu trữ [19] và mô tả đặc tính hiệu năng container ở biên (edge) [20]. Trong bối cảnh của chúng tôi, chi phí khởi động container cho mỗi lần biên dịch được khấu hao: nó là một hằng số nhỏ so với lần biên dịch 9 s, và thời gian bay tự do T1T_1 mà chúng tôi khớp đã hấp thụ nó. Thuộc tính container thực sự quan trọng là việc không có giới hạn tài nguyên (§6.2), thứ biến một lần tràn bộ nhớ của một lần biên dịch thành sự cố của toàn bộ máy chủ.

7.4 LaTeX như một đầu vào không đáng tin cậy

Biên dịch trong sandbox tồn tại vì TeX là một ngôn ngữ lập trình và tài liệu là đầu vào không đáng tin cậy [21, 22]. Lựa chọn thiết kế đó là thứ khiến nghiên cứu này khả thi — mỗi lần biên dịch là một container cô lập với hành vi tài nguyên có thể quan sát — và cũng là thứ khiến giới hạn bộ nhớ bị thiếu trở nên nghiêm trọng, vì người vận hành triển khai nó đều mặc định có sự cô lập.

7.5 Trình biên dịch như một đối tượng nghiên cứu

Bản thân TeX được tài liệu hóa đầy đủ như một ngôn ngữ [16], nhưng hành vi của nó như một mục tiêu build chỉ mới thu hút sự chú ý gần đây. Tan và Rigger [8] biên dịch một kho lớn mã nguồn arXiv trên nhiều engine và phiên bản bản phân phối và nhận thấy rằng lựa chọn engine không thể thay thế cho nhau: chỉ một phần nhỏ của một phần trăm tài liệu tạo ra đầu ra giống hệt từng byte dưới XeTeX và pdfTeX. Kết quả đó liên quan trực tiếp đến phương pháp của chúng tôi. Năng lực là thuộc tính của một tài liệu và một engine, nên một benchmark không cố định cả hai thì không thể tái lập; do đó chúng tôi cố định một tài liệu, một engine và một bản phân phối (texlive-full:2025.1) xuyên suốt, và nêu engine trong mọi chú thích hình. Nó cũng giới hạn tính tổng quát của các con số của chúng tôi theo cách đáng được nói rõ: chúng mô tả XeLaTeX trên tài liệu này, không phải TeX nói chung. Công việc về các hệ thống build LaTeX phần lớn do người thực hành dẫn dắt. l3build của dự án LaTeX3 [13] chuẩn hóa kiểm thử hồi quy và đóng gói, và các benchmark độc lập so sánh các công cụ bao bọc — một khảo sát 26 hệ thống build cho thấy một preamble được biên dịch trước mang lại lợi ích khoảng 20% so với chạy thông thường và 40% so với latexmk [14]. Những thứ này tối ưu hóa lần biên dịch đơn lẻ. Chúng trực giao với, và có thể kết hợp với, những gì chúng tôi đo: một cache preamble rút ngắn T1T_1, và mọi con số năng lực trong bài viết này đều tỷ lệ theo T1T_1.

7.6 Kiểm soát đồng thời trong trình soạn thảo, không phải trình biên dịch

Nửa cộng tác của Overleaf dựa trên một dòng nghiên cứu đã được xác lập vững chắc. Operational transformation bắt nguồn từ Ellis và Gibbs [9] và được làm cho khả thi với các client độ trễ cao bởi hệ thống Jupiter [10], thiết kế của nó có thể nhận ra trong document-updater: một máy chủ sắp xếp thứ tự các thao tác và một bộ đệm theo tài liệu mà các client đồng bộ với nó. Các kiểu dữ liệu nhân bản không xung đột (CRDT) [11] giải quyết cùng vấn đề mà không cần bộ sắp xếp trung tâm. Sự phân biệt này là thứ khiến kiến trúc ở §4.3 hoạt động được: vì bộ đệm cập nhật đang chờ nằm trong Redis dùng chung chứ không nằm trong bộ nhớ của một instance, một lần biên dịch được định tuyến tới bất kỳ bản sao nào cũng quan sát được những lần gõ phím mới nhất, và affinity biên dịch có thể được chọn vì tính cục bộ của cache chứ không phải vì tính đúng đắn.

7.7 Các mô hình năng lực

Định luật Amdahl [24] giới hạn mức tăng tốc từ song song hóa và định luật Little [23] liên hệ mức chiếm dụng với tốc độ đến và thời gian phục vụ; cả hai đều được sử dụng ở trên. Định luật khả năng mở rộng phổ quát của Gunther [12] mở rộng định luật đầu tiên với một số hạng thoái lui cho độ trễ nhất quán, dự đoán rằng thông lượng đạt đỉnh rồi suy giảm. Chúng tôi lưu ý rằng hệ thống của chúng tôi không thể hiện chế độ thoái lui đó cho tới N=1024N=1024: thông lượng bão hòa và độ trễ tăng, nhưng không có gì sụp đổ. Lý do mang tính cấu trúc chứ không phải may mắn — các lần biên dịch không chia sẻ trạng thái nào cần giữ nhất quán, nên số hạng mà định luật thêm vào gần bằng không, và mức trần tiếp nhận ở §4.3 giới hạn tranh chấp trước khi nó có thể gây ảnh hưởng.

8. Khuyến nghị cho người vận hành

1

Sửa hai tham số phần mềm trước khi mua phần cứng

Cả hai đều miễn phí và đều có giá trị hơn bất kỳ nâng cấp phần cứng đơn lẻ nào chúng tôi đã đo. Tăng features.compileTimeout lên một giá trị mà người dùng thực sự chấp nhận được — mức tối đa CLSI chấp nhận là 600 s — và, nếu bạn dự kiến vượt quá 65 lần biên dịch đồng thời, hãy nâng compileConcurrencyLimit trong một image dẫn xuất hoặc mở rộng theo chiều ngang. Không làm cả hai nghĩa là trả tiền cho những lõi mà phần mềm từ chối sử dụng.
2

Định cỡ một máy theo điểm gãy, không theo giới hạn trần

Phép quét trên runner lớn (§4.3) tách biệt hai con số thường bị nhầm lẫn với nhau. Giới hạn trần — mức đồng thời lớn nhất vẫn trả về mọi PDF — ít nhất là 1024 trên máy chủ 64 lõi, và chúng tôi chưa bao giờ chạm tới nó. Điểm gãy — điểm mà vượt qua đó độ trễ đuôi không còn tăng nhẹ nhàng mà bắt đầu tăng gấp đôi — nằm ở 512, và điểm vận hành thoải mái cuối cùng bên dưới nó là 256. Giữa N=256N=256 và N=512N=512, thời gian chờ p95p_{95} đi từ hai phút lên gần sáu phút; giữa 512 và 1024 nó đạt tám phút. Người vận hành định cỡ theo giới hạn trần sẽ cung cấp một hệ thống về mặt kỹ thuật thì hoạt động nhưng không ai muốn dùng.Với máy này và tài liệu này, điểm vận hành được khuyến nghị do đó là 256 lần biên dịch đồng thời, tức 4×4\times số lõi vật lý và 2×2\times số luồng, và giữ p95p_{95} ở gần 120 s. Chúng tôi đề xuất đặt compileConcurrencyLimit thành giá trị đó thay vì để cao: tiếp nhận 1024 lần biên dịch cùng lúc khiến mọi người phải chờ tám phút, trong khi tiếp nhận 256 và xếp hàng phần còn lại phục vụ hầu hết người dùng trong hai phút. Xếp hàng làm chậm những người đến muộn; tranh chấp làm chậm tất cả mọi người.
3

Coi đây là các con số trong trường hợp xấu nhất

Mọi mức trong Bảng 2 đều là biên dịch nguội được kích hoạt cùng lúc. Không điều kiện nào trong số đó đúng trong production: một lần biên dịch nóng của cùng tài liệu mất 8.6 s so với 28.3 s khi nguội, chênh lệch 3.33.3 lần, và người dùng thực không nhấn nút trong cùng một giây. Một tập người dùng ở trạng thái ổn định biên dịch lại mỗi hai phút với tỷ lệ trúng cache điển hình do đó sẽ duy trì được nhiều người viết hơn đáng kể so với những gì con số đồng thời gợi ý — khoảng một nghìn tác giả hoạt động trở lên tại điểm vận hành 256. Con số đồng thời là giới hạn cho đợt bùng phát tức thời, không phải số chỗ ngồi.
4

Quyết định ngân sách độ trễ trước, rồi suy ra kích thước

Phương trình (1) có thể đảo ngược trực tiếp. Với thời gian chờ mục tiêu TT ở xung nhịp ff trên CC lõi, mức đồng thời phù hợp là N≤C fT/kN \le C\,fT/k với k≈28GHz⋅sk\approx28 GHz·s cho tài liệu này. Ngân sách 60 s trên 8 lõi ở 3 GHz cho N≤51N\le51; ngân sách 120 s tăng gấp đôi con số đó. Công bố ngân sách cùng với năng lực là cách trung thực duy nhất để nêu bất kỳ con số nào trong hai.
5

Mua bộ nhớ trước, rồi đến lõi, và kiểm tra bạn đang ở bức tường nào

Dưới 32 GiB, chúng tôi đo được hầu như không có lợi ích gì từ việc thêm lõi. Cách chẩn đoán rất đơn giản: nếu lỗi xuất hiện dưới dạng HTTP 502 khi máy khách thiếu bộ nhớ, hãy thêm bộ nhớ; nếu lỗi xuất hiện dưới dạng timedout trong khi bộ nhớ vẫn dư, hãy thêm lõi hoặc tăng thời gian chờ. Người vận hành có thể đọc điều này từ chính dấu hiệu thất bại mà chúng tôi đã dùng để phân loại cấu hình.
6

Ưu tiên xung nhịp cho trải nghiệm, số lõi cho quy mô người dùng

Vì T∝1/fT\propto 1/f đúng mà không có độ cong (2% trên dải 1.0–5.5 GHz), xung nhịp nhanh hơn làm mọi lần biên dịch nhanh hơn cho mọi người dùng. Nhiều lõi hơn không làm lần biên dịch đơn lẻ nào nhanh hơn; chúng chỉ cho phép tiếp nhận thêm các lần biên dịch đồng thời. Các bản triển khai có than phiền là “biên dịch chậm” nên mua xung nhịp; các bản triển khai có than phiền là “biên dịch thất bại khi đến hạn chót” nên mua bộ nhớ và lõi.
7

Mở rộng theo chiều ngang thay vì chiều dọc khi vượt quá giới hạn trần

Vượt quá 65 lần biên dịch đồng thời, con đường được hỗ trợ là mở rộng theo chiều ngang (Hình 2c và 10, được trình bày chi tiết ở §9): nhiều instance ứng dụng đứng sau một bộ cân bằng tải với session affinity bằng cookie, chia sẻ MongoDB, Redis và lưu trữ tương thích S3 tập trung, với git-bridge được giữ ở dạng một instance duy nhất. Điều này nhân giới hạn theo instance với số lượng instance, đó chính xác là cách bản triển khai SaaS đạt được năng lực của riêng nó.
8

Không dựa vào sự cô lập cho từng lần biên dịch

Cho đến khi giới hạn bộ nhớ của Docker runner được sửa (§6.2), một tài liệu bất thường duy nhất có thể làm cạn kiệt máy chủ thay vì chỉ bị dừng riêng. Người vận hành cần đảm bảo này nên tự áp đặt nó thay vì chờ đợi. Cơ chế chúng tôi dùng trên máy chủ lớn là một systemd slice mang giới hạn cứng, rồi trỏ Docker daemon vào đó để mọi container nó tạo ra đều được tính bên trong slice:
Có một chi tiết ở đây sẽ tốn cả buổi chiều nếu bỏ sót. Một slice tên docker-capped.slice không nằm cạnh docker.slice; nó nằm bên trong đó, vì dấu gạch nối là ký tự phân cách phân cấp chứ không phải một phần của tên. Một giới hạn trông như không có tác dụng thường đã được áp dụng lệch một cấp so với nơi các container thực sự nằm. Hãy xác minh bằng cách đọc lại giá trị đỉnh từ memory.max_usage_in_bytes sau khi chạy thay vì tin vào tệp cấu hình — trên máy chủ của chúng tôi, cgroup biên dịch không bao giờ vượt quá một phần năm giới hạn của nó ngay cả ở 1024 lần biên dịch đồng thời, bản thân điều đó là bằng chứng rằng daemon chứ không phải bộ nhớ là ràng buộc giới hạn.
Hình 10. Kiến trúc tham chiếu cho một bản triển khai mở rộng theo chiều ngang, được vẽ từ cấu hình mà chúng tôi đã xác minh. Các bản sao ứng dụng có thể hoán đổi cho nhau và không lưu giữ gì lâu dài, nên chúng có thể được thêm và bớt tự do. Ba thành phần thì không như vậy: Redis, với bộ đệm tài liệu cho phép một lần biên dịch được định tuyến tới bất kỳ bản sao nào thấy được những lần gõ phím mới nhất; object store, trở thành bắt buộc thay vì tùy chọn khi có nhiều hơn một bản sao; và git-bridge, lưu repository trên đĩa cục bộ mà không có đường nhân bản và phải chạy như một instance duy nhất bên cạnh một bản sao được chỉ định.

9. Một bản triển khai đa máy tham chiếu

Mọi thứ ở trên đều đo một máy duy nhất. Phần này mô tả dạng phân tán đủ chi tiết để xây dựng, và — vì câu hỏi người vận hành thực sự đối mặt không phải là làm thế nào mà là có nên hay không — trước tiên nêu thời điểm mà nó trở nên đáng để bỏ công.

9.1 Khi nào dạng phân tán là cần thiết

Một máy đơn lẻ rẻ hơn để vận hành ở mọi khía cạnh quan trọng: một miền lỗi, không có trạng thái chia sẻ cần giữ nhất quán, không có định tuyến nào có thể cấu hình sai. Dữ liệu của chúng tôi đặt ra ba ngưỡng cho thời điểm nên rời bỏ nó.

9.1.1 Dưới 65 lần biên dịch đồng thời, đừng làm

Giới hạn theo instance là một hằng số phần mềm, không phải phần cứng (§6.1). Cho đến khi tải đưa vào tiến gần tới nó, một máy thứ hai chỉ thêm các chế độ thất bại mà không mang lại gì. Máy chủ 64 lõi phục vụ 256 lần biên dịch đồng thời thành công hoàn toàn chỉ sau khi compileConcurrencyLimit được nâng lên; người vận hành chưa thay đổi giá trị đó thì chưa bị giới hạn bởi phần cứng và không nên đi mua phần cứng.

9.1.2 Từ 65 đến khoảng 500, hãy mở rộng theo chiều dọc trước

Mở rộng theo chiều dọc vẫn tuyến tính trên toàn bộ dải của chúng tôi và không bao giờ đi vào chế độ thoái lui. Một máy chủ lớn đạt 1024 lần biên dịch nguội đồng thời với tỷ lệ thành công 100% (§4.3); điểm gãy độ trễ xuất hiện ở 512, không sớm hơn. Trong khoảng đó, một máy lớn hơn đơn giản hơn hẳn so với nhiều máy nhỏ, và theo §4.4, một máy nhanh hơn cải thiện trải nghiệm của mọi người dùng thay vì chỉ cho phép tiếp nhận thêm người dùng.

9.1.3 Phân tán vì tính sẵn sàng, không phải vì thông lượng

Lý do trung thực để chạy nhiều hơn một bản sao ứng dụng dưới giới hạn trần là vì một máy có nghĩa là một nguồn điện, một kernel, một khoảng thời gian nâng cấp. Đó là một lý do chính đáng và là lý do mà chúng tôi sẽ đưa ra; chỉ là nó không phải là lập luận về năng lực, và việc nhầm lẫn hai thứ khiến người vận hành mua thêm bản sao trong khi thứ họ cần là bộ nhớ.

9.2 Các tầng và cách định cỡ

Hình 10 cho thấy kiến trúc. Nó có bốn tầng, và chúng mở rộng theo các đại lượng khác nhau — đó chính là mục đích của việc tách chúng ra.

9.2.1 Biên (Edge)

Một bộ cân bằng tải, hoặc hai để đảm bảo tính sẵn sàng. Nó kết thúc TLS và không làm gì tốn kém; nó mở rộng theo số kết nối, không theo số lần biên dịch, và một instance nhỏ là đủ cho các mức tải được nghiên cứu ở đây. Cấu hình của nó, chứ không phải kích thước, mới là điều quan trọng (§9.3).

9.2.2 Các bản sao ứng dụng

Chúng gánh tải biên dịch và là tầng duy nhất mở rộng theo mức đồng thời. Định cỡ mỗi bản sao theo các quy tắc ở §8 — bộ nhớ trước lõi, rồi đến xung nhịp — sau đó đặt số bản sao sao cho bao phủ được mức đồng thời đỉnh chia cho giới hạn mỗi bản sao. Các bản sao không lưu giữ gì lâu dài: đĩa cục bộ của chúng chứa dữ liệu tạm của quá trình biên dịch và một cache đầu ra, cả hai đều có thể tái tạo. Đây là điều khiến việc thêm và bớt chúng một cách tự do là an toàn, và nó đáng được xác minh thay vì giả định, vì chỉ một đường dẫn filestore cấu hình sai cũng âm thầm biến tầng này thành tầng có trạng thái.

9.2.3 Trạng thái

Redis, MongoDB và một object store tương thích S3, trên các máy chủ riêng biệt. Redis là thành phần chịu tải chính và ít hiển nhiên nhất: nó giữ kho phiên và bộ đệm tài liệu trực tiếp, thứ cho phép một lần biên dịch được định tuyến tới bất kỳ bản sao nào quan sát được những lần gõ phím được nhập vào một bản sao khác. Người vận hành coi Redis như một cache và định cỡ nó cho việc loại bỏ dữ liệu (eviction) sẽ tạo ra các lần biên dịch trên tài liệu cũ, cực kỳ khó chẩn đoán, vì không có gì thất bại — đầu ra chỉ đơn giản là sai. MongoDB mở rộng theo số dự án chứ không theo tốc độ biên dịch. Object store là tùy chọn khi có một bản sao và bắt buộc khi có nhiều hơn.

9.2.4 Thành phần đơn nhất

git-bridge lưu repository trên đĩa cục bộ, duy trì một chỉ mục cục bộ và không có đường nhân bản. Nó phải chạy đúng một instance, được ghim bên cạnh một bản sao được chỉ định, và nó là thành phần khiến bản triển khai không hoàn toàn phi trạng thái. Hãy lên kế hoạch cho máy chủ của nó tương ứng: đĩa của nó là đĩa cần được sao lưu. Bảng 4. Các tầng tham chiếu. Chỉ tầng ứng dụng mở rộng theo mức đồng thời; việc định cỡ nó là chủ đề của §8.

9.3 Định tuyến là phần dễ cấu hình sai

Ba loại yêu cầu phải đến ba nơi khác nhau, và cấu hình mặc định một quy tắc chỉ đáp ứng được tối đa hai trong số đó. Lưu lượng biên dịch dưới /project/ nên được phân phối bằng băm nhất quán trên định danh dự án, để cache biên dịch của một dự án ở lại với một bản sao. Chúng tôi dùng balance hash path,field(3,/) của HAProxy với hash-type consistent và hash-balance-factor 150. Lựa chọn này quan trọng khi mở rộng: với affinity bằng cookie, các phiên hiện có bị ghim vào bản sao ban đầu vô thời hạn và một bản sao mới thêm vào chỉ nhận người dùng mới, nên cỗ máy mà người vận hành vừa trả tiền không gánh được chút tải nào trong số tải đã thúc đẩy việc mua nó. Trong cấu hình của chúng tôi, băm nhất quán đã phân phối lại 35% dự án khi mở rộng, so với 0% khi dùng cookie. Lưu lượng phiên thì khác. Khi nâng cấp WebSocket thất bại và socket.io chuyển sang XHR polling, các lần polling liên tiếp của một phiên phải đến cùng một bản sao, và không có định danh dự án nào trong đường dẫn để băm. Lưu lượng này cần một backend riêng với affinity bằng cookie. Chúng tôi đã thiết kế sự phân tách này nhưng chưa triển khai; chúng tôi đánh dấu nó là một khoảng trống thay vì tuyên bố đã làm. Cuối cùng, /git/ phải đến bản sao mà git-bridge chạy bên cạnh. Nó được định tuyến tới bản sao đó thay vì trực tiếp tới git-bridge vì bridge xác thực các callback của nó với các endpoint OAuth của ứng dụng và phân giải các URL blob thông qua ứng dụng; bỏ qua bản sao sẽ làm hỏng xác thực thay vì cải thiện điều gì.

9.4 Thu hẹp cần một khoảng đệm để rút cạn

Gỡ bỏ một bản sao không đối xứng với việc thêm một bản sao: một lần biên dịch đang chạy sẽ bị mất, và người dùng thấy một lỗi không phải do họ gây ra. Trình tự khả thi là dừng lưu lượng mới trước, chờ, và chỉ sau đó mới chấm dứt. Chúng tôi triển khai điều này dưới dạng một pre-stop hook giữ pod trong một khoảng thời gian có thể cấu hình trong khi bộ cân bằng tải đánh dấu backend là đang rút cạn — đủ ngắn để kiểm thử trong vài phút, và trong production đủ dài để một phiên kết thúc tự nhiên, tính bằng giờ chứ không phải giây. Khoảng thời gian này là núm điều chỉnh quyết định việc co giãn là vô hình hay gây bực bội. Một ràng buộc khác mà chúng tôi phát hiện qua đo đạc chứ không phải qua thiết kế: tự động mở rộng dựa trên CPU không hoạt động với khối lượng công việc này. Mức sử dụng của chính pod ứng dụng chỉ ở 22 m core so với tổng 3997 m core của node, vì công việc biên dịch diễn ra trong các container anh em mà pod không tính đến. Bất kỳ tín hiệu nào dùng để mở rộng tầng này phải đếm số container biên dịch đang chạy, không phải CPU của pod.

10. Hàm ý vượt ra ngoài Overleaf

Không có gì trong §4.2 hoặc §4.3 là đặc thù cho mã của Overleaf. Các quy luật đo được xuất phát từ ba thuộc tính mà bất kỳ dịch vụ LaTeX được lưu trữ nào cũng có: đơn vị công việc là một tiến trình đơn luồng, nó được cô lập trong một container, và tập làm việc của nó là một cây chỉ đọc lớn mà page cache phải chứa. Ba hệ quả áp dụng trực tiếp cho bất kỳ ai xây dựng một dịch vụ như vậy.

10.1 Cấp phát bộ nhớ trước, rồi đến lõi

Kết quả mạnh nhất của ma trận là một kết quả phủ định: dưới 16 GiB, số lõi gần như không liên quan, và chỉ ở 48 GiB các cấu hình 4, 8 và 16 vCPU mới tách biệt (143, 268, 331). Người vận hành hiểu quy tắc thông thường là “thêm một lõi cho mỗi năm người dùng” sẽ mua sai tài nguyên. Cơ chế là page cache dùng chung trên cây bản phân phối, và đó là thuộc tính của kích thước TeX Live chứ không phải của bất kỳ front-end cụ thể nào.

10.2 Tốc độ tiếp nhận là một tài nguyên, và thường bị lãng quên

Tại N=1024N=1024, máy chủ của chúng tôi không bao giờ giữ quá 205 sandbox đang sống (Hình 7b) dù mọi yêu cầu đến cùng một lúc. Việc tạo container, chứ không phải biên dịch, là yếu tố giới hạn — nhất quán với các nghiên cứu đo đạc quy chi phí khởi động container cho chi phí phụ trội của runtime chứ không phải kích thước image [19, 20]. Một dịch vụ chỉ định cỡ CPU và bộ nhớ sẽ thấy hành vi bùng phát của nó bị chi phối bởi một đại lượng mà nó chưa bao giờ đo. Dạng thực tiễn của điều này là khuyến nghị ở §8: chủ động giới hạn việc tiếp nhận, vì một hàng đợi bạn chọn tốt hơn một hàng đợi bạn phát hiện ra.

10.3 Một sandbox tồn tại lâu hơn lần biên dịch của nó làm mất hiệu lực mô hình

Mọi con số năng lực ở đây đều giả định container được tạo, thực hiện một lần biên dịch rồi thoát — vòng đời vài chục giây và hệ số hoạt động gần bằng một chỉ khi nó đang chạy. Hai mẫu thiết kế gần đây phá vỡ giả định đó, và chúng phá vỡ theo cùng một cách. Mẫu thứ nhất là sandbox cố định theo từng người dùng. Cấp cho mỗi người dùng một môi trường riêng cố định biến một nhóm tài nguyên được ghép kênh thống kê thành một tập hợp các đặt chỗ: một dịch vụ có thể phục vụ 256 lần biên dịch đồng thời từ 64 lõi nhờ chia sẻ thời gian chỉ có thể phục vụ 16 người dùng nếu mỗi người được cấp bốn lõi chuyên dụng, ít hơn một bậc độ lớn trên cùng phần cứng. Dữ liệu của chúng tôi định lượng chi phí của lựa chọn đó chứ không phản đối nó — đặt chỗ mua được tính dự đoán được, và tỷ giá trao đổi khoảng 16×16\times tại điểm vận hành chúng tôi khuyến nghị. Mẫu thứ hai, mới hơn, là AI agent chia sẻ sandbox với trình biên dịch. Trong các nền tảng soạn thảo có agent hỗ trợ, cùng container chạy XeLaTeX cũng có thể chứa một coding agent chạy lâu dài, nên nó bị chiếm dụng liên tục thay vì theo từng đợt. Những người thực hành báo cáo đúng triệu chứng mà mô hình dự đoán cho các bản triển khai như vậy — chậm chạp kéo dài ngay cả với số người dùng khiêm tốn [15]. Sự tương tác này đáng được nêu chính xác, vì nó không đơn giản là “thêm tải”. Ba phát hiện của chúng tôi cộng dồn với nhau. Mức chiếm dụng không còn theo đợt, nên quy luật chia sẻ thời gian ở §4.2 áp dụng cho toàn bộ người dùng cùng lúc thay vì chỉ cho phần đang biên dịch. Page cache, thứ mang lại lợi ích bộ nhớ siêu tuyến tính ở §4.1, giờ bị chia sẻ với tập làm việc của chính agent và không còn nóng cho TeX. Và giới hạn bộ nhớ container bị thiếu ở §6.2 trở nên nguy hiểm hơn nhiều, vì một container không bao giờ thoát thì không bao giờ trả lại bộ nhớ. Chúng tôi không đo đạc một nền tảng như vậy và không đưa ra tuyên bố nào về bất kỳ sản phẩm cụ thể nào. Điều chúng tôi có thể nói là những gì các con số của chúng tôi hàm ý cho thiết kế: một kiến trúc cấp cho mỗi người dùng một sandbox đa lõi tồn tại lâu dài nên được định cỡ như một hệ thống đặt chỗ, không phải theo các con số đồng thời được báo cáo ở đây, và năng lực nó có thể kỳ vọng gần với số lõi chia cho số lõi mỗi người dùng hơn là bất cứ con số nào trong Bảng 1.

11. Các mối đe dọa đến tính hợp lệ

11.1 Một tài liệu duy nhất

Tất cả các phép đo đều dùng một tài liệu XeLaTeX 63 trang. Năng lực tuyệt đối sẽ khác với các tài liệu khác; các quy luật mở rộng, vốn là các tỷ lệ, thì không nên khác. Một tài liệu có bộ nhớ thường trú lớn hơn đáng kể sẽ dịch chuyển bức tường bộ nhớ mà không thay đổi tính chất siêu tuyến tính của nó.

11.2 Máy chủ ảo hóa

Các máy khách chạy dưới KVM trên một máy vật lý, nên các con số tuyệt đối bao gồm chi phí phụ trội của ảo hóa và các máy khách chia sẻ page cache cũng như thiết bị NVMe của máy chủ. Chúng tôi đã giảm thiểu yếu tố gây nhiễu lớn nhất bằng cách tắt các máy khách không liên quan sau khi quan sát thấy áp lực bộ nhớ của máy chủ làm tăng load average bên trong máy khách hơn 3×3\times ở cùng mức đồng thời.

11.3 Đến cùng lúc

Mọi lần biên dịch được gửi tại một thời điểm, đây là trường hợp xấu nhất. Người dùng thực đến theo một quá trình ngẫu nhiên, nên một bản triển khai được định cỡ theo các con số của chúng tôi sẽ có dư địa chứ không thiếu hụt — nhưng đỉnh tải vào cuối hạn nộp bài gần với mô hình của chúng tôi hơn là mô hình Poisson.

11.4 Các cấu hình biên

Ở 2 GiB, hệ thống gần với ngưỡng sụp đổ đến mức các lần chạy lặp lại của cùng một cấu hình có thể chênh nhau một lần biên dịch. Chúng tôi báo cáo giá trị thận trọng và không rút ra kết luận từ các chênh lệch ±1\pm 1 trong chế độ đó.

12. Tính khả dụng

Hệ thống được kiểm thử, công cụ triển khai và dự án upstream mà nó dẫn xuất đều công khai: Mọi vị trí mã nguồn mà chúng tôi trích dẫn đều được cung cấp dưới dạng đường dẫn tương đối trong repository kèm số dòng theo Ayakaleaf Pro v6.2.2, và hai commit upstream mà chúng tôi xác định niên đại (9a519f0d3d, 5d472e9b38) đều có thể truy cập được trong lịch sử của Overleaf.

13. Đóng góp

Musicminion thiết kế nghiên cứu, cung cấp và vận hành các môi trường thử nghiệm, định hướng hướng điều tra và xác minh mọi phép đo được báo cáo ở đây. Claude Opus 5 (Anthropic) xây dựng và vận hành bộ công cụ benchmark, tự động hóa các bản triển khai, thực hiện khảo cứu mã nguồn, tạo các hình và soạn thảo bản thảo. Cả hai tác giả đều đã xem xét văn bản cuối cùng. Ở những nơi một lần chạy được báo cáo là bị nhiễm — phép quét 1021 phiên ở §4.3 và mức N=256N=256 bất thường ở §4.1 — lỗi đã được phát hiện trong quá trình xem xét và lần chạy được lặp lại trước khi công bố thay vì bị âm thầm loại bỏ. Người đọc nên lưu ý rằng các chính sách về quyền tác giả tại ACM, IEEE và ICMJE hiện chỉ dành quyền tác giả cho các bên có khả năng chịu trách nhiệm về một công trình, và sẽ yêu cầu đóng góp của tác giả thứ hai được ghi nhận dưới dạng công bố thông tin thay vì đứng tên. Chúng tôi nêu rõ sự phân công lao động ở đây để hồ sơ chính xác theo cả hai quy ước.

14. Kết luận

Lập kế hoạch năng lực cho Overleaf tự lưu trữ không phải là vấn đề mở rộng một tài nguyên. Ba phát hiện sau đây nên thay đổi cách thực hiện việc này. Thứ nhất, dưới 32 GiB bộ nhớ máy khách, số lõi hầu như không quan trọng: ở 16 GiB, năng lực của các máy khách 4, 8 và 16 vCPU chênh lệch dưới 8%. Bộ nhớ, thông qua page cache dùng chung trên cây TeX Live, đặt ra giới hạn; số lõi chỉ bắt đầu quan trọng khi bộ nhớ đã dồi dào. Thứ hai, hai tham số phần mềm có sức nặng hơn phần cứng. Gỡ bỏ giới hạn 65 lần biên dịch được mã hóa cứng của CLSI và tăng thời gian chờ biên dịch mặc định 180 s đã đưa một máy khách 8 vCPU / 48 GiB từ 64 lên 268 lần biên dịch đồng thời — gấp 4.2 lần mà không cần thêm phần cứng. Không tham số nào có thể được phát hiện từ tài liệu cấu hình; một trong hai thậm chí không thể cấu hình được. Thứ ba, câu hỏi “máy này hỗ trợ bao nhiêu người dùng đồng thời” là chưa được xác định đầy đủ. Tính đồng thời trong hệ thống này là chia sẻ thời gian thuần túy, và năng lực là bất cứ thứ gì thời gian chờ cho phép. Dạng trung thực của câu trả lời nêu cả hai: máy này phục vụ NN lần biên dịch đồng thời nếu người dùng chịu chờ TT giây, với NN và TT liên hệ qua Phương trình (1). Chúng tôi cũng báo cáo một lỗi tiềm ẩn: giới hạn bộ nhớ theo container trong Docker runner đã không có hiệu lực từ năm 2018, cả về độ lớn lẫn vị trí đặt. Hậu quả thực tế của nó là việc cạn kiệt bộ nhớ trên một bản triển khai nhỏ sẽ đánh sập toàn bộ dịch vụ thay vì chỉ một lần biên dịch gây ra lỗi.

Tài liệu tham khảo

[1] Overleaf. Hardware requirements, Tài liệu on-premises. https://docs.overleaf.com/on-premises/getting-started/requirements/hardware-requirements [2] Overleaf. Horizontal scaling, Tài liệu on-premises. https://docs.overleaf.com/on-premises/maintenance/horizontal-scaling [3] Overleaf. Microservices, Tài liệu on-premises. 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] Các báo cáo từ người thực hành về độ trễ kéo dài trên các nền tảng soạn thảo có agent hỗ trợ, nơi một coding agent chạy liên tục được đặt cùng trình biên dịch LaTeX trong một sandbox theo từng người dùng. Chúng tôi trích dẫn đây là kinh nghiệm vận hành được báo cáo, không phải một phép đo có kiểm soát; chúng tôi không benchmark một nền tảng như vậy. [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.
Lần sửa đổi cuối 5 tháng 10, 2026