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 , rồi tăng 190% tại . 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 cho , 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ề với 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áotimedout 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ụ người dùng đồng thời trên 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 quaweb clsi 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ạylatexmk 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:
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 tiến trình đơn luồng, và đó là lý do chúng đều đặn đến vậy.

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.

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.

clsi-cache. Một dự án được ánh xạ bởi , 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 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 đượ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.
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ụngtexlive-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ùngtexlive-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 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 qualatexmk, 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 .
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ửiPOST /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,
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ậnX-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ườngoption 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 , 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ớiEMFILE 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.
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 lên 9.1 s tại , thay đổi 5%. Bên trên điểm đó, thời gian tăng đúng tỷ lệ với : tại chúng tôi đo được 18.5 s và 27.1 s, tức tỷ lệ so với mức lý tưởng .

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 quaDELETE /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.

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ả — 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 , 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 số luồng sẽ tốn gấp độ trễ. Chi phí đo được là so với một lần biên dịch đơn lẻ, nhưng chỉ so với — 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 và , độ trễ tăng khi tải tăng gấp đôi; mọi lần tăng gấp đôi trước đó chỉ tốn từ đến . Năng lực được nêu là ” 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 . 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 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: 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, sẽ phẳng ra ở xung nhịp cao khi CPU vượt quá tài nguyên kia.
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: với tính bằng gibibyte và tính bằng vCPU. Số mũ của bức tường bộ nhớ luôn siêu tuyến tính, : 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ỏ.
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 , 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 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 chúng tôi đo được lần thành công và phản hồiunavailable 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:
success=65, unavailable=15 tại đã 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: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 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ùngfeatures.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 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 , và mọi con số năng lực trong bài viết này đều tỷ lệ theo .
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 trongdocument-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 : 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 và , thời gian chờ đ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 số lõi vật lý và số luồng, và giữ ở 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 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 ở xung nhịp trên lõi, mức đồng thời phù hợp là với cho tài liệu này. Ngân sách 60 s trên 8 lõi ở 3 GHz cho ; 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ì đú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.
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 khicompileConcurrencyLimit đượ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ẫnfilestore 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 , 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 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 ở 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 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:- Ayakaleaf Pro — https://github.com/ayaka-notes/ayakaleaf-pro
- Bộ công cụ triển khai — https://github.com/ayaka-notes/toolkit
- Tài liệu — https://ayakaleaf-pro.ayaka.space
- Overleaf upstream — https://github.com/overleaf/overleaf
- Các image biên dịch TeX Live —
ghcr.io/ayaka-notes/texlive-full:2025.1
9a519f0d3d, 5d472e9b38) đều có thể truy cập được trong lịch sử của Overleaf.

