10 phút đọc
Insights /

Case Study

Redis vẫn UP nhưng application latency tăng từ 50 ms lên 5 giây

Lê Hoàng
Lê Hoàng Tác giả
Đăng ngày 17/08/2026 Senior Site Reliability Engineer
Redis vẫn UP nhưng application latency tăng từ 50 ms lên 5 giây

Mình gặp case này trong một ngày khá bình yên cuối tuần không có up code :D… Trông hệ thống lúc xem dashboard của Redis vẫn hiển thị trạng thái bình thường:

redis_up = 1
used_memory = 64%
cpu = 38%

Port 6379 vẫn mở, kết nối TCP thông suốt, gõ lệnh redis-cli PING vẫn nhận về PONG ngay lập tức. Không có pod hay container nào bị restart, cũng không có cảnh báo OOM (Out Of Memory).

Tuy nhiên ở phía ứng dụng, hệ thống giám sát (APM) và API Gateway lại bắt đầu ghi nhận bất thường:

HTTP 504 Gateway Timeout: 0.1% -> 4.8%
API p95 Latency:          45 ms -> 1.8 s -> 5.2 s
Active HTTP Threads:      120 -> 1800 (Pool Exhaustion)

Không còn đủ thread khả dụng để xử lý request, connection pool bị sử dụng hết, kéo theo timeout ở nhiều service liên quan.

Vậy là câu hỏi rõ ràng mọi người có thể thấy ngay là Redis vẫn đang chạy bình thường, tại sao ứng dụng lại bị chậm như vậy?

Thực tế, đây là một vấn đề khá phổ biến: tiến trình Redis vẫn hoạt động, nhưng tốc độ xử lý hoặc hàng đợi bên trong đã bị nghẽn.

Dưới đây là phân tích nguyên nhân và các bước kiểm tra, mong rằng giúp ích nếu bạn gặp phải nhé.

Tại sao PING trả về PONG nhưng ứng dụng vẫn chậm?

Nhiều hệ thống giám sát và liveness probe mặc định dùng lệnh redis-cli PING để kiểm tra health của Redis. Khi thấy phản hồi PONG, hệ thống ghi nhận Redis vẫn ổn định.

Tuy nhiên, PING là một lệnh rất nhẹ với độ phức tạp $O(1)$. Phản hồi PONG chỉ xác nhận socket TCP còn kết nối và tiến trình Redis chưa dừng, chứ chưa phản ánh được các yếu tố sau:

  1. Event Loop đang bị nghẽn: Redis xử lý logic thực thi lệnh trên một event Llop đơn luồng. Mọi request gửi đến đều phải xếp hàng xử lý tuần tự.
  2. CPU 38% thực chất là 1 Core đã chạm mức 100%: Trên máy chủ 4 vCPU, nếu core chạy Main Thread của Redis hoạt động hết công suất (100%), chỉ số CPU tổng hiển thị trên dashboard giám sát chung chỉ ở mức khoảng 25% – 38%.
  3. Queueing Latency: Một câu lệnh GET thông thường chỉ tốn vài micro-giây, nhưng nếu phải đứng sau một câu lệnh xử lý mất 400ms thì request đó vẫn phải chờ đủ 400ms mới đến lượt xử lý.
Client A (GET key1: 10µs)        ──> [                     ]
Client B (HGETALL big_cart: 400ms) ─> [ Redis Main Thread   ] ──> Trả kết quả
Client C (GET key2: 10µs)        ──> [ (Chờ 400ms trong queue) ]

Khi Client C (và hàng loạt request tiếp theo) phải chờ:

  • Các thread hoặc goroutine bên phía ứng dụng bị giữ lại để đợi kết quả từ Redis.
  • Connection pool phía ứng dụng nhanh chóng cạn kiệt, dẫn đến việc quá tải cục bộ ở tầng Web/API.

3 bước kiểm tra nhanh khi gặp sự cố

Khi phát hiện latency tăng cao, chúng ta không nên vội restart Redis ngay lập tức. Việc restart sẽ làm mất toàn bộ cache trên RAM, khiến lưu lượng truy cập dồn thẳng xuống cơ sở dữ liệu và dễ gây quá tải hệ thống phía sau.

Thay vào đó, bạn có thể thực hiện kiểm tra nhanh theo 3 bước:

Bước 1: Đo độ trễ thực tế bằng --latency

Chạy lệnh kiểm tra từ chính server/pod của ứng dụng trỏ về Redis để đo độ trễ mạng và thời gian phản hồi:

# Đo độ trễ liên tục
redis-cli -h <redis-host> -p 6379 --latency

# Hoặc theo dõi độ trễ theo từng khoảng thời gian (15 giây lấy mẫu 1 lần)
redis-cli -h <redis-host> -p 6379 --latency-history -i 15
  • Nếu latency cao (> 20ms – 50ms): Vấn đề nằm ở phía Redis Server (Event Loop bị nghẽn, bận Network I/O hoặc CPU của core chính chạm ngưỡng).
  • Nếu latency < 1ms nhưng ứng dụng vẫn timeout: Vấn đề thường do tầng ứng dụng (cạn connection pool, lock contention trong code, hoặc nghẽn băng thông giữa ứng dụng và Redis).

Bước 2: Tìm các câu lệnh chạy chậm bằng SLOWLOG

Redis tích hợp sẵn bộ đệm ghi lại các câu lệnh có thời gian thực thi vượt ngưỡng cấu hình (mặc định là > 10ms):

redis-cli SLOWLOG GET 10

Output (sample nhé):

1) 1) (integer) 342
   2) (integer) 1718012450
   3) (integer) 385420            # Thời gian thực thi trên CPU: 385,420 µs (~385 ms)
   4) 1) 'HGETALL'                # Câu lệnh thực thi
      2) 'ecommerce:cart:all'     # Key được gọi
   5) '10.0.4.15:42180'           # IP của client gửi lệnh

Lưu ý quan trọng về SLOWLOG:

  • Có ghi lại: Thời gian CPU trực tiếp xử lý lệnh (Execution Time).
  • Không ghi lại: Thời gian request đứng xếp hàng chờ (Queue Time) hoặc thời gian truyền qua mạng (Network I/O).

Ví dụ thực tế: Một lệnh GET chỉ tốn micro giây thực thi nên sẽ không xuất hiện trong SLOWLOG, dù phía ứng dụng vẫn bị timeout do phải đứng chờ sau một lệnh HGETALL mất 385ms.

Bước 3: Đánh giá các chỉ số tài nguyên qua INFO

Với vài lệnh cơ bản, bạn có thể nắm được các thông số quan trọng của Redis tại thời điểm xảy ra vấn đề:

redis-cli INFO stats
redis-cli INFO clients
redis-cli INFO memory

Các chỉ số đáng lưu ý:

  • instantaneous_ops_per_sec: Lưu lượng QPS hiện tại có tăng đột biến so với mức trung bình không?
  • connected_clients: Số lượng kết nối từ các client có tăng bất thường (nghi ngờ connection leak)?
  • rejected_connections: Đã có kết nối nào bị từ chối do chạm ngưỡng maxclients chưa?
  • latest_fork_usec: Thời gian thực hiện thao tác fork() gần nhất (nếu vượt quá 100,000 µs tức là main thread đã bị block hơn 100ms do tiến trình snapshot dữ liệu).
  • mem_fragmentation_ratio: Nếu chỉ số phân mảnh bộ nhớ quá cao (> 1.5) hoặc bộ nhớ bị chuyển vào swap disk, hiệu năng đọc ghi sẽ suy giảm rõ rệt.

Các nguyên nhân phổ biến và giải pháp xử lý

1. Quét dữ liệu trên Big Key

Khi lưu dữ liệu lớn vào Hash, Set hay ZSet, việc gọi các lệnh quét toàn bộ như HGETALL, SMEMBERS, LRANGE 0 -1 sẽ khiến Redis mất nhiều thời gian xử lý và làm nghẽn các request khác trong hàng đợi.

Cách xử lý:

  • Đọc phân trang: Dùng HSCAN thay cho HGETALL, dùng SSCAN thay cho SMEMBERS.
  • Xóa an toàn bằng UNLINK: Tránh dùng DEL với key lớn vì sẽ block Redis khi giải phóng bộ nhớ. Dùng UNLINK để xóa ngầm ở background thread:
    redis-cli UNLINK ecommerce:cart:all
  • Tìm Big Key an toàn: Thêm cờ -i 0.05 để quét chậm, tránh làm tăng tải hệ thống:
    redis-cli -h 
    <host> --bigkeys -i 0.05

2. Hàng loạt key cùng hết hạn (TTL Storm & Cache Breakdown)

Khi đặt TTL cố định (ví dụ EX 3600), có 2 tình huống dễ phát sinh:

  • Hàng loạt key hết hạn cùng lúc: Redis chạy dọn rác đồng bộ trên main thread và tạm dừng xử lý request nếu phát hiện hơn 25% số key kiểm tra đã hết hạn.
  • Hot Key bị hết hạn: Một key có lượng truy vấn rất lớn vừa hết hạn, hàng nghìn request đồng thời dồn thẳng xuống Database.

Cách xử lý:

  • Thêm Jitter vào TTL: Phân tán thời điểm hết hạn bằng cách cộng thêm khoảng thời gian ngẫu nhiên:
    base_ttl = 3600
    jitter = random.randint(0, 300)  # Cộng ngẫu nhiên từ 0 đến 5 phút
    redis_client.set('product:123', data, ex=base_ttl + jitter)
  • Dùng Mutex Lock (SingleFlight): Khi cache miss, chỉ cho phép duy nhất 1 thread truy vấn Database để nạp lại cache.

3. Độ trễ do tiến trình snapshot dữ liệu (fork() Latency)

Khi sao lưu dữ liệu ra disk (RDB snapshot hoặc AOF rewrite), Redis gọi fork() để tạo tiến trình con. Trên máy chủ RAM lớn hoặc môi trường ảo hóa Cloud, thao tác copy Page Table này có thể khiến main thread của Redis bị dừng xử lý tạm thời từ vài chục mili-giây đến vài giây.

Cách xử lý:

  • Kiểm tra thời gian fork gần nhất bằng lệnh: redis-cli INFO stats | grep latest_fork_usec.
  • Tắt tự động lưu RDB trên Master (save "" trong redis.conf) và chuyển việc snapshot sang Replica node.
  • Tắt tính năng Transparent Huge Pages (THP) trên Linux:
    echo never > /sys/kernel/mm/transparent_hugepage/enabled

4. Cấu hình Connection Pool và Timeout chưa hợp lý

Redis xử lý lệnh chỉ mất 1 – 2 ms, nhưng connection pool phía ứng dụng quá nhỏ khiến các thread phải xếp hàng chờ kết nối. Khi timeout xảy ra, client lại gửi lại liên tục (Retry Storm) làm nghẽn thêm hệ thống.

Cách xử lý:

  • Command Timeout: Đặt ngắn từ 50ms - 100ms để giải phóng thread sớm nếu cache không phản hồi.
  • Pool Acquire Timeout: Đặt 50ms - 100ms, trả về lỗi ngay nếu hết kết nối thay vì để thread tiếp tục chờ.
  • Circuit Breaker: Tạm ngắt truy vấn cache hoặc trả về fallback data khi tỷ lệ lỗi từ cache tăng cao.

5. Quá tải Network I/O

Ở hệ thống có hàng chục nghìn QPS trở lên, việc đọc/ghi dữ liệu qua socket mạng có thể chiếm phần lớn thời gian CPU của Redis, dù bản thân các câu lệnh thực thi rất nhẹ ($O(1)$).

Cách xử lý: Bật tính năng Multi-threaded I/O trong redis.conf (hỗ trợ từ Redis 6.0+):

io-threads 3            # Số luồng xử lý I/O (thường đặt bằng vCPU - 1 hoặc vCPU / 2)
io-threads-do-reads yes # Đa luồng cả tác vụ đọc socket từ client

(Logic thực thi dữ liệu vẫn giữ nguyên cơ chế đơn luồng an toàn, các luồng io-threads chỉ phụ trách nhận/gửi dữ liệu qua mạng).

Kết luận

Khi vận hành Redis, trạng thái redis_up = 1 hay phản hồi PONG chỉ cho biết tiến trình dịch vụ vẫn đang hoạt động, chứ chưa đảm bảo Redis đang phản hồi nhanh.

Bản chất của Redis là xử lý logic đơn luồng. Một câu lệnh đọc Big Key, việc hàng loạt key cùng hết hạn, một lệnh fork() snapshot kéo dài hay cấu hình Connection Pool chưa phù hợp đều có thể khiến toàn bộ các request phía sau bị nghẽn lại.

Hiểu rõ cơ chế hoạt động, kiểm tra đúng chỉ số qua SLOWLOG / INFO và chủ động thiết kế timeout hợp lý, thêm jitter cho TTL sẽ giúp hệ thống luôn hoạt động ổn định kể cả trong các thời điểm lưu lượng truy cập tăng cao.

Chia sẻ bài viết

Theo dõi
Thông báo của
0 Góp ý
Được bỏ phiếu nhiều nhất
Mới nhất Cũ nhất
Đã sao chép liên kết vào bộ nhớ tạm!