bigkey
1、bigkey帶來的問題
- 如果是集群模式下,無法做到負載均衡,導致請求傾斜到某個實例上,而這個實例的QPS會比較大,內存占用也較多;對於Redis單線程模型又容易出現CPU瓶頸,當內存出現瓶頸時,只能進行縱向庫容,使用更牛逼的服務器。
- 涉及到大key的操作,尤其是使用hgetall、lrange 0 -1、get、hmget 等操作時,網卡可能會成為瓶頸,也會到導致堵塞其它操作,qps 就有可能出現突降或者突升的情況,趨勢上看起來十分不平滑,嚴重時會導致應用程序連不上,實例或者集群在某些時間段內不可用的狀態。
- 假如這個key需要進行刪除操作,如果直接進行DEL 操作,被操作的實例會被Block住,導致無法響應應用的請求,而這個Block的時間會隨着key的變大而變長。
2、bigkey是如何產生的
一般來說,bigkey是由於程序員的程序設計不當,或對數據規模預料不清楚造成的:
1、社交類:粉絲列表,如果某些明顯或大V,一定是bigkey
2、統計類:如果按天存儲某項功能或網站的用戶集合,除非沒幾個人用,否則必定是bigkey
3、緩存類:作為數據庫數據的冗余存儲,這種是redis的最常用場景,但有2點要注意:
1)是不是有必要把所有數據都緩存
2)有沒有相關關聯的數據
舉個例子,該同學把某明星一個專輯下的所有視頻信息都緩存成了一個巨大的json,這個json達到了6MB。
3、查找bigKey的方法
- 在redis實例上執行bgsave,然后我們對dump出來的rdb文件進行分析,找到其中的大KEY
- 有個不太推薦的命令,debug object xxx 可以看到這個key在內存中序列化后的大小,當然我們可以通過SCAN+debug object xxx 得到當前實例所有key的大小。
- redis-cli 原生自帶 –bigkeys 功能,可以找到某個實例 5種數據類型(String、hash、list、set、zset)的最大key。
4、直接刪除bigkey的風險
DEL命令在刪除單個集合類型的Key時,命令的時間復雜度是O(M),其中M是集合類型Key包含的元素個數。
DEL keyTime complexity: O(N) where N is the number of keys that will be removed. When a key to remove holds a value other than a string, the individual complexity for this key is O(M) where M is the number of elements in the list, set, sorted set or hash. Removing a single key that holds a string value is O(1).
生產環境中遇到過多次因業務刪除大Key,導致Redis阻塞,出現故障切換和應用程序雪崩的故障。測試刪除集合類型大Key耗時,一般每秒可清理100w~數百w個元素; 如果數千w個元素的大Key時,會導致Redis阻塞上10秒可能導致集群判斷Redis已經故障,出現故障切換;或應用程序出現雪崩的情況。
說明:Redis是單線程處理。單個耗時過大命令,導致阻塞其他命令,容易引起應用程序雪崩或Redis集群發生故障切換。所以避免在生產環境中使用耗時過大命令。
Redis刪除大的集合鍵的耗時, 測試估算,可參考;和硬件環境、Redis版本和負載等因素有關
Key類型 | Item數量 | 耗時 |
---|---|---|
Hash | ~100萬 | ~1000ms |
List | ~100萬 | ~1000ms |
Set | ~100萬 | ~1000ms |
Sorted Set | ~100萬 | ~1000ms |
5、如何優雅地刪除各類大Key
從Redis2.8版本開始支持SCAN命令,通過m次時間復雜度為O(1)的方式,遍歷包含n個元素的大key.這樣避免單個O(n)的大命令,導致Redis阻塞。 這里刪除大key操作的思想也是如此。
先給鍵改名。
5.1 Delete Large Hash Key
通過hscan命令,每次獲取500個字段,再用hdel命令,每次刪除1個字段。Python代碼:
def del_large_hash():
r = redis.StrictRedis(host='redis-host1', port=6379)
large_hash_key ="xxx"
cursor = '0'
while cursor != 0:
cursor, data = r.hscan(large_hash_key, cursor=cursor, count=500)
for item in data.items():
r.hdel(large_hash_key, item[0])
5.2 Delete Large Set Key
刪除大set鍵,使用sscan命令,每次掃描集合中500個元素,再用srem命令每次刪除一個鍵Python代碼:
def del_large_set():
r = redis.StrictRedis(host='redis-host1', port=6379)
large_set_key = 'xxx'
cursor = '0'
while cursor != 0:
cursor, data = r.sscan(large_set_key, cursor=cursor, count=500)
for item in data:
r.srem(large_size_key, item)
5.3 Delete Large List Key
刪除大的List鍵,未使用scan命令; 通過ltrim命令每次刪除少量元素。Python代碼:
def del_large_list():
r = redis.StrictRedis(host='redis-host1', port=6379)
large_list_key = 'xxx'
while r.llen(large_list_key)>0:
r.ltrim(large_list_key, 0, -101)
5.4 Delete Large Sorted set key
刪除大的有序集合鍵,和List類似,使用sortedset自帶的zremrangebyrank命令,每次刪除top 100個元素。Python代碼:
def del_large_sortedset():
r = redis.StrictRedis(host='large_sortedset_key', port=6379)
large_sortedset_key='xxx'
while r.zcard(large_sortedset_key)>0:
r.zremrangebyrank(large_sortedset_key,0,99)
5.5 后台刪除之lazyfree機制
為了解決redis使用del命令刪除大體積的key,或者使用flushdb、flushall刪除數據庫時,造成redis阻塞的情況,在redis 4.0引入了lazyfree機制,可將刪除操作放在后台,讓后台子線程(bio)執行,避免主線程阻塞。
lazy free的使用分為2類:第一類是與DEL命令對應的主動刪除,第二類是過期key刪除、maxmemory key驅逐淘汰刪除。
主動刪除
UNLINK命令是與DEL一樣刪除key功能的lazy free實現。唯一不同時,UNLINK在刪除集合類鍵時,如果集合鍵的元素個數大於64個(詳細后文),會把真正的內存釋放操作,給單獨的bio來操作。
127.0.0.1:7000> UNLINK mylist
(integer) 1
FLUSHALL/FLUSHDB ASYNC
127.0.0.1:7000> flushall async //異步清理實例數據
注意:DEL命令,還是阻塞的刪除操作。
FLUSHALL/FLUSHDB ASYNC
通過對FLUSHALL/FLUSHDB添加ASYNC異步清理選項,redis在清理整個實例或DB時,操作都是異步的。
127.0.0.1:7000> DBSIZE
(integer) 1812295
127.0.0.1:7000> flushall //同步清理實例數據,180萬個key耗時1020毫秒
OK
(1.02s)
127.0.0.1:7000> DBSIZE
(integer) 1812637
127.0.0.1:7000> flushall async //異步清理實例數據,180萬個key耗時約9毫秒
OK
127.0.0.1:7000> SLOWLOG get
1) 1) (integer) 2996109
2) (integer) 1505465989
3) (integer) 9274 //指令運行耗時9.2毫秒
4) 1) "flushall"
2) "async"
5) "127.0.0.1:20110"
6) ""
被動刪除
lazy free應用於被動刪除中,目前有4種場景,每種場景對應一個配置參數; 默認都是關閉。
lazyfree-lazy-eviction no
lazyfree-lazy-expire no
lazyfree-lazy-server-del no
slave-lazy-flush no
lazyfree-lazy-eviction
針對redis內存使用達到maxmeory,並設置有淘汰策略時;在被動淘汰鍵時,是否采用lazy free機制;
因為此場景開啟lazy free, 可能使用淘汰鍵的內存釋放不及時,導致redis內存超用,超過maxmemory的限制。此場景使用時,請結合業務測試。
lazyfree-lazy-expire
針對設置有TTL的鍵,達到過期后,被redis清理刪除時是否采用lazy free機制;
此場景建議開啟,因TTL本身是自適應調整的速度。
lazyfree-lazy-server-del
針對有些指令在處理已存在的鍵時,會帶有一個隱式的DEL鍵的操作。如rename命令,當目標鍵已存在,redis會先刪除目標鍵,如果這些目標鍵是一個big key,那就會引入阻塞刪除的性能問題。 此參數設置就是解決這類問題,建議可開啟。
slave-lazy-flush
針對slave進行全量數據同步,slave在加載master的RDB文件前,會運行flushall來清理自己的數據場景,
參數設置決定是否采用異常flush機制。如果內存變動不大,建議可開啟。可減少全量同步耗時,從而減少主庫因輸出緩沖區爆漲引起的內存使用增長。
expire及evict優化
redis在空閑時會進入activeExpireCycle循環刪除過期key,每次循環都會率先計算一個執行時間,在循環中並不會遍歷整個數據庫,而是隨機挑選一部分key查看是否到期,所以有時時間不會被耗盡(采取異步刪除時更會加快清理過期key),剩余的時間就可以交給freeMemoryIfNeeded來執行。
6、鍵值設計
key名設計
可讀性和可管理性(建議)
以業務名(或數據庫名)為前綴(防止key沖突),用冒號分隔,比如業務名:表名:id
set trade:order:1//業務名:表名:id
簡潔性(建議)
保證語義的情況下,減低key長度,key過長也占用內存空間
user:{uid}:friends:messages:{mid} 簡化為 u:{uid}:fr:m:{mid}
不要包含特殊字符(強制)
反例:包含空格、換行、單雙引號以及其他轉義字符
value設計
拒絕bigkey
在Redis中,一個字符串最大512MB,一個二級數據結構(例如hash、list、set、zset)可以存儲大約40億個(2^32-1)個元素,但實際中如果下面兩種情況,我就會認為它是bigkey。
1.字符串類型:
它的big體現在單個value值很大,一般認為超過10KB就是bigkey。
2.非字符串類型:(hash,list,set,zset等)
哈希、列表、集合、有序集合,它們的big體現在元素個數太多。
一般來說hash、list、set、zset元素個數不要超過5000。
反例:一個包含200萬個元素的list。
3.bigkey的刪除
非字符串的bigkey,不要使用del刪除,使用hscan、sscan、zscan方式漸進式刪除,同時要注意防止bigkey過期時間自動刪除問題(例如一個200萬的zset設置1小時過期,會觸發del操作,如果沒有使用Redis 4.0的過期異步刪除(lazyfree-lazy-expire yes),就會存在阻塞Redis的可能性)
7、優化bigkey
優化bigkey
1.一個字拆,大拆小
hash結構 比如一個big hash中有100萬的數據可以通過key的名稱做定義將100萬的數據進行拆分成200個key,每個key中存放5000個數據
list結構也是同樣操作,一個list的key存放5000個集合,拆開來存
2.避開危險操作
如果必須使用bigkey的話,那操作的時候避開hgetall、lrange、smembers、zrange、sinter等全數據查詢的命令,有遍歷的需求可以使用hscan、sscan、zscan代替(例如有時候僅僅需要hmget,而不是hgetall),刪除也是一樣,盡量使用優雅的方式來處理。
3.合理使用數據類型(推薦)
例如:實體類型(要合理控制和使用數據結構,但也要注意節省內存和性能之間的平衡)
正例:
hmset user:1 name tom age 19 favor football
反例:
set user:1:name tom
set user:1:age 19
set user:1:favor football
4.控制key的生命周期,redis不是垃圾桶(推薦)
建議使用expire設置過期時間(條件允許可以打散過期時間,防止集中過期)。
命令使用
1.O(N)命令關注N的數量
例如hgetall、lrange、smembers、zrange、sinter等並非不能使用,但是需要明確N的值。有遍歷的需求可以使用hscan、sscan、zscan代替。
2.禁用命令
禁止線上使用keys、flushall、flushdb等,通過redis的rename機制禁掉命令,或者使用scan的方式漸進式處理。
3.合理使用select
redis自帶的多數據庫較弱,使用數字進行區分,很多客戶端支持較差,同時多業務用多數據庫實際還是單線程處理,會有干擾。
4.使用批量操作提高效率
原生命令:例如mget、mset。
非原生命令:可以使用pipeline提高效率。
但要注意控制一次批量操作的元素個數(例如500以內,實際也和元素字節數有關)。
注意兩者不同:
原生命令是原子操作,pipeline是非原子操作。
pipeline可以打包不同的命令,原生命令做不到
pipeline需要客戶端和服務端同時支持。