Bỏ qua để đến nội dung
Knowledge Hub

Redis single-threaded nghĩa là gì khi có I/O threads? Cơ bản

Thời gian đọc ước tính: 7 phút

Endpoint lấy profile bằng GET thường rất nhanh. Một hôm p99 tăng mạnh dù request không đổi và mạng sạch. Nguyên nhân: một client khác đang chạy lệnh xử lý tập dữ liệu lớn. Command trên một Redis instance chạy tuần tự, nên GET rẻ vẫn phải chờ đến lượt.

“Redis single-threaded” đúng cho thực thi command, nhưng không phải toàn bộ process:

Thành phần Thread
Thực thi command (đọc/ghi keyspace, Lua) Main thread, từng command một
Việc nền (fsync AOF, đóng file, lazy free) Background threads
Đọc/ghi socket, parse protocol Tuỳ chọn I/O threads (io-threads)
Redis 7 (bài dùng trong demo) Redis 8 / Valkey 8
I/O threading Có từ Redis 6, mặc định tắt Làm lại theo mô hình bất đồng bộ
io-threads Mặc định tắt (redis.conf: “By default threading is disabled”) Bật bằng io-threads N
Ai chạy command Main thread Vẫn main thread

Theo blog Redis về bản 8.0, luồng xử lý là:

  1. Main thread gán client cho các I/O thread.
  2. I/O thread đọc và parse query rồi báo main thread.
  3. Main thread xử lý query và sinh reply.
  4. I/O thread ghi reply ra socket.

io-threads mặc định là 1 (chỉ main thread). Valkey 8 cũng đổi cách dùng thread (thông báo GA của Valkey). Con số throughput phụ thuộc phần cứng và workload; bài không trích.

Kết luận không đổi với mọi phiên bản: I/O threads tăng khả năng xử lý socket khi nhiều connection; chúng không chia nhỏ một Lua script hay SMEMBERS trên set lớn thành nhiều phần chạy song song. Bật io-threads khi CPU main thread bị socket I/O chiếm, không phải để chữa lệnh chậm.

Một command chậm trên Redis làm PING của client khác chờ
Một command chậm trên Redis làm PING của client khác chờBước 1/4: Client A gửi một lệnh phải quét nhiều phần tử. Lệnh bắt đầu chiếm luồng thực thi command của Redis.Client ARedis serverClient Blệnh chậm
Client A → Redis serverlệnh chậm
Bước 1/4

Client A gửi một lệnh phải quét nhiều phần tử. Lệnh bắt đầu chiếm luồng thực thi command của Redis.

Thời gian B đo = thời gian xếp hàng + thời gian xử lý + mạng + scheduling của client. PING không chậm; nó chờ.

Tách hai khái niệm: concurrency của kết nối (nhiều client cùng chờ I/O) và parallelism của command (nhiều command dùng CPU cùng lúc). Redis có cái trước, không có cái sau trên một instance.

Atomic cho từng command, không cho cả chuỗi

Phần tiêu đề “Atomic cho từng command, không cho cả chuỗi”

INCR counter là atomic: hai client cùng gửi không mất lần tăng nào. Nhưng GET, tính trong code, rồi SET là hai command, và client khác chen vào giữa được. Cần nhiều bước thì dùng command chuyên dụng, MULTI/EXEC có WATCH, hoặc Lua script. Lua chạy atomic, đổi lại mọi client khác đợi hết script.

Script không chặn vô hạn. Sau busy-reply-threshold (alias lua-time-limit, mặc định 5 giây theo redis.conf), Redis bắt đầu trả lỗi BUSY cho client khác.

  • Script chưa ghi gì vào dataset: dùng được SCRIPT KILL.
  • Script đã ghi: cách duy nhất là SHUTDOWN NOSAVE.

Giữ script ngắn hơn nhiều so với 5 giây: 5 giây là ngưỡng cuối cùng, không phải ngân sách.

Lệnh Vấn đề Thay bằng
KEYS pattern Quét toàn bộ keyspace trong một lần SCAN (theo từng lô)
SMEMBERS, HGETALL trên collection lớn Trả hết phần tử một lần SSCAN, HSCAN, hoặc giới hạn kích thước collection
DEL key rất lớn Giải phóng đồng bộ trên main thread UNLINK (giải phóng ở background)
FLUSHALL/FLUSHDB Xoá đồng bộ thêm ASYNC
Lua script nặng Chặn mọi client Chia lô, giữ script ngắn, đo thời gian
Giao dịch nhiều GET-SET Race giữa các command INCR/command phù hợp, WATCH, Lua ngắn

Tài liệu latency của Redis gọi KEYS là nguồn gây latency rất phổ biến ở production và chỉ nên dùng để debug.

Công cụ Cho biết Giới hạn
SLOWLOG GET 5 Command vượt ngưỡng thời gian thực thi Không gồm thời gian mạng; không phải lịch sử đầy đủ
INFO commandstats Số lần gọi và usec_per_call theo tên command Không chỉ ra key nào gây chậm
LATENCY DOCTOR Mô tả các sự kiện latency Redis ghi nhận Cần bật latency monitor
redis-cli --bigkeys Key lớn theo từng kiểu Quét keyspace, chạy cẩn thận

SLOWLOG sạch mà client vẫn chậm thì kiểm tra mạng, event loop phía ứng dụng, connection pool và payload lớn.

Demo chạy một Lua loop CPU-bound ngắn trên Redis local; không chạy trên Redis dùng chung hoặc production.

Terminal window
docker compose -f examples/docker-compose.yml up -d redis
npx --prefix examples ts-node examples/src/redis-event-loop/command-queue-demo.ts
docker compose -f examples/docker-compose.yml exec -T redis redis-cli SLOWLOG GET 5

Connection A chạy Lua loop chỉ tính số; 10 ms sau, B gửi PING và script in pingWaitMs rồi xác nhận Lua đã xong. Nếu vòng lặp kết thúc trước khi B gửi, thời gian đo gần như bình thường; tăng số vòng trên local để thấy hàng đợi.

Demo cần Docker nên không chạy lại trong lần rà soát này và bài không đính kèm output; chạy trên máy bạn để xem pingWaitMs.

Điểm mạnh của mô hình

  • Mỗi command atomic rõ ràng trong một instance, hợp cho counter và thao tác ngắn
  • Ít overhead đồng bộ giữa các worker thực thi
  • I/O threads và background work tăng throughput socket mà không đổi mô hình command

Điểm nghẽn phải quản lý

  • Command hoặc Lua chạy lâu kéo tail latency của mọi client
  • Một instance không dùng nhiều core cho logic command; muốn scale cần cluster/sharding hoặc tách workload
  • Atomic của một command không thay transaction nghiệp vụ nhiều bước
  1. Không dùng KEYS trong code ứng dụng; dùng SCAN.
  2. Dùng UNLINK thay DEL cho key lớn; FLUSH* kèm ASYNC.
  3. Giới hạn kích thước collection; duyệt bằng *SCAN.
  4. Lua script ngắn, đo thời gian; coi busy-reply-threshold chỉ là lưới an toàn.
  5. Bật io-threads chỉ khi đo thấy CPU main thread bị socket I/O chiếm (đa connection).
  6. Theo dõi p99 phía client cùng SLOWLOG và INFO commandstats.
  7. Tách workload báo cáo/batch khỏi instance phục vụ request.

Tự kiểm tra: (1) Vì sao PING có thể chậm khi client khác chạy EVAL? (2) SCRIPT KILL dùng được khi nào? (3) Nếu SLOWLOG trống nhưng request vẫn chậm, kiểm tra thêm tầng nào?