Danh sách bài viết

Bài 18: Hot Keys — Nhận Diện & Xử Lý Cơ Bản

Hot key xảy ra khi một số rất ít key nhận phần lớn traffic Redis — thường do sự kiện bất thường như user nổi tiếng, sản phẩm trending, hoặc nội dung viral. Hậu quả là một Redis instance hoặc một node trong Cluster bị đẩy lên CPU 100% trong khi phần còn lại idle. Bài này đi qua nguyên nhân cấu trúc (80/20 rule, single-threaded event loop, hash slot trong Cluster), bốn phương pháp detect hot key với ưu/nhược điểm của từng cách, ba chiến lược xử lý cơ bản (L1 cache local, replicate key thành nhiều bản, read replica), pattern sampling để phát hiện hot key không làm chậm Redis, hash tag trong Cluster, và anti-pattern cần tránh. Scaling chuyên sâu (client-side caching RESP3, dedicated proxy, sharding strategy) sẽ ở Module 9.

28/05/2026
0 lượt xem
1

Mục Tiêu Bài Học

  • Hiểu cơ chế vì sao hot key gây bottleneck trong Redis single-threaded và trong Redis Cluster.
  • Nhận biết được các triệu chứng hot key qua metrics CPU, latency, và log ứng dụng.
  • Biết dùng redis-cli --hotkeys, MONITOR, INFO commandstats và phân biệt khi nào nên dùng công cụ nào.
  • Triển khai được pattern sampling ở application layer để detect hot key mà không làm chậm Redis runtime.
  • Nắm ba chiến lược xử lý cơ bản: L1 cache local, key replication, read replica — cùng trade-off của mỗi chiến lược.
  • Biết dùng hash tag {} trong Redis Cluster để kiểm soát hash slot của hot key.
  • Nhận diện anti-pattern phổ biến khi xử lý hot key.
2

Vấn Đề: Khi 1 Key Chiếm Phần Lớn QPS

Trong hầu hết hệ thống production, traffic phân bổ không đồng đều. Quy tắc 80/20 (Pareto) xuất hiện thường xuyên: 20% key nhận 80% request đọc. Phân bổ lệch này là bình thường và thường không gây vấn đề — vì "20% key" vẫn là hàng chục nghìn key, mỗi key nhận một lượng traffic vừa phải.

Vấn đề xảy ra ở mức cực đoan hơn: 1 hoặc vài key nhận 30-50% toàn bộ QPS Redis. Một số tình huống dẫn đến điều này:

  • User nổi tiếng (celebrity account): profile page của họ được render cho hàng triệu follower cùng lúc.
  • Sản phẩm trending: flash sale, sản phẩm viral trên mạng xã hội.
  • Cấu hình hệ thống toàn cầu: một key config được mọi request đọc qua.
  • Counter/Leaderboard phổ biến: bảng xếp hạng mà mọi trang đều hiển thị.

Tại sao đây là vấn đề cấu trúc của Redis? Hai lý do:

Redis single-threaded (một luồng cho command processing): mặc dù Redis 6+ có I/O threads, logic xử lý command vẫn chạy trên 1 luồng. CPU của luồng đó là tài nguyên dùng chung cho mọi key. Khi 1 key chiếm 50% QPS, nó tiêu thụ ~50% CPU của luồng đó, khiến các key khác phải xếp hàng chờ.

Redis Cluster và hash slot: mỗi key thuộc về đúng 1 hash slot trong 16384 slot, và mỗi slot thuộc về đúng 1 node. Nếu hot key nằm ở node A, thì node A chịu toàn bộ tải từ key đó trong khi các node B, C, D idle — dù bạn đã scale Cluster lên nhiều node.

# Minh hoạ: Cluster 4 node, 1 hot key
# Node A: CPU 95%  (key user:celebrity nằm ở đây)
# Node B: CPU 18%
# Node C: CPU 22%
# Node D: CPU 20%
#
# Thêm node E, F vào Cluster: không giúp ích gì
# vì hot key vẫn chỉ nằm ở 1 node
3

Triệu Chứng Nhận Biết Hot Key

Hot key thường không tự báo thẳng; bạn nhận ra nó qua sự kết hợp của các signal:

  • 1 Redis instance CPU ~100%, các instance khác thấp: trong setup nhiều Redis instance hoặc Cluster nhiều node, nếu 1 node CPU cao bất thường, hot key là nghi phạm hàng đầu.
  • P99 latency spike trên 1 node cụ thể: các node khác latency bình thường. Xem LATENCY HISTORY trên từng node để so sánh.
  • Application log cho thấy 1-2 key bị query lặp lại: nếu bạn log cache key mỗi request (kể cả dưới dạng sampling), top key sẽ nổi lên rõ ràng.
  • redis-cli --hotkeys báo key cụ thể: công cụ built-in kể từ Redis 4.
  • DB tải bình thường nhưng Redis tải cao: ngược lại với cache miss storm — đây là Redis phải xử lý quá nhiều hit trên cùng 1 key.

Điểm phân biệt hot key với các vấn đề Redis khác:

# Big key:     1 key có value rất lớn (vài MB)   → bandwidth cao
# Hot key:     1 key có số lần truy cập rất cao   → CPU cao, queue dài
# Cache miss storm: nhiều key cùng miss           → DB tải cao, Redis tải thấp
# Hot key:     nhiều key cùng hit 1 key           → Redis tải cao, DB tải thấp
4

Công Cụ Detect Hot Key

a) redis-cli --hotkeys (Redis 4+)

Công cụ built-in, scan keyspace và báo các key có LFU (Least Frequently Used) hit count cao nhất.

redis-cli --hotkeys
# Kết quả mẫu:
# -------- summary -------
#
# Sampled 12843 keys in the keyspace!
# hot key found with counter: 8192 keyname: user:98765
# hot key found with counter: 4096 keyname: config:global
# hot key found with counter: 2048 keyname: product:trending:1

Điều kiện bắt buộc: maxmemory-policy phải được đặt thành một giá trị LFU — allkeys-lfu hoặc volatile-lfu. Redis dùng LFU counter để theo dõi tần suất truy cập; nếu dùng LRU policy, counter không được cập nhật và --hotkeys sẽ không cho kết quả hữu ích.

# Kiểm tra policy hiện tại
redis-cli CONFIG GET maxmemory-policy

# Đổi sang LFU (không cần restart, có hiệu lực ngay)
redis-cli CONFIG SET maxmemory-policy allkeys-lfu

Hạn chế: --hotkeys phải scan toàn bộ keyspace bằng SCAN. Trên DB có hàng chục triệu key, quá trình này mất nhiều giây đến vài phút và vẫn tiêu thụ CPU Redis trong thời gian đó. Không chạy thường xuyên trên production; dùng cho debug chủ động khi đã có nghi ngờ.

b) MONITOR (chỉ debug ngắn hạn)

MONITOR stream tất cả command Redis đến client. Kết hợp với awk + sort để group by key:

redis-cli MONITOR | head -1000 | awk '{print $4}' | sort | uniq -c | sort -rn | head -20
# Giải thích:
# head -1000    → chỉ lấy 1000 dòng (không chạy liên tục)
# awk '{print $4}' → lấy argument đầu tiên (thường là key)
# sort | uniq -c | sort -rn → đếm và xếp giảm dần

Cảnh báo quan trọng: MONITOR phải serialize và gửi mọi command đến client, tạo ra overhead đáng kể. Theo Redis docs, MONITOR có thể giảm throughput xuống còn 50% trên hệ thống tải cao. Chỉ dùng trong vài giây để lấy sample, rồi tắt ngay (Ctrl+C).

c) INFO commandstats

Cung cấp thống kê theo loại command, không theo key cụ thể:

redis-cli INFO commandstats | grep -E "^cmdstat_(get|set|hget)"
# cmdstat_get:calls=14827392,usec=89432145,usec_per_call=6.03,rejected_calls=0,failed_calls=0
# cmdstat_set:calls=823451,usec=7891234,usec_per_call=9.58,rejected_calls=0,failed_calls=0

Hữu ích để phát hiện command pattern bất thường (ví dụ GET chiếm 98% calls) nhưng không cho biết key cụ thể nào là hot. Dùng làm bước đầu xác nhận vấn đề trước khi dùng --hotkeys.

d) Application-level sampling (best for production)

Thay vì dựa vào Redis tools, bạn ghi lại key nào được truy cập ngay trong application. Phần tiếp theo trình bày chi tiết pattern này.

So sánh nhanh bốn phương pháp:

Phương pháp          | Impact Redis | Granularity | Phù hợp
---------------------|--------------|-------------|------------------
--hotkeys            | Trung bình   | Per key     | Debug chủ động
MONITOR              | Cao          | Per command | Debug khẩn, ngắn
INFO commandstats    | Thấp         | Per command | Xu hướng lệnh
App-level sampling   | Không        | Per key     | Production liên tục
5

Pattern Sampling Ở Application Layer

Thay vì để Redis tự đếm, application ghi lại cache key được truy cập với xác suất nhỏ (sample_rate). Ở cuối mỗi chu kỳ báo cáo, flush và log top key.

import random
import redis
from collections import Counter
from threading import Lock

r = redis.Redis(host="127.0.0.1", port=6379, decode_responses=True)

_key_counter: Counter = Counter()
_counter_lock = Lock()
_SAMPLE_RATE = 0.01  # chỉ sample 1% request


def cached_get(key: str) -> str | None:
    # Sample trước khi gọi Redis — không block đường hot path
    if random.random() < _SAMPLE_RATE:
        with _counter_lock:
            _key_counter[key] += 1
    return r.get(key)


def report_hot_keys(top_n: int = 20) -> list[tuple[str, int]]:
    """Gọi định kỳ (ví dụ mỗi 60s) để flush và in top key."""
    with _counter_lock:
        top = _key_counter.most_common(top_n)
        _key_counter.clear()

    for key, sample_count in top:
        estimated_total = int(sample_count / _SAMPLE_RATE)
        print(f"HOT: {key!r}  sampled={sample_count}  estimated_calls={estimated_total}")

    return top

Một vài điểm cần lưu ý về cách triển khai này:

  • Sample rate 1%: với 100k request/phút, counter tích lũy ~1000 mẫu — đủ để phân biệt hot key khỏi cold key. Với traffic thấp hơn (dưới 10k req/phút), tăng lên 5-10%.
  • Thread safety: Counter không thread-safe khi ghi đồng thời; dùng lock hoặc chuyển sang per-thread counter rồi merge định kỳ để giảm contention.
  • Không đếm cache miss: đoạn code trên đếm mọi lần gọi cached_get, kể cả miss. Nếu chỉ muốn đếm hit, chuyển điều kiện sampling sang sau khi kiểm tra kết quả.
  • Tích hợp Prometheus: thay vì print, push counter vào Prometheus Counter metric với label key và tạo alert khi top key vượt ngưỡng.

Phương pháp này có ưu điểm lớn: không tạo overhead nào cho Redis, hoạt động ngay cả khi LFU policy chưa được bật, và cho phép đo đạc liên tục thay vì debug thủ công.

6

Ba Chiến Lược Xử Lý Cơ Bản

Chiến lược 1: L1 cache local (in-process)

Hot key đọc nhiều nhưng thay đổi ít → cache thêm một lần nữa ngay trong bộ nhớ của application process. Khi application nhận request, nó kiểm tra L1 cache trước; chỉ khi L1 miss mới đến Redis.

import time
from functools import lru_cache

# Ví dụ đơn giản với TTL thủ công
_l1_cache: dict[str, tuple[str, float]] = {}
_L1_TTL = 5.0  # 5 giây — ngắn để tránh stale lâu


def get_with_l1(key: str) -> str | None:
    now = time.monotonic()

    # Kiểm tra L1
    if key in _l1_cache:
        value, expires_at = _l1_cache[key]
        if now < expires_at:
            return value          # L1 hit
        del _l1_cache[key]        # hết hạn

    # L1 miss → Redis
    value = r.get(key)
    if value is not None:
        _l1_cache[key] = (value, now + _L1_TTL)
    return value

Hiệu quả: với hot key được đọc 50k lần/giây và TTL L1 là 5 giây, chỉ 1 request trong 5 giây mới đến Redis — giảm 99.998% traffic cho key đó. Tổng Redis QPS có thể giảm 80-95% nếu hot key chiếm phần lớn traffic.

Trade-off: invalidation phức tạp. Khi Redis key bị xóa hoặc cập nhật, L1 cache ở mỗi instance application không tự biết. Giải pháp: TTL L1 ngắn (5-30 giây) để giới hạn stale window. Nếu cần invalidation chính xác, kết hợp với keyspace notification hoặc Pub/Sub để broadcast invalidate signal tới mọi instance — xem chi tiết ở bài 17.

Chiến lược 2: Replicate hot key thành N bản sao

Thay vì 1 key user:123, tạo N bản sao user:123:r0, user:123:r1, ..., user:123:r9. Read chọn ngẫu nhiên 1 trong N; write cập nhật tất cả N.

import random
import redis

r = redis.Redis(host="127.0.0.1", port=6379, decode_responses=True)

HOT_KEY_REPLICAS = 10  # N bản sao


def hot_key_replicas(base_key: str) -> list[str]:
    return [f"{base_key}:r{i}" for i in range(HOT_KEY_REPLICAS)]


def hot_get(base_key: str) -> str | None:
    # Random pick 1 trong N → phân tán đọc
    replica_key = f"{base_key}:r{random.randint(0, HOT_KEY_REPLICAS - 1)}"
    return r.get(replica_key)


def hot_set(base_key: str, value: str, ex: int = 300) -> None:
    # Dùng pipeline để ghi tất cả N bản sao trong 1 round-trip
    pipe = r.pipeline(transaction=False)
    for replica_key in hot_key_replicas(base_key):
        pipe.set(replica_key, value, ex=ex)
    pipe.execute()


def hot_delete(base_key: str) -> None:
    pipe = r.pipeline(transaction=False)
    for replica_key in hot_key_replicas(base_key):
        pipe.delete(replica_key)
    pipe.execute()

Hiệu quả trong Cluster: N bản sao có hash slot khác nhau (vì key name khác nhau) → phân tán sang N node khác nhau. Mỗi node chỉ nhận 1/N traffic của hot key đó.

Trade-off:

  • Write cost tăng N lần: mỗi lần update phải ghi N key. Với N = 10, đây là chi phí chấp nhận được nếu read/write ratio của hot key là 1000:1.
  • Nguy cơ temporary inconsistency: trong khoảng thời gian pipeline đang chạy, một số replica có bản mới, một số vẫn bản cũ. Với TTL ngắn thì window này nhỏ.
  • Không phù hợp với key thay đổi thường xuyên (ví dụ counter thay đổi mỗi request) — dùng cho read-heavy, infrequent write.

Chọn N hợp lý: N = 5-20 thường đủ. N quá lớn (100+) làm write cost quá cao và lãng phí bộ nhớ cho các replica ít được dùng.

Chiến lược 3: Read replica

Cấu hình Redis với một hoặc nhiều replica (slave). Master xử lý write; read traffic được phân tán sang các replica.

import random
import redis

# Pool gồm master và replicas
master = redis.Redis(host="redis-master", port=6379, decode_responses=True)
replicas = [
    redis.Redis(host="redis-replica-1", port=6379, decode_responses=True),
    redis.Redis(host="redis-replica-2", port=6379, decode_responses=True),
]


def read_with_replica(key: str) -> str | None:
    # Read từ replica ngẫu nhiên
    replica = random.choice(replicas)
    return replica.get(key)


def write_to_master(key: str, value: str, ex: int = 300) -> None:
    master.set(key, value, ex=ex)

Trade-off: Redis replication là asynchronous — replica có thể lag vài mili-giây đến vài giây so với master tùy tải. Read từ replica là eventually consistent. Không dùng replica cho các key yêu cầu đọc luôn tươi (ví dụ token authentication, rate limit counter).

Module 9 sẽ đi sâu hơn vào replication configuration, Sentinel, client routing đến replica, và kết hợp với Redis Cluster.

7

Hash Tag Trong Redis Cluster

Trong Redis Cluster, hash slot của một key được tính từ toàn bộ key name bằng CRC16. Hash tag cho phép bạn chỉ định phần nào của key tham gia vào phép tính hash:

# Không có hash tag: toàn bộ key name được hash
user:123:profile    → CRC16("user:123:profile") % 16384 = slot X
user:123:settings   → CRC16("user:123:settings") % 16384 = slot Y  ≠ X

# Với hash tag {}: chỉ phần trong {} được hash
user:{123}:profile  → CRC16("123") % 16384 = slot Z
user:{123}:settings → CRC16("123") % 16384 = slot Z  (cùng node!)
user:{123}:cache    → CRC16("123") % 16384 = slot Z  (cùng node!)

Hash tag hữu ích khi bạn cần chạy Lua script hoặc transaction trên nhiều key liên quan của cùng 1 entity — Cluster yêu cầu các key trong cùng 1 transaction phải ở cùng slot.

Với hot key, hash tag có thể dùng theo hai hướng:

  • Gom key của hot user vào cùng node: giúp đồng thời đọc nhiều key của user đó (pipeline trong 1 node, không bị CROSSSLOT). Nhưng nếu user đó thực sự hot, việc này làm tệ hơn — nhiều key hot đổ vào cùng 1 node.
  • Manual sharding ngoài Cluster: bạn tự quyết định shard nào nhận hot key bằng cách chọn hash tag sao cho key đi đến node có headroom. Ví dụ: thay vì user:{123}, dùng user:{123_shard2} để route sang slot/node khác.

Trong hầu hết trường hợp, chiến lược replicate key (phần 6) hiệu quả hơn hash tag manipulation cho hot key — vì nó chủ động phân tán load chứ không chỉ kiểm soát vị trí.

8

Anti-patterns

  • Dùng KEYS pattern* để detect hot key: KEYS block Redis event loop trong suốt thời gian scan — trên DB lớn, đây là vài giây không xử lý được request nào. Luôn dùng SCAN thay thế (hoặc redis-cli --hotkeys đã dùng SCAN bên dưới).
  • Chạy SCAN hoặc --hotkeys liên tục mỗi vài giây: dù không block như KEYS, SCAN vẫn tiêu thụ CPU Redis. Chạy định kỳ mỗi 5 giây trên DB 50 triệu key là đủ để làm chậm mọi thứ. Chỉ chạy khi debug.
  • Replicate hot key thành 1000 bản sao: write cost tăng 1000 lần. Với key có write rate trung bình (100 write/phút), bạn biến nó thành 100k write/phút trên Redis. Giữ N ở mức hợp lý (5-20).
  • L1 cache không có TTL: nếu key Redis bị xóa hoặc cập nhật nhưng L1 cache không có TTL, application sẽ phục vụ dữ liệu cũ mãi mãi cho tới khi process restart. Luôn đặt TTL cho L1 cache, dù ngắn.
  • Read replica cho dữ liệu cần strong consistency: rate limit counter, idempotency check, token validation — những key này phải đọc từ master. Đọc từ replica có thể trả kết quả cũ và gây ra sai logic nghiêm trọng.
  • Không monitor sau khi áp dụng giải pháp: sau khi thêm L1 cache hoặc key replication, cần verify qua metrics rằng Redis QPS thực sự giảm và CPU distribution đều hơn. Giải pháp có thể hoạt động kém hơn dự kiến.
9

Khi Nào Cần Lo Về Hot Key

Hot key là vấn đề ở một ngưỡng nhất định; hầu hết hệ thống không cần lo về nó ở giai đoạn đầu.

Nên quan tâm khi

  • Tổng QPS Redis trên 10k/s top 1% key chiếm hơn 30% traffic.
  • Latency spike tương quan rõ ràng với truy cập key cụ thể (kiểm tra bằng LATENCY HISTORY hoặc application trace).
  • Trong Cluster: 1 node CPU cao bất thường so với các node còn lại (chênh lệch trên 2 lần).
  • Alert CPU trên 1 Redis instance trong khi instance khác cùng loại không bị.

Không cần lo khi

  • Tổng QPS thấp (dưới 1k/s): Redis dư sức xử lý mọi key mà không cần tối ưu thêm.
  • Traffic distribution đều (top key không chiếm quá 5-10% tổng request).
  • Single-node Redis với CPU headroom còn nhiều (dưới 40%).
  • Hot key chỉ xuất hiện trong các burst ngắn (vài giây), hệ thống tự phục hồi.

Phát hiện hot key và lo lắng về nó quá sớm thường là premature optimization. Tập trung monitor và thiết lập alert; hành động khi alert thực sự bật.

10

Tổng Kết & Quiz

Tổng kết

  • Hot key: 1 key nhận phần lớn QPS Redis → 1 CPU thread bị nghẽn (single-threaded model) → 1 node Cluster bị nóng trong khi các node khác idle.
  • Triệu chứng: 1 instance CPU cao bất thường, P99 latency spike trên 1 node, application log có 1-2 key lặp lại nhiều.
  • Detect: redis-cli --hotkeys (cần LFU policy, chạy khi cần), MONITOR (cực ngắn, impact cao), INFO commandstats (pattern lệnh), application sampling (tốt nhất cho production liên tục).
  • Ba chiến lược cơ bản: L1 cache local (giảm 80-95% Redis traffic, cần quản lý TTL), key replication (phân tán sang N node, write cost × N), read replica (phân tán đọc, eventually consistent).
  • Hash tag {} trong Cluster kiểm soát hash slot của key; hữu ích cho cross-key transaction nhưng không phải giải pháp chính cho hot key.
  • Module 9 sẽ đi sâu: client-side caching RESP3, dedicated proxy, sharding strategy nâng cao.

Quiz 5 câu

  1. Tại sao Redis single-threaded lại làm hot key trở thành vấn đề nghiêm trọng hơn so với database multi-threaded? Giải thích về giới hạn tài nguyên.
  2. Điều kiện bắt buộc để redis-cli --hotkeys cho kết quả hữu ích là gì? Lệnh nào để kiểm tra và đổi policy?
  3. Bạn có hot key config:global được đọc 200k lần/phút. Nếu replicate thành 10 bản sao, mỗi bản sao nhận khoảng bao nhiêu request? Write rate hiện tại là 5 lần/phút — sau khi replicate, write rate lên bao nhiêu?
  4. Tại sao không nên dùng read replica để đọc rate limit counter? Nêu cụ thể loại lỗi logic có thể xảy ra.
  5. Vẽ (bằng text) luồng xử lý của hàm get_with_l1(key): các bước kiểm tra từ L1 → Redis → trả kết quả, kể cả trường hợp L1 hết hạn.

Đáp án gợi ý

  1. Redis xử lý command trên 1 luồng CPU. Khi hot key chiếm 50% QPS, nó chiếm ~50% thời gian của luồng đó; các key khác phải xếp hàng chờ → tăng latency toàn bộ instance. Database multi-threaded có thể phân tán xử lý sang nhiều CPU core song song.
  2. Điều kiện: maxmemory-policy phải là allkeys-lfu hoặc volatile-lfu. Kiểm tra: redis-cli CONFIG GET maxmemory-policy. Đổi: redis-cli CONFIG SET maxmemory-policy allkeys-lfu.
  3. Mỗi bản sao nhận 200k/10 = 20k lần/phút (nếu random distribution đều). Write rate từ 5 tăng lên 5 × 10 = 50 lần/phút trên Redis (dùng pipeline, ghi 10 key trong 1 round-trip).
  4. Replica có replication lag — nó có thể trả rate limit counter cũ hơn master vài mili-giây. Nếu 2 request đến 2 instance application đồng thời, cả hai đọc replica và đều thấy counter = 99 (dưới limit 100), cả hai thực hiện request và increment — kết quả master thành 101, vượt limit mà không bị chặn.
  5. get_with_l1(key): (1) Lấy thời gian hiện tại. (2) Key trong _l1_cache? → Có: kiểm tra expires_at → còn hạn: return value (L1 hit) / hết hạn: xóa khỏi L1, xuống bước 3. Không: xuống bước 3. (3) Gọi r.get(key) (Redis). (4) Kết quả không null: lưu vào _l1_cache với expires_at = now + _L1_TTL. (5) Return value.

Bài tiếp theo

Bài 19 tổng hợp checklist caching và phân tích các anti-pattern phổ biến nhất, bao gồm incident thực tế khi KEYS * được gọi trong production.

Tham khảo