Thường không phải leak mà là memory bloat do phân mảnh (fragmentation). Leak nghĩa là số object sống tăng mãi; bloat nghĩa là object đã được GC thu hồi nhưng bộ cấp phát của hệ điều hành không trả lại RAM cho OS.
Phân biệt trước khi sửa:
GC.start
ObjectSpace.count_objects # live object counts — growing => real leak
GC.stat[:heap_live_slots]Nếu heap_live_slots ổn định nhưng RSS vẫn tăng → bloat, không phải leak.
Nguyên nhân bloat phổ biến nhất: allocator mặc định của glibc trên Linux tạo nhiều arena cho mỗi thread. App multi-thread (Puma nhiều thread, Sidekiq concurrency cao) sinh ra rất nhiều arena, mỗi arena giữ phần bộ nhớ riêng và không tái sử dụng chéo được.
Cách xử lý theo thứ tự chi phí:
1. Đặt MALLOC_ARENA_MAX=2 — một biến môi trường, giảm phân mảnh đáng kể, không cần đổi code.
2. Chuyển sang jemalloc (build Ruby với --with-jemalloc, hoặc dùng bản dựng sẵn). Các báo cáo thực tế cho thấy process Sidekiq giảm từ 1.5–2 GB xuống 500–700 MB.
3. Giới hạn concurrency: bớt thread mỗi worker thay vì bớt worker.
4. Dùng worker killer (khởi động lại process khi vượt ngưỡng RSS) như biện pháp tạm, không phải cách chữa.
Nếu đúng là leak thật thì tìm nguồn: biến class/hằng số giữ tham chiếu (@@cache, hằng số tích luỹ), thread không bao giờ kết thúc, hoặc gem C extension. Dùng ObjectSpace.count_objects chụp trước/sau, hoặc rbtrace/heap dump để so sánh.