- Đây là bài hồi ký do chính Jason Evans, người dẫn dắt việc phát triển jemalloc, chắp bút. Bài viết nhìn lại hành trình phát triển jemalloc kéo dài khoảng 20 năm qua 5 giai đoạn, ghi lại một cách chân thành những thành công và thất bại, giới hạn của dự án mã nguồn mở, cùng quá trình suy tàn do các thay đổi nội bộ trong doanh nghiệp
- Trình cấp phát bộ nhớ jemalloc bắt đầu từ năm 2004 và được sử dụng rộng rãi trong khoảng 20 năm, nhưng gần đây việc phát triển chính thức đã dừng lại do những thay đổi nội bộ tại Meta
- Qua nhiều giai đoạn như tích hợp ban đầu vào FreeBSD, giải quyết vấn đề hiệu năng của Firefox, và được Facebook triển khai ở quy mô lớn, dự án đã tích lũy nhiều kinh nghiệm tối ưu hiệu năng và porting
- Trong quá trình vượt qua nhiều thách thức như vấn đề phân mảnh lưu trữ và vấn đề khả năng mở rộng, dự án đã bổ sung nhiều tính năng như cải thiện hiệu năng, thu thập thống kê và hạ tầng kiểm thử
- Cùng với sự thay đổi văn hóa doanh nghiệp tại Meta, đầu tư kỹ thuật suy giảm và thiếu nhân sự bảo trì cốt lõi đã khiến việc phát triển dài hạn bị dừng lại
- Việc loại bỏ Valgrind và thiếu phản hồi từ người dùng bên ngoài đã trở thành những giới hạn mang tính cấu trúc trong hệ sinh thái mã nguồn mở
- Trong tương lai, khả năng phát triển mới thông qua fork vẫn còn để ngỏ, nhưng việc phát triển chính thức ở nhánh chính nhiều khả năng sẽ không còn tiếp tục
Tổng quan về jemalloc
- jemalloc là một trình cấp phát bộ nhớ mã nguồn mở được hình thành lần đầu vào năm 2004, được phát triển với mục tiêu cải thiện hiệu năng và khả năng mở rộng, và đã được sử dụng tích cực suốt 20 năm trong các dự án mã nguồn mở lớn cũng như hạ tầng của các công ty công nghệ lớn
- Việc phát triển chính hiện đã dừng lại, nên trong tương lai dự kiến sẽ được duy trì thông qua fork hoặc ở cấp độ từng tổ chức riêng lẻ
Phase 0: Lyken
- Năm 2004, trong quá trình phát triển ngôn ngữ lập trình phục vụ tính toán khoa học mang tên Lyken, dự án bắt đầu từ một trình cấp phát bộ nhớ thủ công
- Trình cấp phát bên trong Lyken đạt mức hoàn thiện về mặt tính năng vào tháng 5 năm 2005
- Sau đó trình cấp phát được tích hợp vào FreeBSD, và bị gỡ khỏi Lyken, chỉ để lại một lớp bọc mỏng quanh trình cấp phát hệ thống
- Lý do gỡ bỏ là vì sau khi tích hợp vào FreeBSD, những điểm còn thiếu của trình cấp phát hệ thống, chẳng hạn như theo dõi hạn ngạch cấp phát theo luồng, mới trở nên rõ ràng
- Thú vị là về sau jemalloc lại được bổ sung chính tính năng thu thập thống kê từng cần đến ở thời kỳ Lyken
Phase 1: FreeBSD
- Năm 2005, khi máy tính đa bộ xử lý dần trở nên phổ biến, FreeBSD đang dùng phkmalloc, nhưng nó không phù hợp với môi trường nhiều luồng chạy song song
- Trình cấp phát của Lyken cho thấy lợi thế rõ rệt về khả năng mở rộng, nên khi được tích hợp vào FreeBSD, nó nhanh chóng mang tên jemalloc
- Tuy nhiên, các ứng dụng như KDE gặp phải vấn đề phân mảnh nghiêm trọng, khiến người ta nghi ngờ khả năng tồn tại của nó
- Nguyên nhân phân mảnh là do cách cấp phát không gian hợp nhất không phân chia theo kích thước; sau quá trình nghiên cứu, cấu trúc đã được thay đổi mạnh sang thuật toán tách vùng theo từng kích thước
- Nội dung của quá trình này đã được ghi lại trong bài báo BSDCan năm 2006
Phase 1.5: Firefox
- Năm 2007, trước khi Mozilla Firefox 3 phát hành, phân mảnh bộ nhớ là một vấn đề lớn trên môi trường Windows
- Việc port jemalloc sang Linux khá dễ, nhưng Windows thì phức tạp hơn nhiều
- Mozilla đã fork jemalloc từ phiên bản nằm trong libc của FreeBSD để cải thiện tính di động và khả năng tương thích
- Theo thời gian, Mozilla đóng góp nhiều cải tiến trở lại upstream của jemalloc, nhưng bản fork luôn cho hiệu năng tốt hơn
- Không rõ điều này là do vấn đề hồi quy hiệu năng hay do các tối ưu hóa được tinh chỉnh cho môi trường cụ thể
Phase 2: Facebook
- Khi gia nhập Facebook năm 2009, trở ngại lớn nhất của jemalloc là thiếu công cụ như profiling và phát hiện rò rỉ bộ nhớ
- Để khắc phục điều này, tính năng heap profiling tương thích pprof đã được đưa vào jemalloc 1.0.0
- Sau khi quá trình phát triển được chuyển sang Github, nhiều cải tiến đã được thực hiện cùng với người dùng và các cộng tác viên bên ngoài như hạ tầng kiểm thử, hỗ trợ Valgrind, thống kê JSON, và cơ chế quản lý trang mới
- Về nội bộ, hệ thống telemetry khổng lồ của Facebook đã hỗ trợ rất lớn cho việc tối ưu hiệu năng và ngăn ngừa hồi quy
- 3.x: đưa vào hạ tầng kiểm thử và hỗ trợ Valgrind
- 4.x: bổ sung decay-based purging, thống kê JSON
- 5.x: chuyển từ thiết kế dựa trên chunk sang extent, tạo nền tảng để tận dụng huge page 2MiB
- Phân tích hiệu năng dựa trên telemetry bên trong Facebook đóng vai trò quyết định trong tối ưu hóa
- Việc gỡ hỗ trợ Valgrind ở phiên bản 5.0.0 được quyết định vì nội bộ không sử dụng, nhưng đã vấp phải phản ứng mạnh từ bên ngoài, bao gồm cả các nhà phát triển Rust
- Sau đó, do thay đổi tổ chức ở Facebook/Meta, quy mô đội ngũ jemalloc bị thu hẹp và chiến lược kinh doanh chuyển sang ưu tiên hiệu quả hơn là đầu tư vào công nghệ cốt lõi
- Vì thế, việc phát triển các tính năng lớn như Huge Page Allocation bị chững lại, một số phần việc không được hoàn tất
- Sau khi Evans rời đi vào năm 2017, Qi Wang tiếp tục duy trì việc phát triển trong nhiều năm
- Dù sau khi chuyển giao lãnh đạo, nhiều người đóng góp vẫn tiếp tục duy trì dự án, nhưng đã không còn người quản lý tầm nhìn dài hạn
Phase 4: Stasis (trạng thái đình trệ)
- Hiện tại, việc phát triển upstream của jemalloc đã kết thúc, và Meta cũng đang theo đuổi hướng đi riêng theo nhu cầu nội bộ
- Nợ kỹ thuật của codebase hiện tại đã rất lớn, nên cần một cuộc refactor quy mô lớn trước tiên
- Các yêu cầu của Facebook/Meta và người dùng bên ngoài không còn trùng khớp nữa
- Nếu muốn khởi động lại việc phát triển trong tương lai thì sẽ phải dành hàng trăm giờ để xử lý nợ kỹ thuật trước, còn tác giả thì không còn động lực
- Dựa trên nhánh
dev hoặc phiên bản 5.3.0, fork từ bên ngoài vẫn khả thi, nên bất kỳ lúc nào cũng có thể xuất hiện một dự án mới dựa trên fork
Hồi tưởng và bài học
- Xung đột phát sinh từ việc gỡ hỗ trợ Valgrind bắt nguồn từ thiếu hiểu biết về các trường hợp sử dụng bên ngoài
- Mãi 2 năm sau mới biết rằng jemalloc đang được sử dụng trên Android
- Dự án được mở hoàn toàn công khai trên GitHub, nhưng những người đóng góp cốt lõi từ các tổ chức bên ngoài không thể duy trì lâu dài
- Từ Mike Hommey của Firefox đến nỗ lực chuyển sang CMake đều dở dang
- Theo kinh nghiệm thực tế, chỉ đơn thuần công khai dự án không có nghĩa là nó sẽ trở thành một dự án độc lập bền vững
- Bài viết nhấn mạnh rằng mã nguồn mở không thể được duy trì chỉ bằng việc công khai; việc nuôi dưỡng người đóng góp cốt lõi và xây dựng quản trị dự án là điều thiết yếu
Lời kết
- jemalloc là một trải nghiệm đặc biệt ngay cả với tác giả, người đã là người ủng hộ garbage collection trong hơn 25 năm
- Dù hiện đang tập trung trở lại vào việc phát triển các hệ thống garbage collection, tác giả vẫn gửi lời cảm ơn sâu sắc tới tất cả những ai đã cộng tác cùng jemalloc
2 bình luận
Cũng có bản dịch của toàn bộ bài viết.
https://rosettalens.com/s/ko/jemalloc-postmortem
Ý kiến trên Hacker News
Tôi hiểu quyết định lưu trữ kho upstream. Trước khi rời Meta, nhóm Jemalloc của chúng tôi không đủ nguồn lực để phản hồi đủ mọi issue được đưa lên GitHub (ví dụ, đã từng có người mở issue vì test không pass trên môi trường Itanium, chuyện đó với tôi hơi buồn cười). Dù vậy, nhìn tình hình này vẫn thấy khá tiếc. Đến giờ tôi vẫn thấy jemalloc là lựa chọn có hiệu năng tốt nhất mà vẫn dễ dùng trong số các triển khai malloc đa dụng. TCMalloc cũng rất xuất sắc, nhưng nếu không dùng bazel thì thật sự rất khó dùng (gần đây bazel 7.4.0 đã thêm
cc_static_library, giúp xuất ra thư viện tĩnh dễ hơn một chút, nhưng vấn đề đó vẫn còn rất đáng kể). Tôi đang định hỏi Qi xem có thể làm thêm một bản phát hành 6.0 cuối cùng trước khi lưu trữ lại kho hay không. Nếu là bản phát hành cuối, có lẽ cũng nên hiện đại hóa đôi chút các thiết lập mặc định. Chẳng hạn, một cải tiến lớn là tắt mặc định thiết lậpcache obliviouscó tên gọi gây hiểu nhầm, để tránh việc size-class 16 KiB bị phình vô ích thành 20 KiB. Không phải tôi muốn chỉ trích lựa chọn trước đây (tức quyết định ban đầu của Jason), vì khi tôi trao đổi chuyện này với Qi và David thì việc TLB associativity nhìn chung ngày đó thấp hơn nhiều so với hiện nay là một lý do hoàn toàn hợp lý. Theo cùng logic đó, tăngpage sizemặc định từ 4 KiB lên một giá trị lớn hơn như 16 KiB cũng sẽ là thay đổi tốt. Khi đó ngưỡng của các size-class lớn (điểm chuyển từ việc phân nhiều allocation vào slab sang cấp phát mỗi cái vào vùng riêng khi vượt một kích thước nhất định) sẽ tăng từ 16 KiB lên 64 KiB, nên tác động khá lớn. Trước khi rời Meta, đây là thay đổi cuối cùng tôi từng cân nhắc áp dụng cho một dịch vụ nội bộ lớn; đó là một tối ưu giúp giảm vài phần trăm CPU đổi lại mức tăng bộ nhớ nhỏ do phân mảnh RAM. Ngoài ra còn vài điểm khác tôi muốn thay đổi nữa (ví dụ đổi mặc địnhmetadata_thptừdisabledsangauto, đổi cách sizing extent của slabs từ bội số chính xác của kích thước trang sang hướng chấp nhận lãng phí khoảng 1% để giảm phân mảnh, v.v.). Dù vậy, các thiết lập nêu trên có lẽ sẽ là thay đổi lớn nhấtChính tôi là người đã mở issue về việc bộ test trên Itanium bị lỗi
Chính những câu chuyện thực tế và góc nhìn của người trong cuộc như thế này là lý do khiến tôi vẫn quay lại Hacker News. Tôi tò mò tại sao TCMalloc lại khó dùng nếu không có bazel (tôi hỏi hoàn toàn vì thật sự muốn biết)
Tôi mong những kiến thức nội bộ quan trọng như vậy sẽ được công bố thành tài liệu chính thức hoặc bài blog chi tiết hơn. Hiện tại tài liệu chính thức có vẻ quá sơ sài. Sẽ rất tốt nếu những kinh nghiệm tích lũy từ rất nhiều công việc đã làm trong Meta được chia sẻ trước khi bị thời gian làm mai một
Hơi tiếc là dù phần mềm rất xuất sắc, quá trình build và tích hợp lại phức tạp nên không được tận dụng đúng mức
Ông có nhắc rằng “nhóm Jemalloc không đủ khả năng xử lý các issue ngẫu nhiên trên GitHub”, nên tôi khá tò mò. Bối cảnh nào khiến việc quản lý issue vẫn không trơn tru dù Meta có vẻ có đủ người cho dự án đó? Nếu tôi đang hiểu sai tình hình thì mong được đính chính
Tôi muốn nói về việc công trình của Jason đã ảnh hưởng lớn đến công việc của chúng tôi như thế nào. Công ty chúng tôi khá lớn, mỗi ngày xử lý hàng trăm triệu ảnh/video. Trong những năm đầu, chúng tôi khổ sở vô cùng vì vấn đề phân mảnh bộ nhớ. Rồi một ngày chúng tôi đưa jemalloc vào, chỉ thay đúng hai dòng trong Dockerfile mà mọi vấn đề đều biến mất. Giờ đây công ty đã đạt doanh thu hàng chục tỷ, và chúng tôi dùng jemalloc trong mọi dịch vụ lẫn Dockerfile. Tôi thật sự muốn gửi lời cảm ơn từ tận đáy lòng
Trên thực tế, nhiều dịch vụ xử lý ảnh viết bằng golang có khuyến nghị hoặc đang sử dụng jemalloc. Với 3 dịch vụ đứng đầu trong chủ đề resize-images (tính đến 2025-06-13), imaginary dùng nó trong Dockerfile như thế này, imgproxy cũng được thảo luận ngay trong repo imaginary với tham chiếu đến tài liệu đã được lưu trữ. imagor cũng đang dùng jemalloc ở đây
Tôi hỏi hoàn toàn vì tò mò thật lòng thôi (không hề mỉa mai): anh có quyên góp gì không? Tôi cứ nghĩ thể hiện lòng biết ơn bằng tiền có lẽ là cách cảm ơn tốt nhất
Về ý kiến rằng “jemalloc bị loại khỏi binary Rust nhanh hơn dự kiến”, tôi muốn chia sẻ rằng issue đó thực ra chỉ là một trong nhiều nguyên nhân. Có thể xem bối cảnh liên quan trong bình luận này. Và jemalloc bị loại bỏ hoàn toàn là tận 2 năm sau khi issue đó lần đầu được nêu ra (tham khảo: PR này)
Tôi đã thành thói quen dùng jemalloc trong mọi game engine mình làm suốt nhiều năm qua. Trên win32 nó nhanh hơn allocator mặc định rất nhiều, và việc dùng cùng một allocator trên mọi nền tảng cũng là lợi thế lớn. Tôi biết đến jemalloc khi thấy nó được tích hợp trong FreeBSD, và từ đó đến nay chỉ dùng mỗi jemalloc. Tôi thấy tự hào khi nghĩ rằng nhờ jemalloc mà rất nhiều người chơi game đã có những phút giây vui vẻ
Bài viết rất hay — tôi tò mò không biết Facebook (giờ là Meta) có còn dùng jemalloc nữa không, hay chỉ còn ở mức bảo trì. Cũng tự hỏi liệu họ có thể đã chuyển sang tcmalloc hoặc allocator khác không. Có câu rằng “trong hạ tầng kỹ thuật của Facebook, trọng tâm đã dịch chuyển từ đầu tư vào công nghệ lõi sang ROI”
Tôi rời Meta gần 2 năm rồi (và đoán là mọi thứ vẫn chưa thay đổi nhiều), nhưng jemalloc vẫn đang được liên kết tĩnh vào mọi binary trong Meta. Còn về chuyện có thể dễ dàng chuyển sang tcmalloc hay allocator khác không, câu trả lời là khó hơn tưởng tượng nhiều, vì jemalloc đã ăn sâu vào hệ sinh thái nội bộ của công ty. Từ phần telemetry plumbing của Strobelight, đến hàng loạt extension dùng riêng cho jemalloc (ví dụ các manual arena dùng custom extent hook trực tiếp), rồi cả quá trình tiến hóa khiến phần lớn ứng dụng thực tế đã được thiết kế để tận dụng tối đa đặc tính của jemalloc — tất cả đều đan xen vào nhau
Thay đổi lớn gần đây là toàn bộ các maintainer lâu năm của jemalloc đều đã rời đi. Nhưng ngược lại, Facebook dường như lại bắt đầu quan tâm đến dự án này nhiều hơn trước, và sau một số issue gây tranh cãi gần đây, tôi khá lạc quan rằng họ có thể tiến tới theo hướng cân bằng hơn giữa Qi, Jason và cả người dùng bên ngoài
Meta vẫn đang tích cực phát triển bản fork riêng của mình tại đây
Tôi thấy vinh dự vì đã được đồng hành cùng công việc có ảnh hưởng lớn này trong một chặng đường dài, từ Firefox đến Facebook
Đúng lúc này tôi cũng muốn để lại lời cảm ơn ở đây. Tôi không ngờ bài này lại lên hôm nay, nhưng được góp một phần nhỏ trong hành trình lớn đó cũng đã là niềm vinh dự. Tôi cũng muốn cảm ơn @je, qi, david cùng các cộng tác viên của thời đó và về sau
Tôi nghĩ sự lãnh đạo của anh trong việc thúc đẩy Facebook tiếp tục đầu tư vào công nghệ lõi đã tạo ra thành quả lớn nhất có thể. Đó là nền tảng giúp những đổi mới như GraphQL, PyTorch, React trở nên khả thi
Câu trích FTA rằng “khi con người bị đặt trước những lựa chọn bất khả thi, họ chỉ còn 1) đưa ra quyết định tồi dưới áp lực cực lớn, 2) phục tùng dưới áp lực cực lớn, hoặc 3) tìm đường lách qua” nghe thật khó tưởng tượng nếu đó là bầu không khí nơi làm việc
Tôi nghĩ jemalloc là allocator duy nhất trên macOS có thể override malloc/free trơn tru theo kiểu LD_PRELOAD (ít nhất là vào khoảng năm 2020). Nó có thể chen vào làm allocator mặc định khá dễ nhờ cơ chế zone-based, đồng thời đáp ứng tốt các yêu cầu allocator rất đặc thù của Apple. Những allocator bên thứ ba khác thường hay vấp ở các yêu cầu đó
Tuy nhiên, cách này chỉ hoạt động nếu allocator hệ thống của macOS không thay đổi cấu trúc nội bộ, nên hình như đã có khoảng hai lần Apple thay đổi làm nó bị hỏng
Tôi nghĩ mimalloc cũng có thể hoạt động theo cách tương tự, nhưng không dám chắc
Với tôi jemalloc dường như cải thiện hơn malloc của glibc ở mọi mặt, và qua benchmark thì lúc nào cũng cho hiệu năng tốt hơn, nên với tư cách người ngoài tôi luôn thắc mắc vì sao nó không phải allocator mặc định
Trên FreeBSD thì jemalloc đã là mặc định rồi. Nếu muốn đổi malloc thì có khi thay luôn libc bằng FreeBSD libc còn dễ hơn, mà đã vậy thì chuyển cả kernel sang FreeBSD cũng là bước tự nhiên. Lúc công ty chúng tôi sáp nhập vào Facebook, tôi từng hào hứng giới thiệu jemalloc cho một đồng nghiệp, nhưng họ vốn đã ở FreeBSD nên xem đó là điều quá hiển nhiên
Tôi không phải kỹ sư chuyên về allocator nên đây không phải ý kiến chuyên môn, nhưng trước đây tôi từng nói chuyện với một kỹ sư quản lý allocator của hệ điều hành. Theo lời anh ấy, allocator tùy biến thường giúp một tiến trình đơn lẻ có lợi thế áp đảo trong cấp phát bộ nhớ, nhưng lại làm giảm tính công bằng phân bổ của toàn hệ thống. Từ góc độ allocator hệ thống, khi từng tiến trình có mô hình hành vi riêng thì rất khó tối ưu tổng thể. Vì vậy trong đa số môi trường dịch vụ, jemalloc thường được khuyến nghị khi tiền đề là “điều duy nhất quan trọng là tiến trình của tôi lúc này”
Tôi không nghĩ có lý do kỹ thuật nào khiến jemalloc không thể là allocator mặc định. Thực tế trên FreeBSD nó chính là mặc định rồi (bài viết cũng có nhắc). Theo hiểu biết của tôi, vấn đề này mang tính chính trị nhiều hơn là kỹ thuật
jemalloc đã được kiểm chứng trong môi trường production quy mô lớn, giấy phép lại rất thoáng, hiệu năng cũng đã được chứng minh. Vậy rốt cuộc ai được lợi gì từ việc cứ bám vào malloc của glibc ngoài “sự thuần khiết về ý thức hệ” và quán tính di sản? Tôi thật sự không hiểu tại sao người ta vẫn vin vào “tính tương thích” để biện minh
Trước đây có vấn đề lớn là các allocator thay thế thường không bao giờ trả lại bộ nhớ đã free về cho OS mà cứ giữ dưới dạng dirty page mãi (việc này cuối cùng cũng đã được cải thiện, nhưng đó là ví dụ điển hình cho thấy khác biệt về ưu tiên của allocator). Và thực ra phần lớn tiến trình chỉ chạy một luồng chính hoặc vài luồng gần như nhàn rỗi. Các allocator tối ưu cho đa luồng trong môi trường như vậy có thể là quá mức cần thiết và chỉ tăng chi phí. Nhân tiện cũng muốn nói rằng đa số người không để ý việc chi phí để kernel zero trang nhớ và chi phí để tiến trình người dùng tự zero chúng cho mục đích tái sử dụng nội bộ thực chất không khác nhau đáng kể
Tôi ước gì họ thêm hẳn liên kết bài blog vào kho GitHub. Tôi nghĩ việc những người ghé repo sau này biết được bối cảnh đó và tham khảo là rất quan trọng