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ểu | Encoding gọn | Encoding đầy đủ | Ngưỡng quyết định |
|---|---|---|---|
hash | listpack | hashtable | hash-max-listpack-entries, hash-max-listpack-value |
set toàn số | intset | hashtable | set-max-intset-entries |
set có chuỗi | listpack | hashtable | set-max-listpack-entries, set-max-listpack-value |
zset | listpack | skiplist | zset-max-listpack-entries, zset-max-listpack-value |
list | listpack | quicklist | list-max-listpack-size, và giới hạn cứng 8 192 byte |
string | int, embstr | raw | 44 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ị | ENCODING | MEMORY USAGE |
|---|---|---|---|---|
hash | 128 | chuỗi 3 byte | listpack | 1 584 |
hash | 511 | chuỗi 3 byte | listpack | 6 192 |
hash | 512 | chuỗi 3 byte | listpack | 6 192 |
hash | 513 | chuỗi 3 byte | hashtable | 28 816 |
hash | 1 | chuỗi 64 byte | listpack | 128 |
hash | 1 | chuỗi 65 byte | hashtable | 248 |
set | 512 | số nguyên | intset | 1 328 |
set | 513 | số nguyên | hashtable | 24 712 |
set | 128 | chuỗi 3 byte | listpack | 816 |
set | 129 | chuỗi 3 byte | hashtable | 6 280 |
zset | 128 | chuỗi 3 byte | listpack | 1 072 |
zset | 129 | chuỗi 3 byte | skiplist | 13 572 |
string | — | 44 ký tự | embstr | 96 |
string | — | 45 ký tự | raw | 112 |
string | — | 12345 | int | 48 |
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ường | ENCODING | MEMORY USAGE | Byte mỗi trường |
|---|---|---|---|---|
h512 | 512 | listpack | 6 192 | 12,1 |
h513 | 513 | hashtable | 28 816 | 56,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 listpack | Số phần tử đầu thành quicklist | MEMORY USAGE ở biên |
|---|---|---|---|
| 8 byte | 818 | 819 | 8 240 |
| 64 byte | 122 | 123 | 8 240 |
| 200 byte | 40 | 41 | 8 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.
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.