Bài 1

Redis chọn cấu trúc trong bộ nhớ thế nào

HSET không nói cho bạn biết Redis vừa lưu hash đó bằng cái gì. Với hash nhỏ, nó là một dãy byte liền mạch; qua một ngưỡng nào đó, nó thành bảng băm với con trỏ cho từng trường. Cùng một lệnh, cùng một kiểu dữ liệu, hai cách lưu chênh nhau vài lần bộ nhớ. OBJECT ENCODING là lệnh cho biết Redis đang dùng cách nào.

Một khoá, hai tầng

Mỗi khoá trong Redis có một kiểu mà người dùng thấy (TYPE trả về hash, set, zset, list, string) và một encoding là cách lưu thật trong bộ nhớ (OBJECT ENCODING). Kiểu do bạn chọn; encoding do Redis chọn, dựa trên số phần tử và độ dài giá trị so với các ngưỡng cấu hình.

KiểuEncoding gọnEncoding đầy đủNgưỡng quyết định
hashlistpackhashtablehash-max-listpack-entries, hash-max-listpack-value
set toàn sốintsethashtableset-max-intset-entries
set có chuỗilistpackhashtableset-max-listpack-entries, set-max-listpack-value
zsetlistpackskiplistzset-max-listpack-entries, zset-max-listpack-value
listlistpackquicklistlist-max-listpack-size, và giới hạn cứng 8 192 byte
stringint, embstrraw44 byte, không cấu hình được

112 tổ hợp, đo trên Redis thật

Bộ đo dựng từng khoá bằng một lệnh EVAL rồi gọi OBJECT ENCODING, với bốn kiểu tập hợp × bảy số phần tử (1, 127, 128, 129, 511, 512, 513) × bốn kiểu giá trị (chuỗi 3 byte, chuỗi 64 byte, chuỗi 65 byte, số nguyên). Ngưỡng dùng đúng mặc định của Redis 7.4, chỉ list-max-listpack-size đặt về 128 để có một con số dương so sánh được.

docker exec rdlab redis-cli EVAL "
  for i=1,tonumber(ARGV[1]) do
    redis.call('HSET', KEYS[1], 'f'..i, ARGV[2])
  end
  return redis.call('HLEN', KEYS[1])" 1 h513 513 v
docker exec rdlab redis-cli OBJECT ENCODING h513
# hashtable

Bảng dưới đây là những hàng nói được nhiều nhất trong 112 hàng:

KhoáSố phần tửGiá trịENCODINGMEMORY USAGE
hash128chuỗi 3 bytelistpack1 584
hash511chuỗi 3 bytelistpack6 192
hash512chuỗi 3 bytelistpack6 192
hash513chuỗi 3 bytehashtable28 816
hash1chuỗi 64 bytelistpack128
hash1chuỗi 65 bytehashtable248
set512số nguyênintset1 328
set513số nguyênhashtable24 712
set128chuỗi 3 bytelistpack816
set129chuỗi 3 bytehashtable6 280
zset128chuỗi 3 bytelistpack1 072
zset129chuỗi 3 byteskiplist13 572
string—44 ký tựembstr96
string—45 ký tựraw112
string—12345int48
Set toàn số nguyên không rơi về listpack Đây là hàng khó đoán nhất. set-max-listpack-entries là 128, nhưng một set gồm 200 số nguyên vẫn là intset chứ không phải hashtable — vì với set toàn số, ngưỡng có hiệu lực là set-max-intset-entries (512). Và khi vượt 512, nó sang thẳng hashtable, bỏ qua listpack. Thêm một phần tử không phải số vào set 200 phần tử đó thì nó thành hashtable ngay, còn thêm vào set 100 phần tử thì thành listpack.

Giá của một trường thứ 513

Hai khoá chỉ khác nhau một trường:

KhoáSố trườngENCODINGMEMORY USAGEByte mỗi trường
h512512listpack6 19212,1
h513513hashtable28 81656,2

Một trường thêm vào làm khoá đó chiếm gấp 4,65 lần bộ nhớ, và chi phí mỗi trường tăng từ 12,1 lên 56,2 byte. Lý do là listpack lưu các trường liền nhau trong một khối byte, không con trỏ, không bucket; hashtable phải có bảng bucket cộng một dictEntry cho mỗi trường.

Hệ quả thực tế: nếu ứng dụng của bạn có hàng triệu hash nhỏ, giữ chúng dưới ngưỡng là khoản tiết kiệm lớn nhất bạn có thể lấy mà không đổi một dòng mã. Chiều ngược lại cũng đúng — nới hash-max-listpack-entries lên quá cao thì mọi phép đọc một trường phải quét tuần tự cả listpack.

Một chi tiết nhỏ đáng ghi: h511 và h512 cùng chiếm 6 192 byte. Đó không phải lỗi lấy mẫu của MEMORY USAGE — chạy lại với SAMPLES 0 (đếm hết, không lấy mẫu) vẫn cho đúng 6 192. Listpack cấp phát theo bước, nên có lúc thêm một trường không tốn thêm byte nào.

List: giới hạn tính bằng byte, không phải số phần tử

list-max-listpack-size mặc định là -2, và số âm nghĩa là giới hạn tính theo kích thước chứ không theo số phần tử. Nhưng ngay cả khi đặt nó thành một số dương lớn, vẫn còn một giới hạn cứng: một node listpack không vượt 8 192 byte.

Tìm biên bằng tìm kiếm nhị phân với list-max-listpack-size 100000:

Độ dài mỗi phần tửSố phần tử cuối còn listpackSố phần tử đầu thành quicklistMEMORY USAGE ở biên
8 byte8188198 240
64 byte1221238 240
200 byte40418 240

Cả ba biên đều dừng ở MEMORY USAGE bằng 8 240 byte, tức 8 192 byte listpack cộng khoảng 48 byte phần đầu đối tượng. Phòng thí nghiệm bên dưới tính lại kích thước listpack theo công thức 6 byte header + 1 byte kết thúc + mỗi phần tử (nội dung + byte ghi độ dài + byte backlen), và công thức đó tái hiện đúng cả ba biên.

Chuyển encoding là một chiều

Redis chuyển từ encoding gọn sang encoding đầy đủ khi vượt ngưỡng, nhưng không bao giờ chuyển ngược. Đo được:

hash 513 trường                        → hashtable
CONFIG SET hash-max-listpack-entries 1024  → vẫn hashtable
HSET thêm một trường nữa               → vẫn hashtable
HDEL bớt còn 512 trường                → vẫn hashtable
tạo khoá MỚI với 513 trường (ngưỡng 1024) → listpack

Tương tự với set: 129 chuỗi thành hashtable, xoá bớt còn 128 vẫn là hashtable.

Hệ quả khi vận hành CONFIG SET chỉ có tác dụng với khoá tạo ra sau đó. Nới ngưỡng trên một instance đang chạy không thu nhỏ bất kỳ khoá nào đã lớn; muốn lấy lại bộ nhớ thì phải ghi lại khoá đó từ đầu, hoặc nạp lại dữ liệu từ RDB/AOF sau khi đã đổi cấu hình. Đây là lý do việc chỉnh ngưỡng phải nằm trong file cấu hình trước khi khởi động, không phải một lệnh chữa cháy.

Phòng thí nghiệm

Đặt kiểu, số phần tử, độ dài giá trị và từng ngưỡng. Thuật toán chạy trên trình duyệt là bản viết lại quy tắc ở trên, đã đối chiếu với 112 hàng ma trận, ba biên byte của list và bốn trường hợp string trên Redis 7.4.11 — khớp toàn bộ.

Khoá bạn muốn thử

Các trường hợp đã đo:

Ngưỡng cấu hình

Ô nào khác mặc định thì viền đổi màu.

OBJECT ENCODING trả về

Tự kiểm tra

Một hash có 10 trường, trong đó một trường lưu chuỗi 200 byte. Encoding là gì? hashtable. Số trường còn cách ngưỡng 512 rất xa, nhưng hash-max-listpack-value là 64, và chỉ cần một giá trị vượt ngưỡng là đủ để đổi cả khoá. Đây là lý do phổ biến nhất khiến một hash nhỏ lại tốn nhiều bộ nhớ hơn dự kiến.
Set gồm 300 số nguyên. Encoding là gì, và nếu thêm chuỗi "x" vào thì đổi thành gì? Trước đó là intset (300 ≤ 512). Thêm một chuỗi thì không còn toàn số, nên xét theo set-max-listpack-entries là 128 — 301 phần tử vượt ngưỡng, thành hashtable. Nếu set chỉ có 100 số thì cùng phép thêm đó sẽ cho listpack.
Bạn đặt list-max-listpack-size 10000. Một list 500 phần tử, mỗi phần tử 64 byte, encoding là gì? quicklist. Số phần tử còn cách ngưỡng rất xa, nhưng 500 × khoảng 67 byte đã vượt giới hạn cứng 8 192 byte của một node listpack. Đo được biên ở đúng 122 phần tử với độ dài 64 byte.
Instance đang có một hash 20 000 trường. Bạn chạy CONFIG SET hash-max-listpack-entries 50000. Bộ nhớ có giảm không? Không. Chuyển encoding là một chiều: khoá đã là hashtable thì ở lại hashtable, kể cả khi ngưỡng đã nới và kể cả sau khi thêm hoặc bớt trường. Chỉ khoá tạo sau lệnh đó mới được lợi.