Tỷ file rác — cái giá thầm lặng của immutable storage

Tỷ file rác — cái giá thầm lặng của immutable storage

Mỗi lần write tạo file mới nghe sạch sẽ lắm, cho đến khi hóa đơn storage tháng sau bay về.

"Immutable" nghe an toàn — nhưng ai dọn?

Bạn từng nghe câu này chưa: "Don't mutate, just create new." Nguyên tắc vàng trong distributed systems và functional programming. Mỗi lần ghi, tạo file mới. Không sửa file cũ. Không race condition, không corrupt data, recovery dễ như ăn kẹo.

Nhưng có một sự thật ít ai nói trước: file cũ không tự biến mất.

Hình dung thế này: bạn đang xây nhà, mỗi lần muốn sửa bức tường, thay vì trát lại, bạn xây một bức tường mới ngay bên cạnh. Sạch? Cực kỳ sạch. Nhưng sau vài tháng, công trường ngập gạch vụn, và đội thầu tính tiền mặt bằng theo mét vuông — kể cả mét vuông chứa đống đổ nát.

Đó chính xác là bài toán mà Pinecone — nền tảng vector database quen mặt với dân làm RAG — phải đối mặt khi hệ thống data plane của họ ngày càng phình to.

Khoan, chuyện phức tạp hơn "rm -rf" nhiều

Ở single-node database, xóa data đơn giản: check reference, xóa, ghi log. Xong.

Nhưng ở quy mô Pinecone — nơi writing nodes và reading nodes tách biệt — câu chuyện khác hẳn. Một writer vừa tạo file mới thay thế file cũ, nhưng ở đâu đó, một reader đang serve query từ chính file cũ đó. Xóa ngay? Query fail. Không xóa? Hóa đơn blob storage cứ leo thang.

Pinecone gọi đây là deletion tax — chi phí của dữ liệu bạn không cần nữa nhưng chưa thể xóa an toàn. Và nó âm thầm trở thành một trong những dòng lớn nhất trên hóa đơn infrastructure của họ.

Chiếu chậm lại thì bài toán có 3 phần:

  1. Identify — file nào là rác, file nào đang được dùng?
  2. Verify — chắc chắn 100% không ai đang đọc nó?
  3. Execute — xóa, nhưng phải có audit trail để rollback nếu cần.

Mỗi bước đều có bẫy riêng, và gộp chúng lại thì bẫy nhân lên chứ không cộng.

Janitor — đội quản lý tòa nhà cho blob storage

Pinecone xây một hệ thống nội bộ gọi là Janitor, chuyên trách chu trình identify → verify → execute.

Janitor giống đội quản lý tòa nhà: trước khi phá dỡ phòng nào, phải kiểm tra hợp đồng thuê, xác nhận không ai ở, rồi mới gọi thợ — và quay camera toàn bộ quá trình phòng khi có tranh chấp.

Điểm đáng chú ý: Janitor không chạy kiểu "quét hết một lượt rồi xóa." Nó hoạt động liên tục, vì trong hệ thống immutable, rác sinh ra mỗi giây. Mỗi write mới = một file cũ tiềm năng trở thành orphan.

Ví dụ cụ thể — kịch bản team Việt Nam:

Giả sử team bạn 4 người đang vận hành một hệ thống RAG cho chatbot nội bộ công ty. Mỗi ngày, pipeline indexing chạy 3 lần, mỗi lần tạo vài trăm file embedding mới trên S3. Sau 6 tháng, bucket S3 có hàng trăm nghìn file, nhưng chỉ khoảng 30% là "sống." Phần còn lại? Rác. Và bạn đang trả tiền S3 cho cả 100%.

Không có Janitor-style system, team thường làm gì? Viết một cron job chạy nửa đêm, list tất cả objects, so sánh với metadata hiện tại, rồi xóa cái nào không match. Nghe ổn — cho đến khi cron job xóa nhầm file đang được một reader dùng vì timing lệch vài giây.

Bẫy kinh điển: xóa rồi mới biết đang cần

Câu chuyện mà team nào cũng từng dính ít nhất một lần:

Bạn viết script cleanup, test kỹ trên staging, chạy ngon lành. Deploy lên production đêm thứ Sáu (vì sao mọi thứ tệ đều xảy ra đêm thứ Sáu?). Sáng thứ Bảy, khách hàng báo search trả kết quả trống. Hóa ra script xóa mất batch embeddings vừa được index 2 phút trước khi cleanup chạy — "vừa index xong" chưa kịp update metadata, nên script tưởng là orphan.

Bài học từ cách Pinecone thiết kế Janitor: verify phải là bước riêng biệt, không gộp chung với identify. Và giữa verify và execute, phải có grace period đủ dài để mọi in-flight operation hoàn tất.

Ngoài ra, audit trail không phải nice-to-have, mà là cứu cánh. Khi xóa nhầm (và sẽ có lúc xóa nhầm), log chi tiết giúp bạn biết chính xác cái gì bị xóa, lúc nào, tại sao — và quan trọng nhất: có recover được không.

Thử ngay chiều nay: xây deletion pipeline tối giản

Bạn không cần hệ thống phức tạp như Pinecone. Đây là phiên bản rút gọn cho team nhỏ:

Bước 1 — Tách identify và delete. Viết một job liệt kê tất cả objects có last_modified cách đây hơn N ngày. Output ra một file manifest (danh sách candidate xóa), KHÔNG xóa trực tiếp.

Bước 2 — Cross-reference. So sánh manifest với danh sách objects đang được reference trong database/index hiện tại. Chỉ giữ lại những objects không ai reference.

Bước 3 — Grace period. Đợi ít nhất 24 giờ giữa verify và execute. Trong thời gian đó, nếu có deployment mới hoặc reindex, chạy lại bước 2.

Bước 4 — Soft delete trước. Thay vì xóa thẳng, move objects sang một "trash" bucket/prefix. Sau 7 ngày không vấn đề gì, mới xóa vĩnh viễn.

Bước 5 — Log mọi thứ. Mỗi lần xóa, ghi lại: object key, size, lý do xóa, timestamp. Đơn giản nhất là append vào một file JSON trên chính storage đó.

Nếu team dùng AWS, S3 Lifecycle Rules kết hợp S3 Inventory là điểm khởi đầu tốt. Với MinIO (open-source alternative cho S3), bạn cũng có lifecycle management tương tự.

Open-source: cùng bài toán, tự lo giải

Nếu bạn đang dùng vector database open-source như Milvus hoặc Qdrant thay vì Pinecone, bài toán garbage collection vẫn y hệt — chỉ là bạn phải tự xây phần dọn dẹp. Milvus có built-in compaction, nhưng với custom blob storage layer, bạn vẫn cần pipeline riêng.

Với object storage self-host, MinIO có lifecycle rules khá đầy đủ. SeaweedFS cũng đáng xem nếu cần distributed storage nhẹ hơn.

Dù dùng gì, nguyên tắc không đổi: identify → verify → execute, và luôn giữ audit trail.

Một dòng để nhớ

Immutable storage giải quyết vấn đề consistency, nhưng tạo ra vấn đề cost. Và cost thì không crash lúc 3 giờ sáng — nó chỉ lặng lẽ tăng mỗi tháng cho đến khi CFO gõ cửa hỏi "tại sao hóa đơn S3 tháng này gấp đôi?"

Spoiler: không có silver bullet — nhưng có silver discipline: tách identify khỏi delete, verify trước khi execute, và log như thể ngày mai sẽ cần rollback.

---

Bụi Wire — nghiện đọc release notes lúc 2 giờ sáng

Nguồn tham khảo