Redis 實戰心得
網路上很多這類心得,像這篇提供許多有用的建議。本篇是我最近實戰後的隨手記。
資料分群
Redis 沒有 table,資料存在同一個地方。這篇分析三種依服務類型存資料的方法:
- namespace: 在 key 的前面加上如
user:
的 prefix,所有資料放在一個 Redis 內。 - 使用 database: 用指令 select 切換用不同的 “database”。
- 用不同的 Redis servers。
Redis Cluster 只能用 database 0,所以為了長遠 scalability 考量,database 並不適用。Redis 作者甚至認為 database 是他作過 Redis 設計裡最糟的決定。
用不同 Redis servers 管理成本較高。或許分成兩群 Redis Cluster 會不錯?例如一群設成 LRU mode 存 cache 資料;另一群存 persistent data。這樣可以各別設適合的 shards、replicas 和 OOM 時的處理方式。
關於 namespace 的作法,建議用 :
區分,因為不少 keys 分析的工具,假設用 :
為 keys 分群。如果已有大量資料沒用 :
分群,可以用 redis-audit,它會自動分群,並可加入部份規則,減少自動分群花的時間。
Keys 的格式
可以的話,不要用 binary 會比較方便。像 redis-cli --scan
無法處理 non-printable 字元。要掃出全部 keys 得另寫小程式。雖然很多事都可寫小程式解決,但是分析和除錯時,就沒那麼方便。例如前面提到的 redis-audit,無法處理 binary keys,在 parsing error 的地方得加上 key.scrub(‘’)
才能用。
使用 Expiration
資料多了以後,很難管理。能加上 expiration 就加,避免無止盡的堆垃圾到記憶體裡。
Redis 只支援第一層 keys 加 expiration,若需要用 hash 但 hash 部份內容需要加 expiration,最好將一個大 hash 攤成多個小 hash 或是 string,這樣才能善用 expiration。附帶一提,多個小 hash 比一個大 hash 更省空間,因為 Redis 有針對小 hash 最佳化儲存方式。
分析占空間的 keys
除前面提的分析工具外 ( 我是用 redis-audit ),用 redis-cli 也可以找出顯著的問題:
- 用
redis-cli scan 0 count 100
取樣 100 筆,找出常見的 keys,看看數量分佈是否合理。0 可以換成其它數字,redis-cli 仍能正常運作,藉此當作簡易取樣。 - 用
redis-cli --bigkeys
,這指令會取樣找出各資料類型最大的 keys。 - 用 DEBUG OBJECT KEY 看 KEY serialized 後的大小。
- 用 OBJECT IDLETIME KEY 看 KEY 多久沒有被存取。注意,預設設定下,它不會傳回正確的值。
Latency
官方文件 latency 有落落長的教學,另寫一篇摘要重要的部份。
我自己有用到的是:
- 使用 benchmark 了解自己機器合理的 op/s。
- 用 slowlog 找 ≥ 10ms 的操作。Redis 只有main thread 處理資料,有 100 個 10ms 操作,一秒就只能處理 100 個指令而已了。 10ms 是很久的。
- 使用 unlink 取代 del 刪除 key。Redis 會用 background thread 處理 unlink 後的 keys,不會像 del 卡到 main thread。試想砍一個 ≥ 1M 筆資料的 hash / zset,可能會花 10s 以上。這表示 Redis 服務會停擺 10s。
- 留意 Redis client lib 是否有自己的 thread pool。如果是在 lib 的 thread pool 處理 Redis 返回值,要將花 CPU 時間的操作轉移到別的 thread 執行,避免卡到 Redis client lib 的 thread pool。
- 開啟 latency monitor,使用 latency doctor 檢查有何可改善之處。
Lua Script
- Lua 是唯一能妥善作好 atomic 操作的方法,並且可減少 roundtrip time。熟悉使用 Lua 長遠來說是好事。
- 但 Lua 沒用好會卡住 Redis 很久,也不能隨意中止 Lua script 執行。使用時要非常小心,不要寫複雜的操作。
- 使用 hash tag ( “{…}”) 將 Lua script 用到的 keys 分到同一個 hash slot。
- Lua 的 index 從 1 開始。
“
和‘
不同,雙引號才會將\x12
表示成 16 進位。- Lua 是 dynamic typing,table 是主要的容器 (associative array)。在 table 找不到資料時,會回傳 nil。
- 除錯方法:可以用 redis.log() 寫資訊到 Redis server log 或存到 table 裡,在 script 結束後回傳 table。
- redis-cli 會將 non-printable 字元轉成 16 進位印出來。在 redis-cli 可用eval + pack/unpack 編碼解碼。像這樣:
127.0.0.1:6379> eval 'return struct.unpack(">i4", ARGV[1])' 0 "\x01\x02\x03\x04"
(integer) 16909060
同步資料
Redis 有 cluster mode 也有單節點的 master-replica 模式。有些別人寫好的工具,可用來同步 standalone node 和 redis cluster,例如 redis-shake。在升級單一節點為 redis cluster 時,可以減少停機時間。
Redis Cluster Client Library 的設定
Redis 有多種模式,像是單機、Master-Replica、Sentinel、Cluster,這些模式 Lettuce 都支援。但要留意 cluster mode 預設只有建立第一個連線後會取得 cluster node topology,之後不會處理 topology change。也就是說,failover 後,app (即 redis client) 不會知道要連往新的 master,導致服務繼續處於異常狀態。
Lettuce 提供三種方式 refresh topology:
- App 手動呼叫 RedisClusterClient.reloadPartitions()
- 定期在背景呼叫 reloadPartitions()
- 動態偵測某些條件 (例如收到 MOVED 或是重連數次) 後呼叫 reloadPartitions()
無腦地在背景呼叫最單純,但效能不佳,因為所有 redis client 會連往所有它們知道的節點。也可以設定使用 initial seeds 而非 discovered nodes,減少負擔。
綜合一些考量,設定 connection/command timeout,並在發生 connection/command timeout 後自己手動呼叫 reloadPartitions(),行為比較好控制。或是遇到意外狀況時,重建 client 也行。
之後有什麼適合的內容,再持續補完。