1. 程式人生 > >Redis hash tag進行分槽導致的問題

Redis hash tag進行分槽導致的問題

我們已經對redis cluster中的key進行了一定的分槽,但是導致了redis節點資料的不均勻分佈,三個節點資料量大小對比:5:1:1,但更加恐怖的是記憶體使用對比,在最多的一個程序中佔用超過900M,而最少的一個程序僅60M。 對比redis的dump檔案,是其他兩個的20倍
-rw-r--r--. 1 root root  14448246 8月  19 18:45 dump.6388.rdb
-rw-r--r--. 1 root root 279497287 8月  19 18:38 dump.6389.rdb
-rw-r--r--. 1 root root  14199864 8月  19 18:35 dump.6390.rdb
 

 Redis啟動記憶體配置

通過redis的config get *命令,可以檢視redis配置中所有欄位,其中可以看到。
 13) "maxmemory"
 14) "0"
 15) "maxmemory-samples"
 16) "5"
113) "maxmemory-policy"
114) "noeviction"
  可以在redis.conf配置檔案中,設定maxmemory用於表示redis的讀/寫最大記憶體,如果該值為0則表示沒有限制; maxmemory-policy用於設定回收策略,當記憶體達到maxmemory限制時,如果不進行回收,redis程序可能會掛掉,maxmemory-policy有6種方式用於回收:
  • volatile-lru:(預設值)從已設定過期時間的資料集(server.db[i].expires)中挑選最近最少使用的資料淘汰
  • volatile-random:從已設定過期時間的資料集(server.db[i].expires)中任意選擇資料淘汰
  • volatile-ttl : 從已設定過期時間的資料集(server.db[i].expires)中挑選將要過期的資料淘汰
  • allkeys-lru : 從資料集(server.db[i].dict)中挑選最近最少使用的資料淘汰
  • allkeys-random:從資料集(server.db[i].dict)中任意選擇資料淘汰
  • noeviction : 禁止驅逐資料,永不過期,返回錯誤
redisObject結果中包括一個lru屬性,其中記錄了物件最後一次被命令程式訪問時的時間,lru屬性用於配合實現valatile-ttl和allkeys-lru回收策略使用。 在redis中lru演算法是一個近似演算法,預設情況下,redis會隨機挑選maxmemory-samples個鍵,從中選取一個最近最久未使用的key進行淘汰。 在配置檔案中可以通過maxmemory-samples的值來設定redis需要檢查key的個數,但是檢查的越多,耗費的時間也就越久,但是結構越精確(也就是Redis從記憶體中淘汰的物件未使用的時間也就越久~)。

Redis執行時記憶體佔用

info命令中返回memory部分:
# Memory
used_memory:895628864
used_memory_human:854.14M
used_memory_rss:939532288
used_memory_peak:912382408
used_memory_peak_human:870.12M
used_memory_lua:36864
mem_fragmentation_ratio:1.05
mem_allocator:jemalloc-3.6.0
  used_memory表示由redis分配器分配的記憶體,byte為單位;used_memory_human是used_memory的人類可讀方式;used_memory_rss是指從作業系統的角度,返回redis已分配的記憶體總量(常駐集大小),與top,ps等作業系統命令返回一致。 如果redis中過期了一定的keys,used_memory會降低,rss不會降低(redis釋放的記憶體,短期內不會返回給作業系統),會產生一定的記憶體碎片。 mem_fragmentation_ratio, 1.05表示記憶體碎片率(used_memory_rss/used_memory),稍大於1是比較合理的,說明redis沒有發生記憶體交換,但如果記憶體碎片率超過1.5,就說明redis消耗了實際需要實體記憶體的150%,其中50%是記憶體碎片率;若是記憶體碎片率低於1的話,說明redis記憶體分配超出了實體記憶體,作業系統正在進行記憶體交換,記憶體交換會引起比較明顯的響應延遲。 除非你能夠保證你的機器總是有一半的空閒記憶體,否則別使用快照方式持久化資料或者通過執行BGREWRITEAOF壓縮aof檔案。 redis在執行bgsave時,會進行一次fork,fork後的程序負責將記憶體中的資料寫入磁碟,由於fork採用Copy-On-Write,兩個redis程序共享記憶體中的資料。redis如果有資料更新,則會將對應的共享記憶體頁建立一份副本再更新,當更新操作足夠頻繁時,共享的記憶體空間會迅速地副本化,導致實體記憶體被耗光,系統被迫動用交換空間,從而導致redis服務極不穩定,整個系統堵塞在磁碟io上。 而切分slot的結果是導致了其中資料量比較大的節點佔用記憶體是其他兩個節點的10倍。

Redis中的key記憶體估算

除了通過redis info命令巨集觀地檢視其中的所有keys佔用記憶體,以及系統分配內容,還可以藉助外部工具來檢視 通過安裝該工具來對redis的記憶體進行分析:

1.分析單個key:

redis-memory-for-key -s 192.168.1.137 -p 6389 {prod}_brand
Key                    "{prod}_brand"
Bytes                  443636.0
Type                   hash
Encoding                   hashtable
Number of Elements             550
Length of Largest Element          1479
  此時,會輸出該key的一些基本資訊,type,encoding等,其中我們比較關心的是bytes欄位。 經過分析發現,對於redis中的值,該分析過程輸出的bytes(容量)會比該值(本身的字串bytes[].length)本身要大,這其中可能要考慮到redis的管理成本,通過infoQ那篇文章也能看到端倪。

2.分析整個dump檔案,產生memory報告

很可惜,我們需要的memory report並沒有生成出來,報錯了...
rdb -c memory dump.6389.rdb > memory.csv
Traceback (most recent call last):
  File "/usr/local/bin/rdb", line 9, in <module>
    load_entry_point('rdbtools==0.1.7', 'console_scripts', 'rdb')()
  File "/Library/Python/2.7/site-packages/rdbtools-0.1.7-py2.7.egg/rdbtools/cli/rdb.py", line 72, in main
    parser.parse(dump_file)
  File "/Library/Python/2.7/site-packages/rdbtools-0.1.7-py2.7.egg/rdbtools/parser.py", line 293, in parse
    self.parse_fd(open(filename, "rb"))
  File "/Library/Python/2.7/site-packages/rdbtools-0.1.7-py2.7.egg/rdbtools/parser.py", line 337, in parse_fd
    self._callback.end_database(db_number)
  File "/Library/Python/2.7/site-packages/rdbtools-0.1.7-py2.7.egg/rdbtools/memprofiler.py", line 150, in end_database
    self._stream.next_record(record)
  File "/Library/Python/2.7/site-packages/rdbtools-0.1.7-py2.7.egg/rdbtools/memprofiler.py", line 90, in next_record
    self._out.write("%d,%s,%s,%d,%s,%d,%d\n" % (record.database, record.type, encode_key(record.key),
  File "/Library/Python/2.7/site-packages/rdbtools-0.1.7-py2.7.egg/rdbtools/callbacks.py", line 91, in encode_key
    return _encode(s, quote_numbers=True)
  File "/Library/Python/2.7/site-packages/rdbtools-0.1.7-py2.7.egg/rdbtools/callbacks.py", line 88, in _encode
    return _encode_basestring_ascii(s)
  File "/Library/Python/2.7/site-packages/rdbtools-0.1.7-py2.7.egg/rdbtools/callbacks.py", line 69, in _encode_basestring_ascii
    return '"' + str(ESCAPE_ASCII.sub(replace, s)) + '"'
TypeError: expected string or buffer
  如果dump檔案比較大,會造成分析的過程比較長。

3.將dump檔案轉換成json

如果單獨看dump檔案,使用head命令時,檔案屬於二進位制格式,並不能以可以看的方式顯示:
REDIS0006�$DFDF3DB1-5371-4644-BC12-B784DE6C6F9AppStorepriceStock_spec_106488�28{"price":3299,"marketP�
                                                                                                      850,"stock":         fixS�
                                                                                                                             0}$86A586E8-30A3-4{"brandId":97,�F1366AppStore{prod}_base_info_25560�C�E�
               Name":"TOD'S","[email protected]#10hannel":4
                                                    ustomsR "":[email protected]
editorIntro D一雙美麗的高跟鞋就像 位擁有魔法 #仙女,在腳尖輕  ,點@��能讓穿上它 5  5人變得更性感迷 !藉助那些精@� h�� 8 z力 Y來 V自己成為鎂光燈下 )焦 �吧。!gender �
   但可以將其轉換為json,下面就是將符合正則條件格式的key轉換為json:
rdb --command json --key "{prod}_*" dump.6389.rdb  > temp
   當然還有一些其他引數可以使用,比如db, type(鍵值型別)。
[{
"{prod}_base_info_25560":"{\"brandId\":97,\"brandName\":\"TOD'S\",\"categoryId\":10,\"channel\":4,\"customsRate\":0.10,\"editorIntro\":\"\u4e00\u53cc\u7f8e\u4e3d\u7684\u9ad8\u8ddf\u978b\u5c31\u50cf\u4e00\u4f4d\u62e5\u6709\u9b54\u6cd5\u7684\u4ed9\u5973\uff0c\u5728\u811a\u5c16\u8f7b\u8f7b\u4e00\u70b9\uff0c\u5c31\u80fd\u8ba9\u7a7f\u4e0a\u5b83\u7684\u7684\u5973\u4eba\u53d8\u5f97\u66f4\u6027\u611f\u8ff7\u4eba\uff01\u501f\u52a9\u90a3\u4e9b\u7cbe\u7f8e\u4ed9\u5c65\u7684\u9b54\u529b\uff0c\u6765\u8ba9\u81ea\u5df1\u6210\u4e3a\u9541\u5149\u706f\u4e0b\u7684\u7126\u70b9\u5427\u3002\",\"gender\":0,\"goodsId\":19917,\"imageSource\":\"http://pic2.zhenimg.com/upload2/45/0c/450c490c4bdb85a72eac185ab05b591e.jpg\",\"isSpecial\":0,\"isStop\":0,\"mSmall\":\"http://pic2.zhenimg.com/upload2/45/0c/m_small_450c490c4bdb85a72eac185ab05b591e.jpg\",\"marketPrice\":5600,\"price\":1680,\"productAd\":\"\",\"productCode\":\"HY XXW0LK08780G23 740A\",\"productDetail\":\"\",\"productId\":25560,\"productImage\":\"[\\\"http://pic2.zhenimg.com/upload2/45/0c/450c490c4bdb85a72eac185ab05b591e.jpg\\\",\\\"http://pic2.zhenimg.com/upload2/fa/b9/fab98f7eb6261d90884857d771f1119b.jpg\\\",\\\"http://pic2.zhenimg.com/upload2/8a/9c/8a9c5b5dea7907817eb4721a46fbec42.jpg\\\",\\\"http://pic2.zhenimg.com/upload2/14/b4/14b4c0b6ce75541fa30b364e3788f52a.jpg\\\",\\\"http://pic2.zhenimg.com/upload2/ef/b3/efb3b3b9a14b8215bdc52b33c08c5377.jpg\\\",\\\"http://pic2.zhenimg.com/upload2/43/09/4309cafb27158298add464e75d5c547e.jpg\\\",\\\"http://pic2.zhenimg.com/upload2/5e/d1/5ed141b0efb6b4e6372d6323cb715ff7.jpg\\\",\\\"http://pic2.zhenimg.com/upload2/2e/44/2e44baef1e86b86134c02ef077d40578.jpg\\\"]\",\"productName\":\"TOD'S/\u6258\u5fb7\u65af \u9ed1\u8272/\u7d2b\u7ea2\u8272 \u5c0f\u725b\u76ae \u5973\u58eb\u9ad8\u8ddf\u978b HY XXW0LK08780G23 740A\",\"providerId\":18,\"providerStorageId\":1,\"skuIds\":[\"38455\",\"38456\",\"38457\"],\"status\":0,\"urlId\":79052250}",
  如果將其轉換成json,原有的dump檔案大概270M,json檔案增長為700M左右(且我們只轉換了其中的一部分)。

如何修改預設的分槽策略

在制定HASH Tag方案後,不可避免地使得有些slot key會集中到一臺/幾臺伺服器上,此時或許我們可以將一些佔用空間較大的slot比較集中的server釋放一下壓力,將其slot轉移至新增的伺服器上。 在一個redis叢集中,可以通過cluster info檢視當前叢集狀態:  
> cluster info
cluster_state:ok
cluster_slots_assigned:16384
cluster_slots_ok:16384
cluster_slots_pfail:0
cluster_slots_fail:0
cluster_known_nodes:6
cluster_size:3
cluster_current_epoch:35
cluster_my_epoch:35
cluster_stats_messages_sent:7324
cluster_stats_messages_received:7324
  通過cluster nodes命令檢視當前叢集機器分佈情況:
d08dc883ee4fcb90c4bb47992ee03e6474398324 192.168.1.137:6390 master - 0 1471661565059 33 connected 5641-11040
5974ed7dd81c112d9a2354a0a985995913b4702c 192.168.1.137:6389 slave ffb4db4e1ced0f91ea66cd2335f7e4eadc29fd56 0 1471661563556 35 connected
532e58842d001f8097fadc325bdb5541b788a360 192.168.1.138:6389 master - 0 1471661564058 29 connected 11041-16383
ffb4db4e1ced0f91ea66cd2335f7e4eadc29fd56 192.168.1.138:6390 myself,master - 0 0 35 connected 0-5640
c69b521a30336caf8bce078047cf9bb5f37363ee 192.168.1.137:6388 slave 532e58842d001f8097fadc325bdb5541b788a360 0 1471661564058 29 connected
aa52c7810e499d042e94e0aa4bc28c57a1da74e3 192.168.1.138:6388 slave d08dc883ee4fcb90c4bb47992ee03e6474398324 0 1471661565562 33 connected
 

技術解決

從技術上考慮,解決該問題的方法有幾種:

減少redis的單個元素值大小

改用其他型別的解決方案,各種序列化方案的對比表現:https://github.com/eishay/jvm-serializers/wiki,體現在 序列化/反序列化的速度,以及序列化後佔用空間大小。我們當前使用fastjson,可以考慮使用更加高效的protostuff來代替。

垂直擴充套件,增加單個redis服務可用記憶體

但絕不能隨意加,估算出大概需要的記憶體量並進行合理規劃

水平擴充套件,加機器

重新切分slot,將佔用記憶體較大的slot單獨切分出去,叢集中對slot進行相關操作的命令主要有:  
/叢集(cluster)
CLUSTER INFO 列印叢集的資訊
CLUSTER NODES 列出叢集當前已知的所有節點(node),以及這些節點的相關資訊。
 
//節點(node)
CLUSTER MEET <ip> <port> 將 ip 和 port 所指定的節點新增到叢集當中,讓它成為叢集的一份子。
CLUSTER FORGET <node_id> 從叢集中移除 node_id 指定的節點。
CLUSTER REPLICATE <node_id> 將當前節點設定為 node_id 指定的節點的從節點。
CLUSTER SAVECONFIG 將節點的配置檔案儲存到硬盤裡面。
 
//槽(slot)
CLUSTER ADDSLOTS <slot> [slot ...] 將一個或多個槽(slot)指派(assign)給當前節點。
CLUSTER DELSLOTS <slot> [slot ...] 移除一個或多個槽對當前節點的指派。
CLUSTER FLUSHSLOTS 移除指派給當前節點的所有槽,讓當前節點變成一個沒有指派任何槽的節點。
CLUSTER SETSLOT <slot> NODE <node_id> 將槽 slot 指派給 node_id 指定的節點,如果槽已經指派給另一個節點,那麼先讓另一個節點刪除該槽>,然後再進行指派。
CLUSTER SETSLOT <slot> MIGRATING <node_id> 將本節點的槽 slot 遷移到 node_id 指定的節點中。
CLUSTER SETSLOT <slot> IMPORTING <node_id> 從 node_id 指定的節點中匯入槽 slot 到本節點。
CLUSTER SETSLOT <slot> STABLE 取消對槽 slot 的匯入(import)或者遷移(migrate)。
 
//鍵 (key)
CLUSTER KEYSLOT <key> 計算鍵 key 應該被放置在哪個槽上。
CLUSTER COUNTKEYSINSLOT <slot> 返回槽 slot 目前包含的鍵值對數量。
CLUSTER GETKEYSINSLOT <slot> <count> 返回 count 個 slot 槽中的鍵。
  通過cluster keyslot和countkeysinslot檢視集中的slot佔用的總數量:
> cluster keyslot {prod}
(integer) 811
> cluster countkeysinslot 811
(integer) 361528
  將指定的keyslot遷移到對應的節點上去:
cluster setslot 4781 migrating d08dc883ee4fcb90c4bb47992ee03e6474398324
   此時節點的狀態已經改變:
cluster nodes
d08dc883ee4fcb90c4bb47992ee03e6474398324 192.168.1.137:6390 master - 0 1471663968706 33 connected 5641-11040
5974ed7dd81c112d9a2354a0a985995913b4702c 192.168.1.137:6389 slave ffb4db4e1ced0f91ea66cd2335f7e4eadc29fd56 0 1471663969206 35 connected
532e58842d001f8097fadc325bdb5541b788a360 192.168.1.138:6389 master - 0 1471663968706 29 connected 11041-16383
ffb4db4e1ced0f91ea66cd2335f7e4eadc29fd56 192.168.1.138:6390 myself,master - 0 0 35 connected 0-5640 [4781->-d08dc883ee4fcb90c4bb47992ee03e6474398324]
c69b521a30336caf8bce078047cf9bb5f37363ee 192.168.1.137:6388 slave 532e58842d001f8097fadc325bdb5541b788a360 0 1471663970709 29 connected
aa52c7810e499d042e94e0aa4bc28c57a1da74e3 192.168.1.138:6388 slave d08dc883ee4fcb90c4bb47992ee03e6474398324 0 1471663970209 33 connected
  此種方法是否有效也亟待在生產環境中試驗。
由分槽記憶體佔用過大導致的另一個問題 我們在由於記憶體佔用過高導致的程序down掉的redis節點上,執行批量刪除操作時,提示下面的錯誤:
> redis-cli -c -p 6390 keys "\{prod\}*" | xargs redis-cli -c -p 6390 del
 
(error) MISCONF Redis is configured to save RDB snapshots, but is currently not able to persist on disk. Commands that may modify the data set are disabled. Please check Redis logs for details about the error.
(error) MISCONF Redis is configured to save RDB snapshots, but is currently not able to persist on disk. Commands that may modify the data set are disabled. Please check Redis logs for details about the error.
(error) MISCONF Redis is configured to save RDB snapshots, but is currently not able to persist on disk. Commands that may modify the data set are disabled. Please check Redis logs for details about the error.
(error) MISCONF Redis is configured to save RDB snapshots, but is currently not able to persist on disk. Commands that may modify the data set are disabled. Please check Redis logs for details about the error.
(error) MISCONF Redis is configured to save RDB snapshots, but is currently not able to persist on disk. Commands that may modify the data set are disabled. Please check Redis logs for details about the error.
(error) MISCONF Redis is configured to save RDB snapshots, but is currently not able to persist on disk. Commands that may modify the data set are disabled. Please check Redis logs for details about the error.
(error) MISCONF Redis is configured to save RDB snapshots, but is currently not able to persist on disk. Commands that may modify the data set are disabled. Please check Redis logs for details about the error.
(error) MISCONF Redis is configured to save RDB snapshots, but is currently not able to persist on disk. Commands that may modify the data set are disabled. Please check Redis logs for details about the error.
(error) MISCONF Redis is configured to save RDB snapshots, but is currently not able to persist on disk. Commands that may modify the data set are disabled. Please check Redis logs for details about the error.
經過一番查詢,找出問題所在,是由於redis意外掛掉導致快照停止導致的,檢視redis_last_bgsave_status為err而不是ok,通過狀態可以證實這一點,需要關閉屬性 stop-writes-on-bgsave-error ,注意要關閉master節點上的該屬性而不是 slave節點上的,並在master節點上執行刪除操作。
config set stop-writes-on-bgsave-error no
  記得在執行完成後,將其重置為yes,如果conf檔案中沒有設定,則下次重啟後仍然會設定為預設值yes。