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启动内存配置

 
可以参考知乎上的回答:https://www.zhihu.com/question/31102463
 
通过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运行时内存占用

 
参考调试文档:http://zhongfox.github.io/blog/nosql/2016/01/26/redis-performance
 
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内部的内存优化,可以参考:http://www.infoq.com/cn/articles/tq-redis-memory-usage-optimization-storage
 
 

Redis中的key内存估算

 
除了通过redis info命令宏观地查看其中的所有keys占用内存,以及系统分配内容,还可以借助外部工具来查看
 
https://github.com/sripathikrishnan/redis-rdb-tools
 
通过安装该工具来对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","category@#10hannel":4
                                                    ustomsR "":0.@
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
 
 
此种方法是否有效也亟待在生产环境中试验。