- Phân tích giới hạn hiệu năng của Bundler, đồng thời so sánh lý do trình quản lý gói uv của Python lại nhanh
- Tốc độ của uv không đến từ Rust, mà nhờ tải xuống song song, bộ nhớ đệm toàn cục, xử lý phụ thuộc dựa trên metadata và các lựa chọn thiết kế mang tính cấu trúc
- Bundler gộp quá trình tải xuống và cài đặt, nên bị hạn chế trong xử lý song song; nếu tách hai phần này ra thì có thể cải thiện đáng kể
- Có thể giảm trùng lặp giữa RubyGems và Bundler bằng tích hợp bộ nhớ đệm toàn cục, cài đặt bằng hardlink, tích hợp bộ giải PubGrub
- Ngay cả khi không viết lại bằng ngôn ngữ khác, phần lớn cải thiện hiệu năng vẫn có thể đạt được trong mã Ruby, đủ để tiệm cận tốc độ của uv
So sánh hiệu năng giữa Bundler và uv
- Từ câu hỏi được đặt ra tại RailsWorld: “Vì sao Bundler không nhanh bằng uv?”, tác giả đã điều tra các điểm nghẽn hiệu năng của Bundler
- Tác giả khẳng định Bundler có thể đạt tốc độ ngang ngửa uv, và nhấn mạnh rằng khác biệt về hiệu năng là vấn đề thiết kế chứ không phải ngôn ngữ
- Trích dẫn bài viết “How uv got so fast” của Andrew Nesbitt, rồi phân tích liệu các kỹ thuật tối ưu cốt lõi của uv có thể áp dụng cho Bundler hay không
Có cần viết lại bằng Rust không?
- Đúng là uv được viết bằng Rust, nhưng nguyên nhân cốt lõi của tốc độ không nằm ở bản thân Rust
- Nếu loại bỏ được các điểm nghẽn của Bundler để rồi “viết lại bằng Rust” trở thành phương án cải thiện duy nhất còn lại, thì đó đã là một thành công
- Việc viết lại bằng Rust mang lại sự tự do để thử nghiệm các thiết kế mới mà không bị ràng buộc bởi tương thích cũ, nhưng không phải điều kiện bắt buộc
Các điểm nghẽn cấu trúc của Bundler
- Bundler gộp việc tải gem và cài đặt vào cùng một phương thức, khiến việc tải song song trở nên bất khả thi
- Trong ví dụ mã, phương thức
install thực thi liên tiếp fetch_gem_if_not_cached và install
- Vì vậy, các gem có quan hệ phụ thuộc (
a -> b -> c) chỉ có thể được cài đặt tuần tự
- Kết quả thử nghiệm cho thấy, khi có phụ thuộc thì mất hơn 9 giây, còn các gem độc lập (
d, e, f) thì hoàn tất trong vòng chưa đến 4 giây nhờ tải song song
- Nếu tách riêng tải xuống và cài đặt, vẫn có thể giữ nguyên quy tắc phụ thuộc mà đồng thời cho phép xử lý song song
- Đề xuất tách thành bốn bước (tải xuống → giải nén → biên dịch → cài đặt)
- Với gem Ruby thuần, có thể nới lỏng thứ tự cài đặt phụ thuộc để tăng tốc thêm
Tối ưu bộ nhớ đệm và cài đặt
- Cách làm bộ nhớ đệm toàn cục và cài đặt bằng hardlink của uv cũng có thể áp dụng cho Bundler
- Hiện tại Bundler và RubyGems dùng bộ nhớ đệm riêng theo từng phiên bản Ruby
- Cần hợp nhất thành bộ nhớ đệm dùng chung dựa trên
$XDG_CACHE_HOME
- Sau khi hợp nhất bộ nhớ đệm, có thể áp dụng cài đặt bằng hardlink
- Bundler đã dùng bộ giải phụ thuộc PubGrub, nhưng RubyGems vẫn còn dùng molinillo
- Hợp nhất bộ giải của hai hệ thống là chìa khóa để xử lý nợ kỹ thuật
Khả năng áp dụng các yếu tố tối ưu liên quan đến Rust
- Giải tuần tự hóa zero-copy có thể áp dụng phần nào ở giai đoạn phân tích YAML của RubyGems
- GVL (Global VM Lock) của Ruby không phải là rào cản lớn với các tác vụ thiên về IO
- Xử lý IO và ZLIB đều nhả GVL, nên có thể chạy song song
- Tuy nhiên, khi ghi nhiều tệp nhỏ, overhead quản lý GVL lại trở thành yếu tố làm giảm hiệu năng
- Hiện đang có các nỗ lực cải thiện điểm này bên trong Ruby
- Tối ưu so sánh phiên bản: uv mã hóa phiên bản thành số nguyên
u64 để tăng tốc so sánh
- Trong Ruby cũng có thể chuyển
Gem::Version sang dạng số nguyên để cải thiện hiệu năng bộ giải
- Đã từng có nỗ lực refactor liên quan, nhưng bị hoãn do vấn đề tương thích ngược
Kết luận và kế hoạch tiếp theo
- Tốc độ của uv đến từ thiết kế loại bỏ các công việc không cần thiết, hơn là từ ngôn ngữ lập trình; Bundler cũng có thể cải thiện theo cùng hướng
- RubyGems và Bundler vốn đã có cấu trúc quản lý gói hiện đại, nên việc đạt tốc độ cỡ uv là mục tiêu thực tế
- Thách thức lớn nhất là duy trì tương thích với mã legacy
- Ngay cả khi không viết lại bằng Rust, 99% mức cải thiện hiệu năng vẫn có thể đạt được trong mã Ruby, còn 1% còn lại là không đáng kể
- Trong bài viết tiếp theo, tác giả dự định sẽ bàn về profiling thực tế và các nguyên nhân cụ thể gây nghẽn của Bundler và RubyGems
2 bình luận
Nói thì rẻ. Đưa tôi xem code!
Ý kiến trên Hacker News
Tôi không rành cấu trúc nội bộ của Bundler, nhưng tôi nghĩ cải tiến lớn nhất sẽ là áp dụng thiết kế cache của uv
Một phần cốt lõi khiến uv nhanh nằm ở cấu trúc cache, và điều này có thể được sao chép sang các ngôn ngữ hay hệ sinh thái khác
Tuy nhiên, việc bỏ qua giới hạn trên của
requires-pythonkhông phải vì hiệu năng mà là để giải quyết phụ thuộc tốt hơnVí dụ, một dự án yêu cầu Python 3.8 trở lên nhưng có phụ thuộc nào đó đặt ràng buộc
<4, thì trên Python 4 sẽ không cài đượcuv giải quyết cho mọi phiên bản được hỗ trợ nên việc bỏ qua giới hạn trên gần như không tiết kiệm thêm thời gian
Có thể xem thảo luận liên quan trên diễn đàn Python Discuss
Sau PEP 658, Simple Repository API của Python cung cấp trực tiếp metadata, và RubyGems.org cũng đã cung cấp thông tin tương tự
Nhưng phải giải nén gem ra mới biết được nó có native extension hay không
Vì vậy có đề xuất rằng nếu thêm trực tiếp thông tin này vào metadata của RubyGems.org thì có thể song song hóa hoàn toàn cây cài đặt phụ thuộc
Khi còn làm ở RubyGems.org, tôi nhớ metadata được trích xuất theo từng phiên bản
Cần xử lý lại gemspec của các phiên bản cũ, mà điều này có thể trở thành một thay đổi metadata rủi ro
Vì thế có lẽ khó áp dụng cho các phiên bản cũ, nhưng về sau thì có thể cải thiện để biết thứ tự cài đặt mà không cần unpack
Tôi thích việc Aaron tập trung vào cải tiến thuật toán thực chất thay vì viết lại Bundler bằng Rust
Môi trường lộn xộn với đủ loại công cụ quản lý phiên bản và các bản Ruby trộn lẫn với nhau thực sự rất khó chịu
Tôi nghĩ vấn đề không chỉ là tốc độ mà còn là quyền kiểm soát và định hướng của hệ sinh thái
Ruby đã tập trung vào tốc độ suốt 10 năm qua, nhưng chất lượng tài liệu và việc quản lý cộng đồng mới là thứ quan trọng hơn
Đã đến lúc phải nghiêm túc suy nghĩ vì sao ngôn ngữ này suy giảm và thúc đẩy nhiều ý tưởng đa dạng hơn
Có bài liên quan gần đây là How uv got so fast (tháng 12 năm 2025, 457 bình luận)
Muốn làm RubyGems nhanh hơn thì mấu chốt là đưa danh sách file của từng gem vào registry/cơ sở dữ liệu
Làm vậy sẽ không cần quét file system mỗi lần
requireNếu chỉnh sửa gem trực tiếp thì phải hash lại metadata, nhưng dù sao việc sửa thủ công cũng vốn không được khuyến khích
Giờ thì nó có thể đã lỗi thời, nhưng vẫn là một mini project mà tôi rất quý
Mã nguồn: fastup
Vấn đề thực sự là
$LOAD_PATHthêm mọi gem vào và gây ra bùng nổ tổ hợp trong cấu trúcViệc có nhiều dự án cache tồn tại chính là bằng chứng đây là vấn đề có thật
Trước đây ứng dụng từng mất vài phút để khởi động, nhưng tôi đã thao tác load path để rút xuống còn tính bằng phút
Trước đây tôi từng đề xuất tích hợp bootsnap vào bundler nhưng đã bị từ chối
Phần giải thích về cấu trúc của RubyGems khá thú vị
gem là một file tar, bên trong có YAML GemSpec khai báo các phụ thuộc
RubyGems.org cung cấp thông tin này qua API nên có thể kiểm tra phụ thuộc mà không cần eval
Tuy nhiên YAML là một định dạng có hiệu quả phân tích không cao, nên các lựa chọn như JSON hoặc protobuf có thể tốt hơn
Dù vậy, nếu gemserver đã trả về thông tin phụ thuộc thì có lẽ đây không phải vấn đề lớn
Ví dụ: một cấu trúc chỉ gồm phiên bản, phụ thuộc và hash
Đây cũng là lý do uv nhanh — có thể tính toán phụ thuộc mà không cần tải package xuống
Trước đây tôi từng làm một video prototype về cách cài gem nên hoạt động như thế nào
how_gems_should_be.mov
fibers của Ruby (hoặc thư viện Async) thường bị đánh giá quá cao
Cũng như thread, các vấn đề điều phối ở cấp cao hơn như connection pool vẫn còn nguyên
Nhưng nếu xử lý bất đồng bộ các tác vụ cài đặt thiên về IO thì vẫn có thể thấy cải thiện hiệu năng đáng kể
Hiện đang xem xét ý tưởng “cache toàn cục được mọi instance bundler cùng chia sẻ”
Về lâu dài có vẻ sẽ mang lại lợi ích lớn, nhưng vẫn đang cân nhắc xem có độ phức tạp ẩn nào không
Issue liên quan: rubygems #7249
Ruby không phải bên đầu tiên giải bài toán này, nên giờ là lúc tận dụng thành quả đó
Nguyên tắc cơ bản của tối ưu hóa rất đơn giản — không làm gì cả là nhanh nhất
Không làm những việc không cần thiết ngay từ đầu mới là tối ưu hóa thật sự