"Đừng lưu giá trị lớn trong Redis" là lời khuyên phổ biến. Bài này đo xem lớn tới đâu thì thành vấn đề, và vấn đề đó thật ra là gì.

Bảng độ trễ theo kích thước, tác động lên khách khác, và chi phí bộ đệm đầu ra

Độ trễ theo kích thước

Kích thước p50 p99 Thông lượng
100 byte 0,051 ms 0,149 ms 2 MB/s
1.024 byte 0,052 ms 0,148 ms 19 MB/s
10.240 byte 0,051 ms 0,118 ms 192 MB/s
102.400 byte 0,089 ms 0,171 ms 1.095 MB/s
1 MB 0,278 ms 0,827 ms 3.591 MB/s
5 MB 1,183 ms 9,471 ms 4.226 MB/s
10 MB 3,505 ms 5,501 ms 2.853 MB/s

Từ 100 byte tới 10 KB, độ trễ phẳng hoàn toàn — 0,051 ms ở cả ba mức. Kích thước gần như miễn phí trong khoảng đó.

Vì thời gian bị chi phối bởi lượt đi về, không bởi số byte. Một gói TCP mang được vài KB, nên 10 KB tốn gần đúng bằng 100 byte.

Trên 100 KB thì bắt đầu tuyến tính: 10 MB tốn 3,5 ms, tức khoảng 2,9 GB/s.

Và nó chặn khách khác ít hơn tôi tưởng

đọc 1 KB liên tục     PING p50 0,055 ms | p99 0,287 ms | max 10,8 ms
đọc 10 MB liên tục    PING p50 0,195 ms | p99 2,211 ms | max 10,0 ms

Đọc 10 MB 2.499 lần trong 8 giây — khoảng 3,1 GB/s — chỉ đẩy p50 của khách khác từ 0,055 lên 0,195 ms.

Redis gửi phản hồi lớn theo từng đoạn và nhường lại vòng lặp sự kiện giữa các đoạn, đúng như nó làm với đường ống ở phần 12. Một GET 10 MB không phải một lệnh chặn kiểu KEYS.

Nên "giá trị lớn làm Redis chậm" không phải mô tả đúng. Vấn đề nằm ở chỗ khác.

Chỗ nó thật sự nguy hiểm: bộ đệm đầu ra

20 khách cùng yêu cầu một khoá rồi đọc rất chậm:

giá trị  1 KB   mem_clients_normal        40.488 byte
giá trị 10 MB   mem_clients_normal   209.879.000 byte

Gấp 5.183 lần.

Dữ liệu chỉ 10 MB, nhưng used_memory215 MB — 200 MB trong đó là hai mươi bản sao của giá trị nằm trong bộ đệm chờ gửi cho khách.

Và như đã đo ở phần 12: client-output-buffer-limit normal mặc định là 0 0 0không có giới hạn.

Nên công thức đúng là:

kích thước giá trị × số khách đọc chậm = bộ nhớ máy chủ, không có trần.

Với giá trị 10 MB và 100 khách trên mạng chậm, đó là 1 GB bộ nhớ mà maxmemory không kiểm soát được — và nó đến từ bộ đệm khách, không từ dữ liệu.

Đây là cách một Redis "chỉ chứa 5 GB dữ liệu" bị hệ điều hành giết khi có 20 GB RAM.

Ngưỡng thực dụng

Từ các con số trên:

  • Dưới 10 KB: hoàn toàn không phải nghĩ. Độ trễ y hệt giá trị nhỏ nhất.
  • 10 KB – 100 KB: ổn, nhưng bắt đầu đáng để ý nếu số khách đồng thời lớn.
  • 100 KB – 1 MB: chỉ dùng khi số khách đọc cùng lúc có trần rõ ràng.
  • Trên 1 MB: cần lý do rất tốt, và cần đặt client-output-buffer-limit.
client-output-buffer-limit normal 268435456 134217728 60

Không phải để bảo vệ khỏi giá trị lớn — mà để một khách cư xử tệ không kéo sập cả máy chủ.

Khoá lớn cũng tính

Phần lớn thảo luận nói về giá trị, nhưng một Hash 3 triệu trường hoặc một List một triệu phần tử cũng là "khoá lớn", và chúng có vấn đề riêng đã đo ở các phần trước:

  • DEL một Hash 194 MB chặn 467,7 ms (phần 11) — dùng UNLINK.
  • HGETALL trên Hash lớn trả về toàn bộ trong một phản hồi, tức là một bộ đệm đầu ra khổng lồ cho mỗi khách gọi nó.
  • Vượt ngưỡng listpack làm bộ nhớ tăng gấp 2–18 lần (phần 4) và không quay lại được (phần 5).
  • Trên Cluster, một khoá lớn không chia được — nó nằm trên một nút, và như phần 29, đó là chỗ giới hạn sức chứa.

Nên "khoá lớn" nguy hiểm hơn "giá trị lớn": giá trị lớn tốn băng thông một lần, khoá lớn tốn ở mọi thao tác chạm vào nó.

Chia nhỏ thế nào

Với giá trị lớn: đừng lưu trong Redis. Để trong kho đối tượng và lưu đường dẫn trong Redis. Redis giỏi việc tra khoá nhanh, không giỏi việc làm máy chủ tệp.

Với Hash lớn: chia theo tiền tố.

user:12345:profile      thay cho  users (Hash 3 trieu truong)
user:12345:settings

Mỗi khoá nhỏ, mỗi thao tác rẻ, và trên Cluster chúng rải đều.

Với List lớn: cắt bớt. LTRIM sau mỗi LPUSH, hoặc dùng Stream với MAXLEN ~ như phần 7.

Thử ba mươi giây

Tìm khoá lớn nhất mà không quét toàn bộ:

redis-cli --bigkeys
redis-cli --memkeys       # xep theo bo nho, khong theo so phan tu

--bigkeys lấy mẫu và in khoá lớn nhất của mỗi kiểu; nó dùng SCAN nên không chặn như KEYS.

Và kiểm bộ đệm đầu ra đang lớn cỡ nào:

redis-cli info clients | grep client_recent_max_output_buffer
redis-cli info memory | grep mem_clients_normal
redis-cli client list | awk '{for(i=1;i<=NF;i++) if($i ~ /^omem=/) print $i, $0}' | sort -t= -k2 -rn | head -5

omem trong CLIENT LIST là bộ nhớ đầu ra của từng khách. Thấy một khách có omem hàng chục megabyte nghĩa là bạn đang ở đúng tình huống trong bài này — và nếu có vài chục khách như vậy, đó là bộ nhớ máy chủ đang biến mất mà maxmemory không hề biết.

Phần sau: theo dõi Redis — chỉ số nào đáng cảnh báo và chỉ số nào gây hiểu nhầm.