Chúng tôi đã giảm kích thước git của monorepo JavaScript như thế nào tới 94%
(jonathancreamer.com)- Monorepo JavaScript cỡ lớn 1JS của Microsoft đã phát triển tới hơn 1.000 người dùng hoạt động hằng tháng, khoảng 2.500 package và khoảng 20 triệu dòng mã; gần đây kích thước clone đã lên tới 178GB, khiến một số lập trình viên thậm chí khó có thể clone được
- Nút thắt đầu tiên được phát hiện là cấu trúc để các file thay đổi của Beachball dồn hàng chục nghìn file vào một thư mục; khi số file tăng tới 40.000, mỗi lần thêm file mới lại tạo ra tree object rất lớn
- Nguyên nhân lớn hơn là với các tên file dài được cập nhật lặp đi lặp lại như
CHANGELOG.mdvàCHANGELOG.json, mã đóng gói cũ của Git chỉ nhìn 16 ký tự cuối của tên file để chọn đối tượng delta git repack -adf --window=250cũng có hiệu quả, nhưng khi chạygit repack -adf --path-walktrên Microsoft Git fork2.47.0.vfs.0.2, kho lưu trữ cục bộ đã giảm từ 178GB xuống 5GB- Nếu trong monorepo lớn có các file tên dài thường xuyên thay đổi, cần kiểm tra
pack.usePathWalk truevà lệnh mớigit survey; các thay đổi liên quan cũng sẽ được đưa vào Git upstream
Quy mô và nút thắt của monorepo 1JS
- Monorepo JavaScript cỡ lớn 1JS bên trong Microsoft không chỉ lớn về dung lượng lưu trữ mà còn cả quy mô mã nguồn và số lượng người đóng góp
- Hơn 1.000 người dùng hoạt động hằng tháng
- Khoảng 2.500 package
- Khoảng 20 triệu dòng mã
- Kích thước kho lưu trữ được clone gần đây là 178GB, và một số lập trình viên ở châu Âu không thể clone kho vì kích thước quá lớn
- Khi mới clone lần đầu, nó chỉ ở mức 1~2GB, nhưng vài tháng sau đã tăng lên khoảng 4GB và tiếp tục tăng sau đó
Bài học #1: Đừng dồn hàng nghìn file vào một thư mục
git-sizerđã chỉ ra các blob lớn và vấn đề với file thay đổi của Beachball- Blob lớn thường xuất hiện khi vô tình commit file nhị phân, và có thể ngăn bằng các tính năng như giới hạn kích thước check-in của Azure DevOps
- Các file đã đi vào lịch sử thì không dễ xóa bỏ về sau
- 1JS sử dụng Beachball change files để tự động hóa việc tăng phạm vi semver của package
- Mục đích sử dụng tương tự Changesets
- Mục tiêu là tự động hóa việc tăng phiên bản package như semantic-release
- Do không xóa các file thay đổi, chúng đã chất đống tới 40.000 file trong một thư mục, và khi thêm file mới trong trạng thái đó sẽ tạo ra tree object lớn
- Có hai biện pháp xử lý
- Sửa bằng Beachball PR để thay vì một file cho mỗi package, nhiều thay đổi được gộp vào một file thay đổi duy nhất
- Viết pipeline để tự động dọn dẹp thư mục file thay đổi theo định kỳ
Bài học #2: Nhánh versioned và vấn đề đóng gói changelog
- Luồng quản lý phiên bản của 1JS duy trì nhánh
versionedlà bản mirror củamain- Lưu phiên bản package thực tế để tránh xung đột Git trên
main - Cho phép nhìn chính xác mối quan hệ giữa phiên bản semver đã phát hành lên NPM package và commit Git
- Lưu phiên bản package thực tế để tránh xung đột Git trên
- Nhánh
versionedchủ yếu chỉ chứa các commit thêm nội dung vào fileCHANGELOG.mdvàCHANGELOG.json, nhưng vẫn phình to đến mức việc clone ngày càng khó khăn - Từ năm 2021, kho lưu trữ tự nhiên tăng lên mỗi năm với hàng trăm nghìn dòng mã và hàng trăm lập trình viên mới, nhưng khi tốc độ tăng trưởng vượt monorepo Office của Microsoft thì người ta bắt đầu nghi ngờ có nguyên nhân riêng
- Sau khi các lập trình viên đã xây dựng những tính năng Git cho monorepo lớn như
shallow checkout,sparse indexxem xét, họ phát hiện khi lấy nhánhversionedthì đang fetch thêm 125GB dữ liệu Git
Cách mã đóng gói Git đã tạo delta sai
- Nguyên nhân là mã đóng gói cũ của Git khi so sánh file trước khi nén chỉ xem 16 ký tự cuối của tên file
- Thông thường Git chỉ push diff của file đã thay đổi, nhưng vì vấn đề đóng gói này, các file
CHANGELOG.mdở các package khác nhau lại bị đem ra so sánh với nhau - Ví dụ, nếu thay đổi
repo/packages/foo/CHANGELOG.json, Git trong quá trình chuẩn bị push có thể tạo diff vớirepo/packages/bar/CHANGELOG.json - Vì vậy trong nhiều trường hợp toàn bộ file bị push lại lặp đi lặp lại, và một số file có thể có kích thước hàng chục MB
- Chi tiết của vấn đề được tóm tắt trong bài viết trên Git mailing list của Stolee
Kết quả của repack và path-walk
- Trước tiên, họ cải thiện việc nén file pack bằng
git repack -adf --window=250với cửa sổ lớn hơn, và kích thước kho đã giảm đáng kể - Sau đó git-for-windows PR #5171 đã thêm cách đóng gói bằng cách duyệt theo đường dẫn Git thay vì cách mặc định là duyệt commit
- Trên Microsoft git
git version 2.47.0.vfs.0.2, họ clone lại rồi chạy repack bằng cách mới- Lệnh chạy:
git repack -adf --path-walk - Kết quả: kích thước kho giảm từ 178GB xuống 5GB
- Lệnh chạy:
- Tùy chọn cấu hình mới cũng giúp tạo delta đúng ngay tại thời điểm push
git config --global pack.usePathWalk true
- Các lập trình viên dùng
git version 2.47.0.vfs.0.2có thể chạy repack sau khi clone cục bộ và giảm tốc độ tăng trưởng bằng thuật toán push path-walk mới
Ảnh hưởng tới GitHub, Azure DevOps và Git upstream
- GitHub định kỳ thực hiện repack và Git garbage collection, nhưng cách đóng gói hiện tại của GitHub vẫn chưa tính delta đúng cho các file như
CHANGELOG.md,CHANGELOG.json - Các trường hợp tên file dài có cùng hơn 16 ký tự cuối và thường xuyên thay đổi theo thời gian cũng có thể bị ảnh hưởng
- Ví dụ được đưa ra là các file chuỗi lớn dùng cho i18n
- Azure DevOps mà 1JS đang dùng hiện vẫn chưa thực hiện kiểu repack này, và cũng đang triển khai công việc liên quan để giảm kích thước kho phía máy chủ
- Các thay đổi liên quan cũng dự kiến sẽ được đưa vào Git upstream
Những điểm cần kiểm tra trong monorepo lớn
- Nếu trong monorepo quy mô lớn, các file như
CHANGELOG.mdhoặc các file tên tương đối dài được cập nhật lặp đi lặp lại, thì cần theo dõi các thay đổi liên quan tới path-walk - Lệnh mới
git surveycung cấp các heuristic để đánh giá trạng thái kho lưu trữ- Top Files By Disk Size
- Top Directories By Inflated Size
- Top Files By Inflated Size
- Các heuristic này giúp ước lượng liệu công việc path-walk có ảnh hưởng tới kích thước của kho đó hay không
1 bình luận
Các ý kiến trên Hacker News
Nếu hỏi lệnh
git-surveymới nằm ở đâu thì hiện nó vẫn chưa được đưa vào git.gitTác giả đang dùng fork Git của Microsoft, và lệnh này đã được thêm vào mùa hè này: https://github.com/microsoft/git/pull/667
full-name-hashvàpath-walkcó vẻ cũng chỉ có trong fork hoặc chỉ có ở Git HEADKhông tìm thấy trong trang hướng dẫn hay changelog 2.47
Tôi thắc mắc câu “những người ở châu Âu thậm chí không thể clone vì kích thước repository” nghĩa là gì
Nghe có vẻ lạ khi một repository lớn thì ở châu Âu không tải được còn nơi khác lại tải được, và trước khi thay đổi phía server được áp dụng thì có vẻ họ vẫn sẽ không clone được
Câu “liên tục push lại toàn bộ file” có vẻ bị cắt ngang, và trong lúc đọc bài thì GIF quá gây phân tâm, kể cả ở chế độ đọc cũng vẫn hiện nguyên như vậy
Nếu viết đầy đủ hơn thì có lẽ là: “Có người muốn clone toàn bộ repository vào laptop để dùng khi đi du lịch châu Âu, nhưng lúc đó ổ đĩa đã đầy nên không làm được. Sau đó họ dọn ổ đĩa và clone được”
Tôi không nghĩ ý là châu Âu có vấn đề lớn đến mức không xử lý nổi file 180GB
Đặc biệt nếu người đó dùng VPN, nhất là kiểu mẹo “SSL VPN” tệ hại; phần mềm xử lý độ trễ kém hoặc server có window scaling bị hỏng vẫn còn phổ biến một cách đáng ngạc nhiên
Nếu phần lớn developer ở gần server và dùng kết nối băng thông tốt thì các vấn đề kiểu này thường khó lộ ra
Việc clone từ Azure DevOps hoàn toàn không thể dùng hết băng thông của kết nối gigabit có dây, và vì mất quá nhiều thời gian nên cuối cùng hoặc phía tôi hoặc phía Azure DevOps sẽ dừng lại khiến tôi phải bỏ cuộc
May là tôi làm hầu hết mọi việc bằng shallow clone trong Codespaces nên không phải vấn đề lớn; nếu repository của chúng tôi cũng có mức phình to kích thước tương tự vấn đề của repository 1JS trong bài này thì mong là nó sẽ được sửa
Cập nhật: đây là một sai lầm ngớ ngẩn, và tên tệp không bao gồm toàn bộ đường dẫn
Có vẻ phần giải thích đã bị chìm giữa các ảnh GIF, nhưng 16 ký tự cuối ở đây khác nhau: ví dụ là bạn đổi
repo/packages/foo/CHANGELOG.md, nhưng khi Git chuẩn bị push, nó lại so sánh vớirepo/packages/bar/CHANGELOG.mdđể tạo diffCover letter của API
path-walkcũng đáng xem: https://lore.kernel.org/all/pull.1786.git.1725935335.gitgitg...Ví dụ trong blog không thật sự rõ ràng, nhưng về cơ bản Git lấy mọi phiên bản của mọi tệp trong repository, đưa 16 byte cuối của đường dẫn vào bảng băm, rồi gom những thứ nó kỳ vọng là các phiên bản khác nhau của cùng một tệp để nén delta
foo/CHANGELOG.mdvàbar/CHANGELOG.mdtrong bài chỉ có 13 ký tự nên nếu giữ nguyên ví dụ thì không đúng; cần hình dung các đường dẫn có hậu tố chung dài hơnPhần này đã được sửa bằng tùy chọn
--full-name-hash, nên giờ Git so sánh toàn bộ đường dẫn thay vì 16 byteViệc tăng kích thước cửa sổ được nói đến sau đó gần giống một mẹo né cách nhóm tệp tệ hơn, chứ không phải bản sửa thực sự
Nó vẫn đưa đầu vào rất tệ cho bộ nén rồi né bằng cách dùng lượng bộ nhớ khổng lồ, nên việc trình bày nó như một giải pháp khá gây rối
Phần thật sự thú vị là path-walk API và
--full-name-hashỞ đây tôi thắc mắc “tên tệp” có nghĩa là đường dẫn hay không, hay Git chọn bất kỳ tệp nào có cùng tên để tạo diff
Tôi cũng muốn biết liệu cách chọn tệp khác có mẫu hình nào không
Có thể xem thông điệp commit đầu tiên của PR
--full-name-hash: https://github.com/git-for-windows/git/pull/5157/commits/d5c...Trích ra thì
"/CHANGELOG.json"có 15 ký tự và được công cụ beachball tạo ra; chỉ ký tự cuối của thư mục cha mới có thể phân biệt các phiên bản khác nhau của tệp này, mà ngay cả vậy cũng chỉ phản ánh 2 bit caoNếu ký tự đó là chữ cái thì luôn va chạm; đường dẫn
"/CHANGELOG.md"cũng có vấn đề tương tự, nhưng vẫn còn một chút khả năng khác biệt ở thư mục chaThuật toán nhóm gán trọng số thấp hơn cho từng ký tự khi càng xa phía bên phải của tên:
hash = (hash >> 2) + (c << 24)Hash là 32 bit, và từng ký tự 8 bit của toàn bộ đường dẫn lần lượt được cộng vào 8 bit cao nhất của hash, nhưng vì các bit hash hiện có bị dịch sang phải 2 bit nên cuối cùng chỉ 16 ký tự cuối ảnh hưởng đến hash
Có thể xem điều thực sự xảy ra ở đây: https://go.dev/play/p/JQpdUGXdQs7
Tôi đã chuyển sang Go để so sánh giá trị cuối cùng của
"aaa/CHANGELOG.md"và"zzz/CHANGELOG.md"; nếu thử thay nhiều giá trị vàoaaavàzzz, bạn sẽ thấy chúng ảnh hưởng ít đến giá trị cuối cùng như thế nào16 ký tự cuối của
CHANGELOG.mdchính là toàn bộ tên tệp, và nếu hiểu như vậy thì cũng giải thích được vì sao giải phápfilepathwalkxử lý được vấn đềNhưng nếu thật sự chỉ dựa trên 16 ký tự cuối của tên tệp chứ không phải toàn bộ đường dẫn, thì có vẻ vấn đề này đáng lẽ phải phổ biến hơn nhiều, đặc biệt trong monorepo
Có lẽ các biến meta
foovàbarđã gây nhầm lẫn, và tên package thực tế có khả năng có hậu tố chung, chẳng hạnfirstPkg,secondPkgChỉ riêng
"/CHANGELOG.md"đã là 13 ký tự, nên chỉ cần hậu tố chung 3 ký tự là sẽ có va chạm 16 ký tựTôi thử ngay với
nixpkgs; nếu clone nguyên từ GitHub thì khoảng 5GBTùy chọn đầu tiên được nhắc trong bài,
--window 250, giảm kích thước xuống 1,7GB, còn tùy chọn mới--path-walktrong fork Git của Microsoft kém hiệu quả hơn, tổng cộng thành 1,9GBCả hai đều nhỏ hơn một nửa kích thước ban đầu, nên sẽ rất tốt nếu GitHub có cách chạy những thao tác như vậy; còn tốt hơn nữa nếu người ta bắt đầu host theo cách cho phép tự kiểm soát những thứ này
Bài viết nhắc đến Derrick Stolee, người đã đào sâu các thay đổi cần thiết và phát hành chúng
Nếu bạn quan tâm đến cấu trúc nội bộ của Git, giảm kích thước clone Git ở local và trong CI, Derrick đã viết nhiều bài rất hay trên blog GitHub: https://github.blog/author/dstolee/
Trang cá nhân của anh ấy cũng đáng tham khảo: https://stolee.dev/
Nếu lỡ commit một file nhị phân lớn, có thể xem cách sửa là chạy
filter-branchđể loại bỏ file nhị phân gây vấn đề, hướng dẫn và thiết lập cho mọi người dùng git-lfs cho các file nhị phân, rồi force push và đưa môi trường làm việc của từng người trở lại trạng thái bình thườngKhông lý tưởng, nhưng vẫn tốt hơn là cứ để một file lớn không dùng đến trong Git
filter-branchđáng sợ thì còn có BFG: https://rtyley.github.io/bfg-repo-cleaner/Như người khác đã chỉ ra, vấn đề này liên quan đến các file nhỏ nhưng thay đổi thường xuyên, nên để tiết kiệm dung lượng thì chỉ cần xóa các phiên bản cũ khỏi lịch sử và từ nay dùng LFS là được
Hack Git thì chắc là thú vị, nhưng tự hỏi liệu ngay từ đầu có cách nào để không nhét 2.500 package vào monorepo không
Khi codebase càng lớn, càng khó kỳ vọng mọi người không phát minh lại bánh xe, vì họ không biết là đoạn code đó tồn tại, hoặc sợ rằng khi sửa để xử lý use case của mình thì sẽ làm hỏng các chỗ đang dùng hiện có
Theo cảm nhận thì đi từ 100 module lên 200 module mất ít thời gian hơn từ 50 lên 100
Trông hoàn toàn giống một vấn đề tự tạo ra
Gần đây tôi xem các bài trình bày GitMerge do GitButler đăng và đang theo dõi những tiến triển liên quan đến monorepo và khả năng mở rộng; có rất nhiều thứ hay đến từ Microsoft, GitHub, GitLab
Tôi muốn hiểu rõ hơn việc kiểm tra 16 ký tự cuối và kiểm tra toàn bộ đường dẫn khớp với nén delta, pack index và multi-pack index như thế nào
Thật vui khi thấy Microsoft trực tiếp dùng Azure DevOps
Có vẻ ngày càng nhiều dịch vụ Azure chỉ cung cấp connector native cho GitHub, nên tôi đã tưởng Azure DevOps đang đi theo hướng bị bỏ bê
Có một người hiểu rõ cấu trúc nội bộ của Git đến vậy ở ngay trong tầm tay để có thể hỗ trợ chắc hẳn là một đặc quyền rất tuyệt khi làm dự án kiểu này ở một công ty quy mô như vậy