iT邦幫忙

2026 iThome 鐵人賽

DAY 28
0

昨天的大 Key、熱 Key,都是出事了才去找。其實 Redis 自己就記著很多數字:用了多少記憶體、快取有沒有命中、連了幾條連線,下 INFO 就看得到。

INFO 一次會印出兩百多行,分成 # Server、# Memory、# Stats 等 11 段,後面加段落名稱就只看那一段,前面用過的 INFO stats、INFO persistence 就是這樣。今天只挑平常要盯的幾個。

命中率

INFO stats 裡的 keyspace_hits 是讀 key 有讀到的次數,keyspace_misses 是沒讀到的次數。有讀到的佔幾成,就是命中率:

命中率 = keyspace_hits ÷ (keyspace_hits + keyspace_misses)

重新啟動 Java,先把累計的數字歸零,再跑一次 Day 22 的擊穿(在主機的終端機下):

docker exec redis30days redis-cli CONFIG RESETSTAT

docker exec redis30days redis-cli DEL product:1

./k6/run.sh breakdown

docker exec redis30days redis-cli INFO stats | grep keyspace

https://ithelp.ithome.com.tw/upload/images/20261010/20184209HXWtvtAlb4.png

快取剛好過期那一下的 50 次沒讀到,其他 4950 次都有讀到,命中率是 4950 ÷ 5000 = 99%。

接著跑 Day 21 的穿透(在主機的終端機下):

./k6/run.sh penetration

docker exec redis30days redis-cli INFO stats | grep keyspace

https://ithelp.ithome.com.tw/upload/images/20261010/20184209ZiVaWWqhsn.png

穿透的 5000 次全部沒讀到,命中率一下掉到 4950 ÷ 10000 = 49.5%。平常很穩的命中率突然掉下來,就是有人一直在查快取裡沒有的東西,或是一大批 key 同時過期。

這兩個數字是整台 Redis 一起算的,不只快取,像扣庫存的 Lua 裡那個 GET 也算在裡面。

連線池

跑完上面共 1 萬次,看一下現在連了幾條(在主機的終端機下):

docker exec redis30days redis-cli INFO clients | grep connected_clients

https://ithelp.ithome.com.tw/upload/images/20261010/20184209K3kmoZjLqB.png

只有 2 條:一條是 Java,一條是正在下指令的 redis-cli。50 個人同時打,Java 跟 Redis 之間還是只有一條連線,因為 Spring 用的 Lettuce 預設大家共用同一條,Redis 回得夠快,一條就夠用。

例外是 MULTI 跟 Pipeline,它們的指令要連在一起送,不能跟別人的混在一起,所以要自己一條連線。沒有連線池的話,每次都是新開一條、用完就關掉。

用 Day 19 的 /transfer 轉帳 100 次,看累計連進來幾條(total_connections_received,在主機的終端機下):

docker exec redis30days redis-cli MSET balance:A 100 balance:B 50

docker exec redis30days redis-cli CONFIG RESETSTAT

for i in $(seq 1 100); do curl -s -o /dev/null -X POST "localhost:8080/transfer?amount=1"; done

docker exec redis30days redis-cli INFO stats | grep total_connections_received

curl -s -o /dev/null 是不印出回傳的內容。

https://ithelp.ithome.com.tw/upload/images/20261010/20184209ChbJSoFp9g.png

101 條:轉帳 100 次就新開了 100 條,另外 1 條是 redis-cli 自己。

pom.xml 加上連線池:

<!-- Day 28:連線池。Spring Boot 看到它就會自動開,版本也是 Spring Boot 管 -->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>

重新啟動 Java,同樣四行再跑一次(在主機的終端機下):

https://ithelp.ithome.com.tw/upload/images/20261010/20184209uDAw3ui4gO.png

剩 3 條:連線池裡的 1 條、Java 平常共用的那 1 條(Java 剛重開,這條也是重新連的),加上 redis-cli 自己。轉帳用完的連線會還回池子,下一次直接拿出來用。

連線池預設最多 8 條(spring.data.redis.lettuce.pool.max-active),都被拿走的時候,後面的人要排隊等。沒什麼在用 MULTI、Pipeline 的話,不加連線池也沒關係。

記憶體碎片

INFO memory 先看三個數字(在主機的終端機下):

docker exec redis30days redis-cli INFO memory | grep -E "used_memory_human|used_memory_rss_human|mem_fragmentation_ratio"

https://ithelp.ithome.com.tw/upload/images/20261010/20184209dqamEIUj0U.png

  • used_memory_human:資料用了多少,Redis 自己算的
  • used_memory_rss_human:作業系統實際給了 Redis 多少
  • mem_fragmentation_ratio:大約是 rss 除以 used_memory

ratio 12.46 看起來很嚇人,其實是資料太少:rss 裡有一部分是 Redis 程式本身要用的,資料只有 1.8MB 的時候,比例就會很大。資料多起來,這個數字才準。

做 400 萬個 key 放在 db2,再隔一個刪一個,刪掉一半(在主機的終端機下):

# db2 塞 400 萬個 key:k:0 到 k:3999999
docker exec -i redis30days sh -c 'awk "BEGIN{for(i=0;i<4000000;i++) printf \"SET k:%d %d\r\n\", i, i}" | redis-cli -n 2 --pipe'

docker exec redis30days redis-cli INFO memory | grep -E "used_memory_human|used_memory_rss_human|mem_fragmentation_ratio"

# 隔一個刪一個:k:0、k:2、k:4⋯⋯
docker exec -i redis30days sh -c 'awk "BEGIN{for(i=0;i<4000000;i+=2) printf \"DEL k:%d\r\n\", i}" | redis-cli -n 2 --pipe'

docker exec redis30days redis-cli INFO memory | grep -E "used_memory_human|used_memory_rss_human|mem_fragmentation_ratio"

https://ithelp.ithome.com.tw/upload/images/20261010/20184209NNq4DabPir.png

https://ithelp.ithome.com.tw/upload/images/20261010/20184209qHCmCu4qVH.png

塞完是 1.07,資料多的時候兩邊差不多。刪掉一半之後,used_memory 從 247MB 降到 141MB,rss 沒有跟著降,還是 264MB 多,ratio 變成 1.88。

Redis 跟作業系統拿記憶體,是一整塊一整塊拿的。隔一個刪一個,每一塊裡都還有沒刪的 key,一塊都還不回去,空出來的位子散在每一塊裡,這就是記憶體碎片。這些空位之後寫進來的資料還用得到,只是在作業系統看來,Redis 還是佔著 264MB 多。

MEMORY DOCTOR

這些數字看不懂,也可以直接問 Redis(在主機的終端機下):

docker exec redis30days redis-cli MEMORY DOCTOR

https://ithelp.ithome.com.tw/upload/images/20261010/20184209WxtqFlitX7.png

它列了三點:曾經用過的比現在多很多(Peak memory)、作業系統給的比實際用的多太多(High total RSS)、碎片太多(High allocator fragmentation),最後一點直接建議打開 activedefrag。

資料不到 5MB 的時候問它,只會回一段 Hi Sam, this instance is empty...,不會分析。

整理碎片

照它說的打開(在主機的終端機下):

docker exec redis30days redis-cli CONFIG SET activedefrag yes

等一分鐘左右再看一次(在主機的終端機下):

docker exec redis30days redis-cli INFO memory | grep -E "used_memory_human|used_memory_rss_human|mem_fragmentation_ratio"

https://ithelp.ithome.com.tw/upload/images/20261010/20184209r9pOL5gBLI.png

rss 從 264MB 多降到 167MB,ratio 降到 1.19。activedefrag 會在背景把還在用的 key 搬到一起,整塊空出來的才還得回去。

搬資料會吃 CPU,預設是關的。打開之後,也要碎片超過 100MB(active-defrag-ignore-bytes)才會開始搬。這也是為什麼要塞 400 萬個:200 萬個刪一半,碎片還不到 100MB。

測完還原,db2 也清掉(在主機的終端機下):

docker exec redis30days redis-cli CONFIG SET activedefrag no

docker exec redis30days redis-cli -n 2 FLUSHDB ASYNC

FLUSHDB 是清掉整個 db,ASYNC 跟昨天的 UNLINK 一樣丟給背景釋放。

為什麼要在意

  • INFO 的數字是一路累加的。 keyspace_hits、total_connections_received 都是從啟動(或 CONFIG RESETSTAT)開始加,跑越久,突然掉下來的那一段越看不出來。像上面穿透的 5000 次一次都沒讀到,累加起來看還有 49.5%。正式環境通常會用 Prometheus 這類工具定時去抓 INFO,畫成圖、算每一段時間的命中率,掉下來就發通知。
  • maxmemory 管不到碎片。 之前設的 maxmemory,比的是 used_memory,不是 rss。像上面刪完一半,used_memory 是 141MB,rss 還是 264MB 多。就算 maxmemory 設 200MB,Redis 也覺得還沒滿,但作業系統那邊已經給了它 264MB。所以機器的記憶體要比 maxmemory 多留一些。

相關範例程式碼可以參考 https://github.com/gary880306/redis-30days/tree/dev

明天

明天是最後一個技術題:如果用分散式鎖,Redis 自己出事的時候還靠得住嗎/images/emoticon/emoticon12.gif


上一篇
Day 27|大 Key 與熱 Key
系列文
從購物車到秒殺:30 天 Redis 高併發自學筆記 共 28 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言