Phi công đã bay nghìn giờ vẫn đọc to danh sách kiểm trước mỗi lần cất cánh. Không phải vì họ quên cách bay, mà vì trí nhớ con người bỏ sót đúng vào lúc áp lực cao nhất, và một mục bị quên trên đường băng đắt hơn nhiều so với ba mươi giây đọc lại. Sau bốn mươi phần đo Redis — mỗi phần một phép đo chạy trong container rồi dọn đi — thứ đọng lại đáng giá nhất không phải từng con số riêng lẻ, mà là một danh sách kiểm kiểu ấy: cái để chạy tay qua trước khi đưa một cụm Redis lên chạy thật. Bài này gom lại thành thứ dùng được, kèm ba lần chính tôi đọc nhầm đồng hồ.
Mười con số đáng nhớ nhất
| Con số | Ý nghĩa | Phần |
|---|---|---|
| 64 byte | phụ trội mỗi khoá chuỗi | 3 |
| 44 byte | ranh giới embstr sang raw |
3 |
| 2,6 byte | mỗi số trong intset — rẻ nhất trong Redis |
4 |
| ×18,6 | khi Set số nguyên vượt 512 phần tử | 4 |
| 14.384 byte | HyperLogLog, không tăng dù đếm bao nhiêu | 6 |
| ×19,7 | bộ nhớ khi tiêu thụ Stream quên XACK |
7 |
| ×44 | thông lượng khi thêm đường ống 64 | 2 |
| ×5.183 | bộ đệm đầu ra khi giá trị 10 MB thay vì 1 KB | 30 |
| 0 | bản ghi mất khi SIGKILL, kể cả appendfsync no |
19 |
| 79% | bản ghi mất khi bản sao chậm rồi máy chính chết | 21 |
Hai dòng cuối đứng cạnh nhau là bức tranh đầy đủ về độ bền của Redis: nó tốt hơn nhiều người nghĩ trong trường hợp thường, và tệ hơn nhiều người nghĩ trong trường hợp xấu.
Ba điều lặp lại ở nhiều phần nhất
1. Nút thắt thường là lượt đi về, không phải Redis.
Một kết nối không đường ống: 20.000 ops/s. Cùng kết nối đó với đường ống 64: 1.136.364 ops/s. Redis dùng chưa hết một nhân trong trường hợp đầu. Điều này xuất hiện lại ở phần 12 (đường ống), 23 (chuyển hướng Cluster), 29 (khoá nóng không làm chậm), 32 (TLS miễn phí khi có đường ống). Trước khi tối ưu bất cứ thứ gì, hãy kiểm xem bạn có đang tốn một lượt đi về cho mỗi lệnh không.
2. Vượt một ngưỡng là nhảy bậc, không tăng dần.
512 trường Hash, 129 phần tử Set, 513 số nguyên, 45 byte giá trị, 10.001 kết nối — mỗi con số là một vách đá. Thêm một đơn vị và chi phí nhân lên 2 đến 18 lần. Đây là lý do thử nghiệm ở quy mô nhỏ không dự đoán được hành vi ở quy mô lớn. Không có đường cong để ngoại suy; có bậc thang.
3. Redis hiếm khi báo lỗi. Nó chậm dần hoặc lớn dần.
Khoá không TTL, cache script không dọn, danh sách chờ Stream, chỉ mục tự dựng lệch, mã hoá đắt sau khi khoá đã nhỏ lại — không cái nào sinh ra một dòng lỗi. Chúng chỉ tích lại. Hệ quả: giám sát Redis là giám sát xu hướng, không phải chờ báo động.
Danh sách kiểm trước khi đưa lên chạy thật
Cấu hình
-
maxmemoryđã đặt, khoảng 60–70% RAM khả dụng (18, 36) -
maxmemory-policyphù hợp —allkeys-lfucho bộ đệm, không đểvolatile-*khi không có TTL (9) -
requirepasshoặc ACL, vàdefaultđã tắt (32) -
bindkhông phải mọi giao diện; cổng không ánh xạ ra0.0.0.0(36) -
timeoutkhác 0 (31) -
appendonly yesnếu dữ liệu quan trọng (19, 20) -
client-output-buffer-limit normalđã đặt nếu có giá trị lớn (12, 30)
Mô hình dữ liệu
- Mọi khoá tạm có TTL — so
keysvớiexpirestrongINFO keyspace(8, 39) - Dùng Hash cho đối tượng, không một khoá mỗi trường (39)
- Kiểm
OBJECT ENCODINGcủa khoá lớn — có cái nào đang mang mã hoá đắt không cần thiết (5) - Thẻ băm
{}đủ hẹp nếu dùng Cluster (29) - Không giá trị nào trên 1 MB nếu tránh được (30)
Mã ứng dụng
- Bể kết nối, không mở kết nối mỗi lệnh (31)
- Đường ống hoặc
MGET/MSETcho thao tác hàng loạt (12) -
SET ... KEEPTTLkhi ghi đè khoá có hạn (8, 39) -
SCANthayKEYS,UNLINKthayDELcho khoá lớn (11) -
EVALSHAchứ khôngEVALlặp lại; script dùngKEYS/ARGV(14) - Tiêu thụ Stream có gọi
XACK, và có bộ dọn danh sách chờ (7) - Khoá phân tán kiểm giá trị trả về lúc giải phóng (24)
Vận hành
- Đã khôi phục thử từ RDB/AOF ít nhất một lần (20)
- Cảnh báo trên
evicted_keyschứ không chỉ trên phần trăm bộ nhớ (33) -
slowlog-log-slower-thanhạ xuống 1.000 µs (34) -
CLIENT SETNAMEtrong mọi ứng dụng (34) - Biết khoảng cách bản sao tính bằng byte, không chỉ
lag(21)
Ba lần tôi đo sai rồi phải sửa
Phần 2 — tôi chạy khách benchmark trong cùng container với Redis và đo được 181% CPU cho một máy chủ một luồng. Con số bất khả thi, và đó là cách tôi biết docker stats đang cộng cả máy chủ lẫn khách.
Phần 28 — tôi đo bốn chiến lược cập nhật bộ đệm và kết luận "xoá trước khi cập nhật" an toàn (0 lệch). Đo lại với người đọc song song thì nó để lại dữ liệu cũ vĩnh viễn trong một trên ba lần chạy. Phép đo đầu thiếu đúng thành phần làm lộ vấn đề.
Phần 36 — tôi đặt maxmemory 80mb trong container 128 MB và vẫn bị OOMKilled, tưởng đã tìm ra lỗi của Redis. Thực ra là tôi nạp dữ liệu bằng script Lua, và Redis chỉ kiểm maxmemory một lần ở đầu script — điều chính tôi đã đo ở phần 9.
Cả ba đều lộ ra vì cùng một lý do: một con số vượt qua giới hạn vật lý, hoặc không khớp với phép đo trước đó. Khi điều đó xảy ra, hãy nghi phép đo trước khi nghi hệ thống.
Điều còn lại sau bốn mươi phần
Redis là một bảng băm rất nhanh sống trong RAM, có sẵn mạng và vài kiểu dữ liệu dựng sẵn. Mọi thứ khác — độ bền, nhân bản, phân mảnh, truy vấn — là thứ được thêm vào xung quanh cái lõi đó, và mỗi thứ đều có một cái giá đo được.
Bốn mươi phép đo trong sê-ri này bác bỏ ba điều tôi tưởng là hiển nhiên khi bắt đầu: Redis không nhanh hơn PostgreSQL 100 lần (phần 1), appendfsync no không mất dữ liệu khi tiến trình bị giết (phần 19), và TLS không làm chậm khi có đường ống (phần 32).
Cụm của bạn khác cụm của tôi: khác phiên bản, khác phần cứng, khác hình dạng dữ liệu. Mọi con số ở đây nên coi là giả thuyết cần kiểm lại. Mỗi phần đều kết thúc bằng một lệnh chạy được — đó mới là phần quan trọng.
Mẫu số chung: ba quy luật này không chỉ của Redis
Ba điều lặp lại ở trên không phải đặc sản của Redis — chúng là ba sự thật về gần như mọi kho dữ liệu và dịch vụ mạng, và biết chúng áp ở đâu giúp bạn mang cả sê-ri này sang công cụ khác.
"Nút thắt là lượt đi về" đúng ở mọi tầng có độ trễ mạng. Trong Java, đây là N+1 query của JPA và lý do có MGET/batch; trong Go, là gọi API tuần tự thay vì pipelining/errgroup; ở cơ sở dữ liệu SQL, là chênh lệch giữa một IN (...) và N câu SELECT — cùng một bài toán "đếm số lần băng qua ranh giới mạng", giải bằng cùng một cách: gom lô.
"Vượt ngưỡng là nhảy bậc" là chuyện của mọi cấu trúc có biểu diễn thích ứng. Trong Java, HashMap treeify ở 8 phần tử một bucket, ArrayList nhân đôi capacity; trong Go, map rehash theo ngưỡng tải; ở DB, planner lật từ index scan sang seq scan khi selectivity qua một mốc, và một hàng vượt cỡ trang thì tràn TOAST. Thử ở quy mô nhỏ rồi ngoại suy tuyến tính là sai ở mọi chỗ này.
"Chậm dần chứ hiếm khi báo lỗi" đúng cho mọi hệ tích luỹ trạng thái. Trong Java, rò rỉ bộ nhớ/connection không ném exception, nó bò lên rồi mới OOM; trong Go, goroutine leak lặng lẽ; ở DB, bloat của bảng, index phình, thống kê lỗi thời — không cái nào là một dòng lỗi, tất cả là xu hướng. Nên giám sát chúng là giám sát đạo hàm theo thời gian, không phải chờ một ngưỡng đỏ.
Mẫu số chung của cả mẫu số chung: những đặc tính quan trọng nhất của một hệ thống trạng thái — chi phí băng qua ranh giới, hành vi nhảy bậc quanh ngưỡng, và sự tích luỹ âm thầm không báo lỗi — là chung cho cả họ công cụ, không riêng một cái tên; học chúng ở một hệ (Redis) rồi bạn đọc được hệ tiếp theo nhanh hơn nhiều. Nên khi cầm một kho dữ liệu mới, ba câu hỏi đầu tiên nên là "mỗi thao tác tốn mấy lượt đi về, cấu trúc của nó nhảy bậc ở những ngưỡng nào, và cái gì trong nó tích lại mà không kêu" — vì đó là ba chỗ mọi sự cố sản xuất trú ngụ.
Muốn tự chạy lệnh cuối của sê-ri, gộp năm thứ hay sai nhất:
redis-cli info | grep -E 'redis_version|used_memory_human|maxmemory_human|maxmemory_policy|evicted_keys|keyspace_hits|keyspace_misses|connected_clients|rdb_changes_since_last_save|aof_enabled'
echo "---"
redis-cli info keyspace
echo "---"
redis-cli config get requirepass timeout appendonly slowlog-log-slower-than | paste - -
echo "---"
redis-cli info commandstats | grep -E 'cmdstat_(keys|flushall|flushdb):' || echo "khong ai goi KEYS/FLUSH — tot"
Bốn khối, bốn câu hỏi: bộ nhớ có trần chưa, dữ liệu có tự dọn không, có ai vào được không, và có ai đang gọi lệnh nguy hiểm không. Chạy nó trên hệ thống của bạn ngay bây giờ. Với mỗi dòng bất thường, sê-ri này có một phần đo được hậu quả cụ thể của nó.
Cảm ơn bạn đã theo hết bốn mươi phần.