7 điểm bởi GN⁺ 2024-10-28 | 1 bình luận | Chia sẻ qua WhatsApp
  • 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.mdCHANGELOG.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=250 cũng có hiệu quả, nhưng khi chạy git repack -adf --path-walk trên Microsoft Git fork 2.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 true và lệnh mới git 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 versioned là bản mirror của main
    • 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
  • Nhánh versioned chủ yếu chỉ chứa các commit thêm nội dung vào file CHANGELOG.mdCHANGELOG.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 index xem xét, họ phát hiện khi lấy nhánh versioned thì đ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ới repo/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=250 vớ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
  • 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.2 có 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.md hoặ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 survey cung 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

 
GN⁺ 2024-10-28
Các ý kiến trên Hacker News
  • Nếu hỏi lệnh git-survey mới nằm ở đâu thì hiện nó vẫn chưa được đưa vào git.git
    Tá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-hashpath-walk có vẻ cũng chỉ có trong fork hoặc chỉ có ở Git HEAD
      Khô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

    • Tôi đọc câu đó như một giai thoại
      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
    • Ở đây “châu Âu” rất có thể là cách nói thay cho độ trễ cao, băng thông thấp
      Đặ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
    • Tôi làm việc ở Úc cho một nhóm khác trong nội bộ Microsoft và đang dùng monorepo lớn của Azure DevOps, gần như không thể clone toàn bộ
      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ó lẽ repository nằm ở bờ Tây nước Mỹ, nên mỗi lần clone từ châu Âu đều phải đi qua Đại Tây Dương chăng
    • Cũng có thể là ở một quốc gia có hạ tầng Internet kém phát triển hơn, chẳng hạn như Đức
  • 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ới repo/packages/bar/CHANGELOG.md để tạo diff

    • Derrick giải thích rõ hơn trong cover letter này: https://lore.kernel.org/git/pull.1785.git.1725890210.gitgitg...
      Cover letter của API path-walk cũ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.mdbar/CHANGELOG.md trong 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ơn
      Phầ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 byte
      Việ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--full-name-hash
    • Tôi đã mong bài viết giải thích chính xác chuyện gì đã xảy ra, thay vì phần kể chuyện “màu mè”
      Ở đâ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
    • Đúng là toàn bộ đường dẫn được xét đến
      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 cao
      Nế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 cha
      Thuậ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""zzz/CHANGELOG.md"; nếu thử thay nhiều giá trị vào aaazzz, bạn sẽ thấy chúng ảnh hưởng ít đến giá trị cuối cùng như thế nào
    • Tên tệp không nhất thiết bao gồm toàn bộ đường dẫn
      16 ký tự cuối của CHANGELOG.md chí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áp filepathwalk xử 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
    • Tôi cũng thấy chỗ đó khó chịu
      Có lẽ các biến meta foobar đã 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ạn firstPkg, secondPkg
      Chỉ 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 5GB
    Tù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-walk trong fork Git của Microsoft kém hiệu quả hơn, tổng cộng thành 1,9GB
    Cả 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ường
    Khô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

    • Với những ai thấy 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
    • Vấn đề cốt lõi không phải là file nhị phân được đưa vào một lần rồi không đổi, mà là các file nhị phân nhỏ thay đổi thường xuyên
    • Cách được khuyến nghị hiện nay là filter-repo: https://github.com/newren/git-filter-repo
    • Đổ lỗi cho Linus thì dễ hơn
  • 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

    • Số dòng code nhìn chung có xu hướng tăng theo cấp số nhân
      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
    • Vậy thì có thể có riêng 2.500 repository Git và gánh toàn bộ overhead liên quan
    • Đây đúng là suy nghĩ đầu tiên của tôi
      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

    • Nếu việc đó tốt cho mã nguồn mở đến vậy, tôi tò mò liệu họ có định mở merge request để đưa lệnh Git tùy chỉnh đó trở lại Git chính thức không
  • 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

    • Quan hệ gần gũi với một tổ chức như GitHub chắc chắn có ích, nhưng bất kỳ tổ chức nào có nhiều lập trình viên đến mức đó cũng có thể biện minh được chi phí thuê các consultant chuyên môn hóa cao để giải quyết những vấn đề gần như ngách như thế này