Phần 2 đo được Redis xử lý lệnh bằng một luồng duy nhất. Bài này đo hậu quả: một lệnh chậm làm gì với mọi khách hàng khác.
Phép thử
Hai khách hàng: một gõ PING liên tục và ghi lại độ trễ, một gõ lệnh nặng. Cơ sở dữ liệu có 2 triệu khoá.
| Lệnh nặng chạy song song | Lệnh đó mất | PING p99 |
PING max |
|---|---|---|---|
| (không có gì) | — | 0,344 ms | 1,0 ms |
KEYS 'k:1*' |
0,42 s | 0,924 ms | 216,7 ms |
KEYS '*' |
0,60 s | 1,577 ms | 263,8 ms |
SCAN --count 1000 |
0,95 s | 0,561 ms | 4,2 ms |
Một lệnh KEYS đẩy độ trễ tệ nhất từ 1 ms lên 263,8 ms.
Trong 263 mili giây đó, không khách hàng nào được phục vụ. Với một hệ thống chạy 50.000 lệnh mỗi giây, đó là khoảng 13.000 yêu cầu xếp hàng chờ — và mỗi cái trong số đó là một người dùng đang đợi.
Đó là lý do KEYS không nên gọi là "lệnh chậm". Nó là lệnh làm sập dịch vụ.
SCAN chậm hơn mà tốt hơn
SCAN mất 0,95 giây để duyệt hết 2 triệu khoá — chậm hơn 58% so với KEYS '*'.
Nhưng độ trễ tệ nhất chỉ 4,2 ms, ít hơn 63 lần.
Lý do: SCAN trả về một lô nhỏ rồi nhường lại luồng. Mỗi lô là một lệnh riêng, và giữa các lệnh Redis phục vụ mọi khách hàng khác. Tổng công việc nhiều hơn — phải quản lý con trỏ, phải quay lại nhiều lần — nhưng nó chia nhỏ ra.
Đây là đánh đổi đúng gần như luôn luôn: thông lượng tổng ít quan trọng hơn độ trễ tệ nhất, vì độ trễ tệ nhất là thứ người dùng cảm nhận.
Tham số COUNT điều chỉnh mức chia: lớn hơn thì duyệt nhanh hơn nhưng mỗi lô chặn lâu hơn. Mặc định là 10 — thường quá nhỏ; 100 đến 1000 là khoảng hợp lý.
Ba điều cần biết về SCAN:
- Nó không bảo đảm không lặp: một khoá có thể xuất hiện hai lần. Xử lý phải chịu được điều đó.
- Nó bảo đảm mọi khoá tồn tại suốt quá trình duyệt sẽ được trả về ít nhất một lần.
MATCHlọc sau khi lấy lô, nênSCAN MATCH 'hiem:*' COUNT 10vẫn phải duyệt hết bảng và trả về gần như toàn lô rỗng. Mẫu hiếm thì tăngCOUNT.
DEL còn tệ hơn KEYS
Một Hash 3.000.000 trường, 194 MB:
DEL 0,54 giây PING max 467,7 ms
UNLINK 0,08 giây PING max 3,6 ms
DEL chặn 467,7 ms — tệ hơn cả KEYS '*'.
Lý do: giải phóng 3 triệu đối tượng là 3 triệu lần gọi free, và tất cả chạy trên luồng xử lý lệnh. Xoá là công việc, không phải phép màu.
UNLINK gỡ khoá khỏi không gian tên ngay lập tức rồi giao việc giải phóng cho luồng nền. Khoá biến mất tức thì với mọi khách hàng; bộ nhớ trả về vài mili giây sau.
Đổi DEL thành UNLINK là một lần tìm-thay-thế trong mã nguồn, và nó bỏ đi 130 lần độ trễ chặn. Với khoá nhỏ, hai lệnh hoàn toàn như nhau — Redis tự chọn giải phóng ngay khi đối tượng đủ bé.
Tương tự với FLUSHALL và FLUSHDB: cả hai nhận ASYNC.
Danh sách lệnh cần tránh trong production
| Lệnh | Thay bằng |
|---|---|
KEYS pattern |
SCAN |
SMEMBERS trên tập lớn |
SSCAN |
HGETALL trên hash lớn |
HSCAN, hoặc HMGET đúng trường cần |
LRANGE key 0 -1 |
LRANGE theo trang |
DEL khoá lớn |
UNLINK |
FLUSHALL |
FLUSHALL ASYNC |
SORT trên tập lớn |
Sắp xếp ở phía ứng dụng |
| Script Lua vòng lặp dài | Chia nhỏ thành nhiều lần gọi |
Điểm chung: mọi lệnh có độ phức tạp O(N) với N là số phần tử trong khoá hoặc số khoá trong cơ sở dữ liệu đều là ứng viên. Tài liệu Redis ghi độ phức tạp cho từng lệnh — đó là chỗ đáng đọc trước khi dùng lệnh lạ.
Và Lua đáng nhắc riêng: script chạy nguyên tử, nghĩa là không lệnh nào khác chen vào được suốt thời gian nó chạy. Một vòng lặp 100.000 vòng trong Lua chặn đúng như một lệnh KEYS.
Tìm lệnh chậm trước khi người dùng tìm ra
Redis có sẵn nhật ký lệnh chậm:
redis-cli config set slowlog-log-slower-than 10000 # microgiây, tức 10 ms
redis-cli slowlog get 10
redis-cli slowlog reset
Mặc định là 10.000 µs. Với hệ thống nhạy độ trễ, hạ xuống 1.000 µs (1 ms) để bắt sớm hơn.
Và --latency cho bức tranh tổng:
redis-cli --latency # theo dõi liên tục
redis-cli --latency-history # theo từng cửa sổ 15 giây
Con số max ở đây chính là thứ tôi đo trong bài. Nếu nó nhảy lên hàng trăm mili giây theo chu kỳ, hãy tìm công việc định kỳ nào đó — thường là một script dọn dẹp chạy KEYS.
Một chỗ dễ nhầm
Độ trễ trung bình hầu như không đổi trong mọi phép đo ở trên: p50 dao động 0,074 đến 0,123 ms.
Nếu bảng điều khiển của bạn chỉ vẽ độ trễ trung bình, một lệnh KEYS chạy mỗi phút sẽ hoàn toàn vô hình — nó chỉ ảnh hưởng vài chục yêu cầu trong hàng chục nghìn.
Nhưng vài chục người dùng đó chờ một phần tư giây. Luôn đo p99 và max.
Thử ba mươi giây
Kiểm xem có ai đang gọi lệnh nguy hiểm trên hệ thống của bạn:
# theo doi truc tiep trong 10 giay, loc lenh dang ngo
timeout 10 redis-cli monitor | grep -iE '"(keys|flushall|flushdb|smembers|hgetall|lrange|sort|del)"' | head -20
# va xem nhat ky lenh cham
redis-cli config set slowlog-log-slower-than 1000
sleep 60
redis-cli slowlog get 20
MONITOR cũng tốn tài nguyên vì nó nhân bản mọi lệnh tới khách hàng của bạn — chạy vài giây rồi tắt, đừng để mở. Mỗi dòng in ra kèm tên khoá và địa chỉ khách hàng, đủ để tìm ra mã nguồn nào đang gọi.
Phần sau: đường ống — đo chính xác nó bỏ đi bao nhiêu lượt đi về, và giới hạn của nó ở đâu.