Phần trước tìm ra ba vách đá bộ nhớ khi tập hợp vượt ngưỡng. Bài này đo hai thứ tiếp theo: chuyện gì xảy ra khi nó nhỏ lại, và bạn được gì để đổi lấy chỗ nhớ đó.
Chuyển mã hoá là một chiều
Tôi tạo một Hash 513 trường để ép nó thành hashtable, rồi xoá bớt còn 199 trường:
tạo 513 rồi xoá còn 199 hashtable 16.256 byte
tạo mới đúng 199 trường listpack 1.840 byte
Cùng 199 trường. Cùng dữ liệu. Chênh 8,8 lần.
Redis không bao giờ chuyển ngược về mã hoá gọn. Một lần vượt ngưỡng là khoá đó mang mã hoá đắt đến hết đời — kể cả khi nó nhỏ lại từ lâu.
Đây là chỗ rò rỉ bộ nhớ âm thầm nhất tôi biết trong Redis. Một Hash phình lên trong một đợt cao điểm rồi co lại, và nó giữ chi phí của lúc lớn nhất, vĩnh viễn. Không có cảnh báo nào, không có chỉ số nào cho biết.
Cách chữa duy nhất là ghi lại khoá:
redis-cli --scan --pattern 'user:*' | while read k; do
redis-cli --raw dump "$k" > /tmp/d && \
redis-cli del "$k" >/dev/null && \
redis-cli -x restore "$k" 0 < /tmp/d >/dev/null
done
Hoặc đơn giản hơn nếu dữ liệu dựng lại được: xoá và ghi lại từ nguồn.
Được gì để đổi
Cùng 199 trường, hai mã hoá, đo thời gian thật:
hashtable |
listpack |
|
|---|---|---|
HGET f1 |
0,192 µs | 0,216 µs |
HGET f100 |
0,208 µs | 0,595 µs |
HGET f199 |
0,206 µs | 1,148 µs |
HGETALL |
17,791 µs | 15,653 µs |
hashtable phẳng ở mọi vị trí — 0,19 đến 0,21 µs, đúng như O(1).
listpack tăng tuyến tính theo vị trí: trường cuối tốn gấp 5,6 lần trường cuối trong hashtable. Nó phải quét từ đầu, không có chỉ mục.
Nhưng dòng cuối lật ngược: đọc cả tập thì listpack nhanh hơn 12%. Dữ liệu nằm liền mạch trong bộ nhớ, không con trỏ nào phải đuổi theo, bộ nhớ đệm CPU thích điều đó.
Đó là toàn bộ đánh đổi trong một bảng: gọn và đọc-hết-nhanh, đổi lấy tra-từng-phần-chậm.
Cách chọn ngưỡng theo cách truy cập
Từ bảng trên, quy tắc thực dụng:
- Nếu bạn chủ yếu
HGETALL/SMEMBERS— đọc cả tập — thì nâng ngưỡng lên rất đáng. Bạn được bộ nhớ, và còn nhanh hơn một chút. - Nếu bạn tra từng trường trong tập lớn —
HGETmột trường trong 500 — thì để nguyên. 1,1 µs mỗi lần tra nhân với vài chục nghìn lệnh mỗi giây là một phần đáng kể của luồng duy nhất.
Cụ thể:
redis-cli config set hash-max-listpack-entries 1024
Nhớ hai điều. Thứ nhất, cấu hình không áp cho khoá đã tồn tại — chúng giữ mã hoá cũ tới khi được ghi lại. Thứ hai, config set không bền qua khởi động lại; ghi vào tệp cấu hình hoặc config rewrite.
Ngưỡng thứ hai ít người biết: độ dài giá trị
Mọi bài viết đều nhắc hash-max-listpack-entries. Ít bài nhắc cái này:
giá trị 64 byte -> listpack 128 byte
giá trị 65 byte -> hashtable 248 byte
Một Hash một trường, chênh một byte, gần gấp đôi bộ nhớ.
hash-max-listpack-value mặc định 64. Một giá trị vượt 64 byte là cả Hash chuyển sang hashtable — không phải trường đó, mà toàn bộ khoá.
Điều này giải thích một hiện tượng hay gặp: bạn thêm một trường description dài vào đối tượng người dùng, và bộ nhớ Redis tăng gấp đôi dù chỉ thêm vài chục byte mỗi bản ghi. Không phải trường mới nặng — mà là nó đẩy mọi Hash sang mã hoá đắt.
Cách chữa: tách trường dài ra khoá riêng, hoặc nâng hash-max-listpack-value. Với dữ liệu chủ yếu đọc cả tập, nâng lên 128 hoặc 256 thường an toàn.
Bảng mã hoá đầy đủ
| Kiểu | Gọn | Đầy đủ | Ngưỡng |
|---|---|---|---|
| String | int, embstr |
raw |
44 byte |
| Hash | listpack |
hashtable |
512 mục / 64 byte |
| List | listpack |
quicklist |
128 mục / 64 byte |
| Set số nguyên | intset |
listpack → hashtable |
512 số |
| Set | listpack |
hashtable |
128 mục / 64 byte |
| Sorted Set | listpack |
skiplist |
128 mục / 64 byte |
ziplist là tên cũ; Redis 7 thay bằng listpack — cùng ý tưởng, sửa vài lỗi tràn số và bỏ trường độ dài ngược. Nếu tài liệu bạn đọc nói ziplist thì nó viết cho Redis 6 trở về trước, và các ngưỡng có tên khác (*-max-ziplist-*). Redis 7 vẫn nhận tên cũ làm bí danh.
Thử ba mươi giây
Tìm khoá đang mang mã hoá đắt mà không cần tới — tức là khoá từng lớn rồi nhỏ lại:
redis-cli --scan --count 500 | head -3000 | while read k; do
e=$(redis-cli object encoding "$k")
case "$e" in
hashtable|skiplist)
t=$(redis-cli type "$k")
n=$(case "$t" in
hash) redis-cli hlen "$k";; set) redis-cli scard "$k";;
zset) redis-cli zcard "$k";; *) echo 99999;;
esac)
if [ "${n:-99999}" -lt 100 ]; then
echo "$(redis-cli memory usage "$k") byte / $n phan tu / $e / $k"
fi
;;
esac
done | sort -rn | head -20
Mỗi dòng in ra là một khoá dùng cấu trúc của tập lớn để chứa vài chục phần tử. Ghi lại chúng — DUMP rồi RESTORE, hoặc dựng lại từ nguồn — và bộ nhớ trả về thường nhiều hơn bạn nghĩ.
Phần sau: Bitmap và HyperLogLog — đo bộ nhớ đổi lấy độ chính xác.