Redis cache hit cao nhưng hệ thống vẫn chậm vì Hot Key

Tổng hợp docs thấy có vấn đề hay ho về Redis nên lên bài chia sẻ trải nghiệm thực tế để mong rằng sẽ hữu ích nếu anh em gặp, đây là một kiểu sự cố Redis có thể rất dễ bị bỏ qua: cache hit rate cao, database nhẹ, Redis không báo lỗi rõ ràng, nhưng API vẫn chậm (có thể thấy được).

P95, P99 tăng. Một số request timeout. Dashboard thì lại cho cảm giác mọi thứ vẫn ổn vì cache hit có khi 95 đến 99%.

Vấn đề là cache hit cao chỉ nói rằng request lấy được dữ liệu từ Redis, chứ không nói dữ liệu đó đang dồn vào key nào, key đó lớn cỡ nào, hay một Redis shard có đang phải gánh phần lớn traffic hay không.

Đó chính thế, đây là lúc anh em có thể cần nghĩ đến Hot Key trong trường hợp mình gặp: một key bị truy cập quá nhiều đến mức chính nó trở thành một điểm nghẽn.

a9b95480-4303-468e-83ba-eab459837f01

Hot Key là gì?

Nói đơn giản thì Hot Key là một key bị truy cập nhiều bất thường so với các key khác.

Ví dụ Redis có rất nhiều key, nhưng phần lớn request lại liên tục đọc một vài key kiểu như:

homepage:config
feature_flags:global
product:detail:flash_sale:98231
tenant:permissions:big_customer
public_key:payment

Nếu key đó nhỏ, vấn đề có thể chưa nhận biết rõ.

Nhưng nếu key đó vừa bị đọc nhiều, vừa có payload lớn, hoặc là một data structure lớn như hash, set, sorted set, thì Redis có thể bị chậm dù cache hit rate vẫn rất cao.

Đây là điểm dễ nhầm. Chúng ta nhìn cache hit rate và nghĩ Hit cao nghĩa là cache ổn. Cache ổn nghĩa là Redis không phải vấn đề.

Vì sao cache hit cao vẫn chưa đủ?

Đây là chỗ chúng ta rất dễ nhìn nhầm nếu chỉ dựa vào cache hit rate.

Hit cao nghĩa là Redis đang trả được dữ liệu, nhưng chưa nói được lần trả đó nhẹ hay nặng. Một key nhỏ được đọc nhiều lần thường không đáng ngại, nhưng một JSON vài trăm KB bị đọc liên tục thì lại khác.

Với Redis, mỗi lần cache hit vẫn có chi phí riêng: Redis phải trả dữ liệu qua network, client phải nhận response, rồi application còn phải parse dữ liệu đó. Nếu phần lớn request cùng đọc một key lớn, database có thể nhẹ hơn thật, nhưng phần tải đó sẽ chuyển sang Redis và application layer.

Vì vậy khi thấy hit rate đẹp mà hệ thống vẫn chậm, mình thường không dừng ở câu “cache vẫn hit mà” mà mình sẽ bật ra là:

  • Request đang hit nhiều vào key nào?
  • Key đó lớn cỡ nào?
  • Command đọc key đó có nặng không?
  • Traffic có đang lệch về một Redis node nào không?

Trả lời được mấy câu này thì anh em sẽ biết Redis thật sự ổn, hay chỉ là cache hit rate đang che mất phần chi phí phía sau.

Ví dụ: flash sale và một key sản phẩm

Lấy một ví dụ anh em dễ gặp đi. Đó là một hệ thống thương mại điện tử có cache cho trang chi tiết sản phẩm. Bình thường mỗi sản phẩm có một key:

product:detail:{product_id}

Mọi thứ chạy ổn. Cache hit rate khoảng 96%. Database nhẹ. Redis cũng ổn.

Đến giờ flash sale, gần như toàn bộ traffic đổ vào một sản phẩm duy nhất.

Key này bắt đầu bị đọc liên tục:

product:detail:flash_sale:98231

Vấn đề là value của key đó không nhỏ. Nó là một JSON khoảng vài trăm KB, chứa thông tin sản phẩm, promotion, campaign, inventory display, badge, metadata, recommendation block.

Một key 700KB nghe chưa có gì ghê. Nhưng nếu key đó bị đọc 5.000 lần mỗi giây thì anh em thử nhân lên:

700KB * 5.000 request/s = khoảng 3.5GB/s dữ liệu trả ra

Lúc này Redis không chậm vì lookup key. Redis chậm vì phải trả quá nhiều dữ liệu từ cùng một điểm nóng.

App cũng dính vào vì app còn phải nhận response, parse JSON, deserialize object, rồi trả tiếp cho client.

Vậy là nhìn từ dashboard: Cache hit cao, DB nhẹ, Redis memory ổn.

Nhưng nhìn từ trải nghiệm user thì API detail sản phẩm chậm rõ rệt, P99 tăng mạnh, Một số request timeout

Đây là kiểu lỗi rất dễ bị debug nhầm.

Hot Key không chỉ là key lớn

Hot Key có vài dạng như sau:

1. Key nhỏ nhưng QPS quá lớn

Ví dụ:

feature_flags:global
payment:public_key
app:global_config

Mỗi lần đọc rất nhanh, value cũng nhỏ. Nhưng request nào cũng đọc thì tổng số lần truy cập vẫn rất lớn.

Nếu app không có local cache, mỗi request đều đi xuống Redis, key đó sẽ thành điểm tập trung traffic.

Kiểu này hay gặp ở config, feature flag, public key, metadata dùng chung toàn hệ thống.

2. Key lớn và bị đọc nhiều

Ví dụ:

homepage:cache
product:detail:big
tenant:permissions:big_customer

Vấn đề ở đây không chỉ là số lần đọc, mà còn là kích thước dữ liệu trả về.

Redis có thể xử lý command nhanh, nhưng response lớn sẽ kéo theo network output lớn, client buffer lớn, và chi phí deserialize phía app.

Nhiều anh em chỉ nhìn Redis CPU nên bỏ qua phần này.

3. Data structure lớn nhưng bị đọc kiểu lấy hết.

Ví dụ:

HGETALL tenant:permissions:big_customer
SMEMBERS campaign:active_users
LRANGE notification:list 0 -1
ZRANGE leaderboard 0 -1

Đây là kiểu command anh em nhìn tưởng bình thường, nhưng với Redis thì rất nặng. Một lệnh HGETALL trên hash quá lớn có thể block Redis trong một khoảng thời gian đủ để P99 nhảy vọt.

Redis nhanh, nhưng không có nghĩa mọi command đều nhanh. Một command càng lấy nhiều dữ liệu thì càng tốn thời gian.

Dấu hiệu nhận biết Hot Key

Khi gặp tình huống cache hit cao nhưng hệ thống vẫn chậm, mình thường kiểm tra theo thứ tự này.

Đầu tiên là network output của Redis.

redis-cli INFO stats

Anh em chú ý mấy dòng kiểu:

keyspace_hits
keyspace_misses
instantaneous_ops_per_sec
total_net_output_bytes

Nếu hit rate cao nhưng total_net_output_bytes tăng rất nhanh, khả năng Redis đang trả payload lớn hoặc bị đọc một số key quá nóng.

Tiếp theo là command stats.

redis-cli INFO commandstats

Nếu thấy GET rất nhiều thì chưa đủ kết luận. Nhưng nếu thấy mấy command như HGETALL, SMEMBERS, LRANGE, ZRANGEusec_per_call cao bất thường thì phải nghi ngay.

Ví dụ:

cmdstat_hgetall:calls=12000,usec=480000000,usec_per_call=40000.00
cmdstat_get:calls=90000000,usec=45000000,usec_per_call=0.50

Ở đây GET gọi rất nhiều nhưng mỗi lần rẻ. Còn HGETALL gọi ít hơn nhưng mỗi lần quá đắt.

Tiếp theo là Slow Log.

redis-cli SLOWLOG GET 20

Nếu thấy kiểu này thì phải soi ngay:

HGETALL tenant:permissions:big_customer
SMEMBERS campaign:active_users
LRANGE something 0 -1

Một command Redis mất vài ms đã đáng chú ý rồi. Mất vài chục ms thì chắc chắn không thể bỏ qua.

Nếu dùng Redis Cluster, anh em phải so từng node, đừng nhìn trung bình toàn cluster.

redis-cli -h redis-1 INFO stats | grep instantaneous_ops_per_sec
redis-cli -h redis-2 INFO stats | grep instantaneous_ops_per_sec
redis-cli -h redis-3 INFO stats | grep instantaneous_ops_per_sec

Nếu một node cao bất thường so với các node còn lại, có thể một hot key hoặc một hot slot đang nằm trên node đó

Cách tìm Hot Key

Cách tốt nhất trong production không phải lúc nào cũng là chạy lệnh trên Redis, mà là đo từ phía application.

Vì app hiểu key nào thuộc nghiệp vụ gì.

Ví dụ thay vì log raw key, ta normalize key thành nhóm:

product:detail:{id}
tenant:permissions:{tenant_id}
feature_flags:{env}
homepage:{region}:{device}

Sau đó đo:

redis_key_group_qps
redis_key_group_latency
redis_key_group_payload_size

Chỉ cần có top 10 key group theo QPS là anh em đã thấy rất nhiều thứ.

Trong lúc incident, nếu cần nhìn nhanh, có thể dùng:

redis-cli --bigkeys
redis-cli SLOWLOG GET 20
redis-cli INFO commandstats

Còn MONITOR thì mình rất hạn chế dùng trên production. Nó hữu ích, nhưng nặng. Nếu dùng thì chỉ nên chạy rất ngắn để lấy mẫu.

redis-cli MONITOR

Không nên bật rồi để đó như một công cụ quan sát lâu dài.

Xử lý nhanh khi đang có sự cố

Nếu đã xác định Redis đang bị Hot Key, việc đầu tiên không nhất thiết là scale app. Thậm chí scale app có thể làm Redis bị gọi nhiều hơn. Cách xử lý nhanh trong tình huống này:

  • Thêm local cache ngắn hạn trong app cho hot key
  • Giảm payload của key đang bị đọc nhiều
  • Tách một key lớn thành nhiều key nhỏ hơn
  • Tránh đọc toàn bộ data structure nếu chỉ cần một phần
  • Dùng request coalescing để nhiều request cùng chờ một lần đọc Redis
  • Tạo nhiều bản read-only của hot key nếu cần giảm tải tạm thời

Ví dụ một key global config bị đọc quá nhiều:

global_config

Có thể tạm nhân ra:

global_config:0
global_config:1
global_config:2
global_config:3

App đọc random một trong các key này. Cách này không phải hay lắm, nhưng trong incident nó có thể giảm tải ngay cho một key duy nhất.

Với dữ liệu ít thay đổi như feature flag, config, public key, metadata nhỏ, local cache vài giây trong app thường hiệu quả hơn nhiều so với để request nào cũng đi Redis.

Fix dài hạn

Về lâu dài, anh em nên bỏ tư duy chỉ nhìn cache hit rate. Dashboard Redis cần có thêm:

Top key group theo QPS
Top key group theo latency
Top key group theo payload size
Commandstats
Slowlog
Network output per shard
Ops/sec per shard
Redis client latency từ app

Ngoài ra, cần tránh mấy pattern rất dễ tạo Hot Key:

  • Một key global cho toàn hệ thống
  • Một hash chứa quá nhiều field
  • Một JSON cache quá lớn
  • HGETALL trên object lớn
  • SMEMBERS trên set lớn
  • LRANGE 0 -1 trên list dài
  • Cache response nguyên cục trong khi endpoint chỉ cần một phần nhỏ

Một lỗi khá phổ biến là dùng Redis như ném object đã serialize vào cho nhanh. Lúc traffic còn nhỏ thì không sao. Nhưng khi một object lớn trở thành hot key, toàn bộ chi phí bị đẩy sang Redis và application layer.

Kết

Cái này trước mình cũng nhầm (do trình độ và trải nghiệm cả hoặc là từ lúc mình nghiên cứu nhiều hơn về benchmark mới thấy ra nhiều thứ) Cache hit cao không có nghĩa hệ thống nhanh. Nó chỉ có nghĩa là request lấy được dữ liệu từ cache.

Còn hệ thống có nhanh hay không phụ thuộc vào key nào đang bị hit, payload lớn cỡ nào, command có nặng không, và Redis shard nào đang phải gánh phần traffic đó.

Nói gọn lại: Redis không chỉ chậm vì cache miss. Redis cũng có thể chậm vì cache hit dồn quá nhiều vào một Hot Key.

Chia sẻ cá nhân mong các bác góp ý, nếu có gì sai sót các bác chia sẻ giúp mình để học hỏi thêm : D

Thông tin nổi bật

Event Thumbnail

Sự kiện phát trực tiếp​

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

Tiêu điểm chuyên gia