How We Halved Go Monorepo CI Build Time
Bài tóm tắt Uber 2022 3. Quy mô lớn Độ khó: Khó
Uber · 2022 · 3. Quy mô lớn · Độ khó: Khó — bài gốc
Bối cảnh
- Monorepo Go của Uber build bằng Bazel, hơn 10 triệu target, khoảng 500.000 file. Luồng một thay đổi: review trên Phabricator, CI chạy build + test, rồi SubmitQueue áp thay đổi lên HEAD mới nhất và chạy lại trước khi push.
- Changed Targets Calculation (CTC): dựng cây kiểu Merkle (mỗi package băm từ mọi nguồn và đầu vào) trước và sau thay đổi để biết target nào bị ảnh hưởng, trực tiếp lẫn gián tiếp; mất khoảng 5 phút mỗi thay đổi. Thay đổi ảnh hưởng hơn khoảng 10.000 target gọi là big change (ví dụ sửa một thư viện nền), kiểm chứng có khi mất vài giờ.
- Vấn đề hàng đợi: Conflict Analyzer của SubmitQueue cho các thay đổi không xung đột chạy song song, nhưng big change xung đột với gần như mọi thay đổi sau nó. Khoảng 50 thay đổi mỗi giờ trong giờ làm việc: một big change một giờ ở đầu hàng là 49 người khác chờ hơn một giờ.
Thay đổi chính
- Tối ưu SubmitQueue: đơn giản hoá đồ thị xung đột; cherry-pick lên HEAD ở bước kiểm và đẩy nhánh đó lên remote để các bước sau dùng lại, tránh fetch và patch lại: thời gian push giảm khoảng 65%.
- Chặn big change trong giờ làm việc (làm trong vài ngày, hiệu quả ngay): bị phàn nàn vì thay đổi quan trọng phải chờ; văn phòng trải khắp thế giới nên đổi thành "giờ cao điểm" (chủ yếu giờ Mỹ); big change dễ gây sự cố hơn mà ngoài giờ thì ít người xử lý, nên người ta phải dậy sớm để land (ví dụ nâng compiler Go). Thêm "delayed lands" tự land sau giờ cao điểm. Vẫn chưa đủ: phải kiểm nhanh hơn.
- Song song hoá CI: bước kiểm phụ thuộc chạy song song với CTC (miễn phí); chia target đã đổi ra nhiều máy. Trên Jenkins (MultiJob cần cây tĩnh) họ khai báo sẵn 50 shard và bật theo metadata để giả lập sharding động: 10.000 target từ một giờ còn khoảng 10 phút.
- Shard theo root target: chia đều target đã đổi thì target dùng chung bị build lại trên nhiều shard. CTC đổi sang tính root target (không target nào phụ thuộc chúng) và chia theo đó, để một cây phụ thuộc nằm trên một shard. Đủ để bỏ lệnh chặn big change.
- Buildkite, CI động thật: cấu hình build lúc chạy (không đổi target thì bỏ bước build; ít target thì một shard, nhiều thì chia động). Nhưng mỗi shard một container riêng nghĩa là mỗi shard checkout lại repo và không có cache chung: P95/P99 tốt hơn Jenkins, P50 và trung bình lại tệ hơn, vì trong repo 10 triệu target hầu hết thay đổi là nhỏ và chi phí dựng môi trường lấn át.
- Git nhanh hơn: snapshot Git làm mới định kỳ, tải nguyên khối về máy trước khi checkout; chỉ fetch các commit sau snapshot.
- Container sống lâu: giữ container và Bazel server giữa các shard, định danh theo phiên bản Bazel, hash image, tên job: CTC nhanh hơn gần 60%.
- Cache trên host: snapshot module cache Go (cho
go mod tidymỗi job) và output base của Bazel, job đầu trên máy tải về rồi chia cho job sau; làm nóng đồ thị build trước CTC.
- Remote cache: một package dùng chung bởi nhiều root target vẫn bị build trên nhiều shard. Bazel build theo artifact và (giả định) tất định, nên artifact và kết quả test được đẩy lên cache chung: HTTP tự viết → gRPC trên GCS (P95 từ dao động 40 đến hơn 100 phút về ổn định dưới 40 phút) → Buildfarm năm 2021 (thời gian build giảm khoảng một nửa). Kết quả test lúc review được dùng lại lúc land nếu không phụ thuộc nào đổi; chỉ test bị thay đổi khác land trước làm ảnh hưởng mới chạy lại.
Kết quả (theo Uber)
- Với khoảng 50 thay đổi mỗi giờ, một thay đổi ảnh hưởng toàn repo vẫn land được dưới 15 phút.
Vì sao
- Thời gian của một thay đổi trong monorepo không chỉ là thời gian build của nó mà còn là thời gian chờ sau những thay đổi xung đột phía trước; big change biến hàng đợi thành tuần tự (CI và build ở quy mô lớn).
- Build tất định theo nội dung cho phép cache cả artifact lẫn kết quả test giữa máy, giữa pipeline, giữa lúc review và lúc land (Cache và invalidation).
Bài học cho SWE
- Một lệnh chặn thô (không land big change giờ cao điểm) mua thời gian, nhưng chi phí của nó (chờ, dậy sớm, ít người trực) nhắc rằng phải sửa gốc.
- Chia việc theo cấu trúc phụ thuộc (root target), không chia đều theo số lượng.
- Tối ưu cho trường hợp phổ biến: cách ly sạch từng shard làm đuôi tốt lên mà trung vị xấu đi, vì phần lớn thay đổi nhỏ và bị chi phí dựng môi trường chi phối.
- Kết quả test cũng là artifact cache được, nếu khoá là hash của mọi đầu vào.
Liên quan
#CI#Go monorepo#Bazel#SubmitQueue#Changed Targets Calculation#Sharding#Buildkite#Jenkins#Remote cache#Buildfarm
Được nhắc tới trong
Công ty (1)