Phần trước đo được 185 byte cho mỗi khoá chứa 100 byte dữ liệu. Bài này đào vào con số đó — và tìm ra ba cách khác nhau để cùng một chuỗi tốn những lượng bộ nhớ khác nhau.
Bao nhiêu byte cho một chuỗi
Khoá dài cố định 8 ký tự, đo bằng MEMORY USAGE:
| Giá trị | Tổng | Phụ trội | Mã hoá |
|---|---|---|---|
| 1 byte | 64 | 63 | embstr |
| 8 byte | 72 | 64 | embstr |
| 32 byte | 96 | 64 | embstr |
| 44 byte | 104 | 60 | embstr |
| 45 byte | 112 | 67 | raw |
| 100 byte | 168 | 68 | raw |
| 256 byte | 376 | 120 | raw |
| 1024 byte | 1.336 | 312 | raw |
Phụ trội cố định khoảng 64 byte cho chuỗi ngắn. Đó là khoá, dictEntry, robj, và tiêu đề chuỗi.
Nghĩa là lưu một khoá 8 byte dữ liệu tốn 72 byte — 9 lần kích thước dữ liệu. Với hàng triệu khoá nhỏ, phần phụ trội chiếm phần lớn RAM.
Số nguyên là ngoại lệ đẹp: SET num 12345 tốn 48 byte và mã hoá là int — Redis lưu giá trị số thẳng trong con trỏ, không cấp phát chuỗi nào.
Ranh giới 44 byte
embstr cấp phát một khối chứa cả robj lẫn chuỗi. raw cấp phát hai khối riêng.
Ranh giới là 44 byte, và nó không tuỳ chỉnh được. Chuỗi 44 byte tốn 104, chuỗi 45 byte tốn 112 — thêm một ký tự, thêm 8 byte, vì phải thêm một lần cấp phát.
Với chuỗi ngắn thì embstr nhanh hơn: một lần cấp phát, một lần giải phóng, và dữ liệu nằm liền kề nên tốt cho bộ nhớ đệm CPU.
Điều này gợi ra một chỗ tối ưu thật: nếu bạn lưu JSON nhỏ và đang ở quanh 45–60 byte, cắt vài trường thừa để xuống dưới 44 sẽ đổi hẳn cách lưu.
Cùng một chuỗi, ba con số
Đây là chỗ tôi không lường trước. Cùng 1024 byte dữ liệu:
| Cách ghi | Bộ nhớ |
|---|---|
redis-cli SET bình thường |
1.336 byte |
--pipe với RESP đúng chuẩn |
1.328 byte |
--pipe với lệnh dạng văn bản |
2.096 byte (+58%) |
SET 1 byte rồi APPEND 1023 byte |
2.608 byte (+96%) |
Tôi phát hiện ra vì hai phép đo của chính mình không khớp: MEMORY USAGE báo 1.336 trong một phép thử và 2.096 trong phép thử khác, cùng một độ dài. Tưởng đo nhầm, đo lại ba lần, vẫn vậy.
Nguyên nhân là giao thức. redis-cli --pipe với lệnh dạng văn bản thuần đi qua đường phân tích inline: Redis tách chuỗi từ dòng lệnh và không cắt gọn phần dư. RESP multibulk khai trước độ dài, nên cấp phát vừa khít.
Kiểm chứng: tôi sinh RESP đúng chuẩn rồi đưa qua chính --pipe, và được 1.328 byte. Vậy thủ phạm là dạng lệnh, không phải công cụ.
APPEND thì tệ hơn nữa: chiến lược tăng trưởng của Redis cấp phát gấp đôi để lần sau khỏi cấp phát lại. Chuỗi 1024 byte dựng bằng APPEND chiếm 2.608 byte — gần gấp đôi.
Nó có quan trọng không
Với vài nghìn khoá thì không. Với vài triệu thì có: 58% của 10 GB là 5,8 GB, và RAM là thứ đắt nhất trong hoá đơn Redis.
Ba việc rút ra:
- Nhập dữ liệu hàng loạt bằng RESP, đừng bằng lệnh văn bản. Mọi thư viện đều gửi RESP; chỉ có tệp lệnh viết tay là dùng inline.
- Tránh dựng chuỗi bằng
APPENDnếu biết trước độ dài.SETmột lần. - Có thể ép Redis cắt gọn:
DEBUG SET-ACTIVE-EXPIREkhông giúp, nhưng ghi đè bằngSETmột lần sẽ cấp phát lại vừa khít.
Ba chỗ đo cho ba con số
Với 200.000 khoá × 100 byte:
MEMORY USAGE mỗi khoá 160 byte
used_memory / số khoá 185 byte
used_memory_rss 47,3 MB (used_memory 37,1 MB)
Chênh 25 byte giữa hai dòng đầu là bảng băm — MEMORY USAGE tính khoá và giá trị nhưng không tính hết phần cấu trúc chỉ mục của cả không gian khoá.
Nên MEMORY USAGE luôn báo thấp hơn thực tế. Ước lượng dung lượng bằng nó là sẽ thiếu, và thiếu đúng 15% ở phép đo này.
Dòng thứ ba là phân mảnh: hệ điều hành cấp cho tiến trình 47,3 MB trong khi Redis đang dùng 37,1 MB. Tỷ lệ 1,28 là bình thường.
FLUSHALL không trả bộ nhớ về hệ điều hành
sau FLUSHALL:
used_memory 1,4 MB
used_memory_rss 13,1 MB
mem_fragmentation_ratio 9,87
Xoá sạch dữ liệu, nhưng RSS vẫn 13,1 MB. Tỷ lệ phân mảnh nhảy lên 9,87.
Đây là hành vi bình thường của bộ cấp phát: nó giữ lại vùng nhớ để dùng cho lần sau thay vì trả về hệ điều hành. Nhưng nó tạo ra hai hiểu nhầm hay gặp:
- Nhìn RSS rồi tưởng Redis vẫn giữ dữ liệu. Không —
used_memorymới là dữ liệu. - Thấy
mem_fragmentation_ratiocao rồi hoảng. Tỷ lệ này chỉ có nghĩa khiused_memoryđủ lớn; với một instance gần rỗng nó luôn cao và không nói lên gì.
Muốn trả bộ nhớ về thật thì cần activedefrag yes, và nó tốn CPU. Thường không đáng, trừ khi tỷ lệ vượt 1,5 trong lúc đang chứa nhiều dữ liệu.
Thử ba mươi giây
Ước lượng đúng dung lượng cần cho một tập dữ liệu, thay vì nhân số khoá với kích thước giá trị:
# lấy mẫu 1000 khoá và tính trung bình thật
redis-cli --no-raw eval "
local ks = redis.call('randomkey')
local t = 0
for i = 1, 1000 do
local k = redis.call('randomkey')
if k then t = t + redis.call('memory', 'usage', k) end
end
return t
" 0
# rồi so với con số toàn cục
redis-cli info memory | grep -E 'used_memory:|used_memory_dataset:'
redis-cli dbsize
Chia used_memory cho dbsize sẽ ra con số cao hơn trung bình MEMORY USAGE — phần chênh là bảng băm, và nó không biến mất khi bạn làm giá trị nhỏ đi. Với dữ liệu gồm hàng triệu khoá nhỏ, đó mới là khoản chi lớn nhất.
Phần sau: Hash, List, Set và Sorted Set — đo bộ nhớ và độ phức tạp của từng thao tác.