26/11/2025 — 6 phút đọc

How We Crashed Production: The Hidden Cost of Redis 'KEYS' Command and O(N) Complexity

#redis#distributed-cache#cache-invalid#deep-dive


1. Mở đầu: Một lệnh debug “vô hại”.

Đó là một ngày giáp tết, hệ thống Payment Gateway của chúng tôi đang gồng mình xử lý 50k TPS (Transactions Per Second). Dashboard monitoring xanh rờn, CPU ở mức an toàn 40%, Memory ổn định.

Bỗng nhiên một Engineer nhận được report về một vài giao dịch ở trạng thái “Pending”. Cậu ta SSH vào Basion Host, để kết nối vào Redis Master (Production) và gõ một lệnh định mệnh để debug:

KEYS payment:transaction:pending:*

Mục đích rất ngây thơ: chỉ muốn xem có bao nhiêu key đang bị kẹt.

Kết quả:

Trong đúng 2.5 giây tiếp theo, Latency của toàn bộ Payment Service tăng vọt từ 5ms lên... Timeout (30s).

Load Balancer đánh dấu hàng loạt node Backend là "Unhealthy". Circuit Breaker ở các Upstream Service bung ra (Open State). Doanh thu sụt giảm hàng trăm triệu chỉ trong vài phút hỗn loạn.

Tại sao một câu lệnh READ đơn giản, không ghi dữ liệu, lại có thể đánh sập cả một cụm Cluster mạnh mẽ? Bài viết này không nói về việc "đừng dùng KEYS", mà sẽ mổ xẻ tại sao nó giết chết hệ thống của bạn từ tận cùng của OS Kernel.

2. The deep dive: Anatomy of a Disaster

Nhiều Engineer lầm tưởng rằng khi Redis bị block, nó chỉ đơn giản là "chậm trả lời". Thực tế tàn khốc hơn: Redis đã biến thành một "Blackhole" ở tầng mạng.

2.1. The single-threaded Event Loop

Redis (phiên bản < 6.0 và cả core command execution của bản mới) xử lý command đơn luồng. Nó sử dụng cơ chế Event Loop (epoll trên Linux) để tuần tự hóa mọi request.

Lệnh KEYS có độ phức tạp là O(N), với N là tổng số key trong database. Nếu bạn có 50 triệu keys, Redis phải duyệt qua từng key một để so sánh string pattern.

Trong thời gian đó (ví dụ: 2 giây), Main Thread bị Block hoàn toàn. Không một lệnh GET, SET nào khác (dù chỉ mất 1 microsecond) được xử lý. Chúng xếp hàng chờ chết (Head-of-Line Blocking).

2.2. OS & Network Layer Analysis (The Invisible Killer)

Điều gì thực sự xảy ra ở tầng TCP/IP khi Redis block 2 giây?

a. The Buffer Bloat (Recv-Q Explosion)

Các Client (Microservices) không biết Redis đang bận. Chúng vẫn tiếp tục gửi hàng nghìn request mỗi giây.

Tại OS Kernel của Redis Server:

  • NIC nhận packet -> Kernel đẩy vào Receive Buffer của socket.
  • Bình thường, Redis read() liên tục để dọn buffer. Nhưng giờ nó đang bận loop KEYS, không gọi syscall read().
  • Kết quả: Recv-Q đầy tràn. Kernel kích hoạt TCP Zero Window, báo Client ngừng gửi tin. Hệ thống mạng bị tắc nghẽn vật lý.
$ ss -ntl state established sport = :6379
State       Recv-Q  Send-Q Local Address:Port
ESTAB       128560  0      10.0.0.1:6379

b. The TCP Backlog Overflow

Với các kết nối mới (New Connections), vì Redis không gọi accept(), Accept Queue của Kernel bị đầy.

Kernel không thể di chuyển connection từ SYN Queue sang Accept Queue -> Drop SYN Packets.

Client sẽ nhận lỗi Connection Refused hoặc Connection Timed Out.

c. The "Phantom" Timeout

Đây là điểm đau đớn nhất khi debug:

  1. Client gửi GET user:123.
  2. Packet đã chui lọt vào Recv-Q. TCP Layer đã ACK. Client nghĩ Server đã nhận.
  3. Client đếm ngược Timeout (200ms).
  4. Redis vẫn đang chạy KEYS, chưa hề chạm tới lệnh GET kia.
  5. Client ném ReadTimeoutException -> Kích hoạt Retry Storm -> Càng làm ngập Queue -> Cascading Failure.

3. The Trade-offs: No Silver Bullet

Để giải quyết vấn đề này, chúng ta thường nghe đến SCAN hoặc xây dựng Index. Nhưng là một Architect, bạn phải nhìn thấy cái giá phải trả (Trade-offs) của từng giải pháp.

3.1. SCAN: The Pragmatic Fix (and its traps)

SCAN an toàn hơn vì nó dùng cursor để duyệt từng phần nhỏ (non-blocking). Tuy nhiên, nó không hoàn hảo:

  • No Snapshot Isolation: Dữ liệu không nhất quán. Nếu key mới được thêm vào trong khi bạn đang scan, bạn có thể bỏ sót nó.
  • The Duplicate Trap: Đây là vấn đề Internal ít người biết. Khi Redis thực hiện Hash Table Rehashing (mở rộng/thu hẹp bộ nhớ) trong lúc scan, thuật toán của SCAN (Reverse Binary Iteration) có thể trả về một key nhiều lần.
  • Hệ quả: App Code bắt buộc phải xử lý Deduplication (khử trùng lặp) ở phía Client.
  • Network Overhead: Quét 50 triệu keys bằng SCAN tốn hàng chục nghìn Round-trips (RTT), chậm hơn nhiều so với KEYS.

3.2. Secondary Indexing: The Architect's Fix

Tư duy đúng đắn là: Đừng bắt Redis tìm (Search), hãy chỉ cho nó biết dữ liệu ở đâu.

Sử dụng SET hoặc ZSET để lưu danh sách keys cần quan tâm.

  • Write Path: SET tx:123 + SADD tx:pending 123.
  • Read Path: SMEMBERS tx:pending.

Trade-offs:

  1. Complexity: Phải xử lý Double Write.
  2. Atomicity: Phải dùng Lua Script hoặc MULTI/EXEC để đảm bảo transaction (tránh trường hợp lưu data thành công nhưng lưu index thất bại -> Orphaned Data).
  3. Hot Key Problem: Nếu cái Set tx:pending chứa 10 triệu items, lệnh SMEMBERS lại trở thành một lệnh O(N) gây block hệ thống y hệt KEYS. Lúc này, bạn lại phải Sharding cái Index đó.

4. Key Takeaways

Nếu bạn là Tech Lead hoặc Architect, đây là những việc cần làm ngay hôm nay:

  1. Defense in Depth (Hard Limit): Đừng tin vào "kỷ luật" của developer. Hãy chặn ngay từ config. Trong file redis.conf, hãy đổi tên lệnh KEYS:

    rename-command KEYS "disable_keys_prod_b840fc"
    # Hoặc disable hoàn toàn:
    rename-command KEYS ""
    
  2. Monitor the Right Metrics: Đừng chỉ nhìn CPU. Hãy setup alert cho latency-monitor và đặc biệt là Command Duration. Nếu thấy biểu đồ Command/sec có những cú "dip" (tụt xuống) đột ngột, đó là dấu hiệu của Blocking.

  3. Architectural Mindset: Redis là Key-Value Store, không phải Relational Database. Đừng cố query nó như SELECT * FROM table WHERE key LIKE %.... Nếu Use-case của bạn cần Query phức tạp, hãy đẩy dữ liệu sang Elasticsearch hoặc sử dụng RediSearch.

    Lời kết

    Sự ổn định của hệ thống phân tán không được định nghĩa bởi lúc nó chạy nhanh nhất, mà bởi cách nó hành xử khi gặp một câu lệnh tồi tệ nhất. Một lệnh KEYS sai lầm có thể dạy cho chúng ta bài học đắt giá về sự mong manh của High Availability.


aitu avatar

Hi! Tôi là Tuyên — Hiện tại tôi đang làm Software Architect, Senior developer tại một công ty nhỏ ở Hà Nội. Tôi cảm thấy thích thú, đam mê, yêu thích với việc viết lách và chia sẻ những kiến thức mà tôi biết. Hãy đọc nhiều hơn tại blogs và tới about để biết thêm về tôi nhé.