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.

Bảng MEMORY USAGE theo độ dài, ba cách ghi cùng một chuỗi, và phân mảnh

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 APPEND nếu biết trước độ dài. SET một lần.
  • Có thể ép Redis cắt gọn: DEBUG SET-ACTIVE-EXPIRE không giúp, nhưng ghi đè bằng SET mộ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ămMEMORY 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_memory mới là dữ liệu.
  • Thấy mem_fragmentation_ratio cao rồi hoảng. Tỷ lệ này chỉ có nghĩa khi used_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.