Bốn lỗi này xuất hiện ở gần như mọi hệ thống tôi từng nhìn. Bài này đo giá phải trả của từng cái.

Bốn lỗi với số đo cụ thể cho mỗi cái

Sai 1: khoá không TTL

50.000 phiên không TTL  ->  expires=0        15,47 MB, ở lại mãi
50.000 phiên có TTL     ->  expires=50000    tự dọn sau một giờ

Khoá không có hạn không bao giờ tự biến mất. Với dữ liệu thật sự vĩnh viễn thì đúng; với bộ đệm, phiên, khoá tạm thì đó là rò rỉ.

Và nó rò rỉ theo cách khó thấy nhất: bộ nhớ tăng đều đặn hàng tuần, không có sự kiện nào để lần ra, cho tới lúc chạm maxmemory.

Kiểm bằng một lệnh:

redis-cli info keyspace
db0:keys=200000,expires=50000

Chênh lệch giữa keysexpires là số khoá sẽ ở lại vĩnh viễn. Nếu con số đó lớn và bạn không cố ý, đó là chỗ Redis đang lớn dần.

Sai 2: gọi KEYS trong mã ứng dụng

500.000 khoá, đo độ trễ PING của khách khác:

KEYS 'k:1*'          max 38,5 ms
SCAN --count 1000    max  4,7 ms

Cùng kết quả, chặn ít hơn 8 lần.

Ở phần 11 tôi đo trên 2 triệu khoá và con số là 263,8 ms so với 4,2 ms — chênh 63 lần. Nghĩa là thiệt hại của KEYS tỉ lệ với số khoá, còn SCAN thì gần như không đổi.

Đây là lý do KEYS chạy tốt trên máy phát triển với hai nghìn khoá và làm sập production với hai triệu.

Sai 3: SET ghi đè làm mất TTL

SET phien:abc ... EX 1800   ->  TTL = 1800
SET phien:abc ... (ghi đè)  ->  TTL = -1     phiên BẤT TỬ
SET phien:abc ... KEEPTTL   ->  TTL = 1800

Đây là lỗi tinh vi nhất trong bốn cái.

Phiên đăng nhập được tạo với TTL 30 phút. Rồi ở đâu đó trong mã có một chỗ cập nhật thông tin phiên — thêm một trường, đổi ngôn ngữ hiển thị — và nó gọi SET thẳng.

Từ giây phút đó, phiên không bao giờ hết hạn. Không có lỗi, không có cảnh báo. Người dùng đăng nhập vĩnh viễn, và số khoá phiên chỉ tăng.

Sửa: thêm KEEPTTL vào mọi SET ghi đè lên khoá có hạn. Và như đo ở phần 8, APPEND, INCRBY, SETRANGE giữ TTL — chỉ SETGETSET mới xoá.

Sai 4: một khoá cho mỗi trường

u:1:ten, u:1:email, u:1:tuoi, u:1:thanh   × 20.000 người
   80.000 khoá   7,52 MB

HSET u:1 ten ... email ... tuoi ... thanh ...   × 20.000 người
   20.000 khoá   4,27 MB

Tiết kiệm 43%, và đọc cả đối tượng bằng một lệnh thay vì bốn.

Lý do đã đo ở phần 3: mỗi khoá tốn khoảng 64 byte phụ trội. Bốn khoá là bốn lần phụ trội đó cho cùng một đối tượng.

Và lợi ích thứ hai quan trọng không kém: HGETALL u:1một lượt đi về. Bốn khoá riêng là bốn lượt — hoặc một MGET, nhưng MGET không dùng được trên Cluster nếu bốn khoá rơi vào bốn slot khác nhau (phần 23).

Cảnh báo: đừng gom quá tay. Một Hash 100.000 trường vượt ngưỡng listpack (phần 5) và mọi HGET trở thành tra bảng băm lớn. Gom theo đối tượng, không gom theo loại.

Bốn cái này có điểm chung

Không cái nào gây lỗi. Không cái nào xuất hiện trong nhật ký. Cả bốn đều chạy đúng trong môi trường phát triển và chỉ lộ ra ở quy mô thật — sau vài tuần, khi bộ nhớ đầy hoặc độ trễ đuôi tăng.

Đó là đặc điểm chung của gần như mọi vấn đề đo trong sê-ri này: Redis rất hiếm khi báo lỗi. Nó chỉ chậm dần, hoặc lớn dần, cho tới lúc chạm một trần cứng.

Thử ba mươi giây

Bốn lệnh, mỗi lệnh cho một lỗi:

# 1. bao nhieu khoa khong co han
redis-cli info keyspace

# 2. co ai goi KEYS khong
redis-cli info commandstats | grep -E 'cmdstat_(keys|smembers|hgetall|lrange):'

# 3. lay mau khoa phien, xem TTL
redis-cli --scan --pattern 'phien:*' --count 200 | head -50 | \
  while read k; do echo "$(redis-cli ttl "$k") $k"; done | sort -n | head -10

# 4. co bao nhieu khoa cung tien to nguoi dung
redis-cli --scan --count 500 | head -3000 | \
  sed 's/[0-9][0-9]*/N/g' | sort | uniq -c | sort -rn | head -10

Lệnh thứ tư đáng nhìn kỹ nhất. Nếu bảng có nhiều dòng cùng tiền tố khác hậu tố — u:N:ten, u:N:email, u:N:tuoi — bạn đang ở đúng lỗi thứ tư, và một lần đổi sang Hash lấy lại gần một nửa bộ nhớ.

Phần cuối: gom mọi phép đo của bốn mươi phần thành một danh sách kiểm.