由于每个业务对redis的使用情况是不同的,单独为每个数据结构进行限制,无疑会对开发的进度和难度造成巨大影响,也失去了使用redis丰富的数据结构而不是memcached的初衷。
所以只提几条如何用好redis的建议,望各位能够采纳。
redis的slowlog是参考redis集群瓶颈优化的一个点
当业务出现性能瓶颈的时候,可以分析slowlog里占用时间较长较多的command。
在我的经验中HGETALL,DEL对于存储较多value的key,hash,会阻塞较长时间,这方面业务需要极力去规避,因为一个命令可能造成 多个客户端timeout,或者是连接池timeout,在使用连接池的场合中,read timeout或者connect timeout都是灾难性的,会极大的拖垮业务系统的TPS
这个机制主要是为了减少AOF对业务的影响,在执行这个命令的时候,redis服务器会创建一个aof重写缓冲区,在重写的过程中,所有写命令会写入这个缓冲区
当数据集非常大的时候,遍历内存来重写AOF会占用很多时间,在这段时间如果有高并发写,就会占用很多很多内存,所以一定要注意大数据集是否有足够内存来做操作,或者BGREWRITEAOF命令执行时是否有很高的写并发
实例共享:
最近遇到有个业务的TPS无论如何都上不去的情况,根据测试负载压在redis的网络上,由于公司的资源限制不能加机器了,所以只好砍掉取用的数据量
这个业务的歌词文件属于存储较大的value,把这部分砍掉以后业务的TPS很快就上去了 (后期需要歌词的时候对这部分value进行压缩再存储)
分析:
redis内部的value按照三种编码存储int,raw,embstr
当存储的value为一个整数,按照int对象来存储
当存储的value不为整数,且小于32字节,按照embstr来存储
当存储的value不为整数,且大于32字节,按照raw来存储
int存储整数,并且对于0-1000的常用数字做了共享对象,存储或者读写改内容会直接连接到该对象
embstr是为了短字串存储优化而实现的编码,只能进行读,当修改以后会转换为raw编码
raw顾名思义,未经加工的字串,所以当value较大时,读写这样的数据会造成网络瓶颈
结论:
1.如果对字串需要修改,那么直接存大于32字节的,预留空间,这样减少了转换所需时间
2.redis的字符串是二进制安全的,因此可能会误导人什么样的东西都往里塞,
其实长value是不会压缩的,因此短value的读写效率比长value高得多,
对长value的存储,如果是纯文本,例如歌词,完全可以进行压缩,现在的速度快压缩率高的文本压缩算法非常成熟,加大业务服务器的cpu负载减少IO负载,对于网络IO密集型应用来说显然是划得来的
参考了http://oldblog.antirez.com/post/redis-weekly-update-7.html
他提出一种叫做trick RedisHL的方法,可以显著的提高redis的内存利用率。
稍微翻译一下这篇文章:
假如要存储一个key “foo”,做以下三件事情:
1。计算”foo”的十六进制SHA1校验和:0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33
2。使用头四个字节0bee作为存储的(真实)key
3。把”foo”当作0bee的field存储
所以”SET foo bar”相当于”HSET 0bee foo bar”
具体ruby代码如下:
require 'rubygems' require 'redis' require 'digest/sha1'
class RedisHL
def initialize(r) @redis = r end
def get_hash_name(key) Digest::SHA1.hexdigest(key.to_s)[0..3] end
def exists(key) @redis.hexists(get_hash_name(key)) end
def [](key) @redis.hget(get_hash_name(key),key) end
def []=(key,value) @redis.hset(get_hash_name(key),key,value) end
def incrby(key,incr) @redis.hincrby(get_hash_name(key),key,incr) end
end
r = Redis.new() rr = RedisHL.new(r)
rr[:foo] = "bar" puts(rr[:foo])
1000W的短K使用常规方式如果需要存储空间1.7G的话,使用这种方式只需要300M内存!
文章末还附带了一张测试数据的图片
http://antirez.com/blogdata/214/memgraph.png
分析:
我来分析一下如此amazing的结果是怎样的原理
redis对于hash类型有两种实现方式,一种是ziplist,一种是hashtable。
如果直接使用get/set对总hash进行数据操作,那么在数据利用率(used/size)达到一定数量时,会开始rehash,即为二倍扩容
由于hashtable的预留空间机制,所以数据利用率其实不是很高
ziplist是一种非常高效的数据存储格式,他的指针用的很少,且没有预留空间。
在一个hash类型被创建时,他会以ziplist格式存储,达到一定规模后转为hashtable。
规模的限制可以在redis.conf中以下字段修改
hash-max-zipmap-entries 512
hash-max-zipmap-value 512
当大量的数据以ziplist存储时,内存利用率是非常高的
但是值得注意的是,ziplist的数据插入删除等等是较为消耗CPU资源的,所以对于大量的短key以及读大于写的场合来说,这个trick RedisHL的方式是很不错的
结论:
1.对于大量的短key以及读大于写的场合来说,这个trick RedisHL的方式是很不错的
2.对于大量的短key以及读大于写的场合来说,直接用get/set操作,不如将一些key均衡的打包为hash类型进行操作(注意hash类型 里的field不能过多,不然就掉进前文中HGETALL的坑了),当这些hash类型以ziplist的格式存储时,内存利用率是惊人的。
twemproxy是由twitter开发的一个Redis 和 Memcached 代理。
鉴于目前redis cluster是unstable版本,机制的实现还有待商榷。(不是特别看好使用gossip算法的广播机制,对网络环境要求很高,cassandra就是这种机制)
在大规模的redis集群使用中,twemproxy为代表的proxy方式成为了主流,在这之中twemproxy脱颖而出,甚至在redis作者的blog上被点名分析了,据他说proxy的性能损耗在20%左右。
http://www.antirez.com/news/44
twemproxy的机制和源码分析会在下一篇文章中放出。这一篇只是讨论一下twemproxy下redis使用的注意点。
这个是被redis作者在前文中提到的blog中点名批评的,原话是:
So I expected to see almost the same numbers with an MGET as I see when I run the MGET against a single instance, but I get only 50% of the operations per second.
在twemproxy上操作MGET的性能居然只有操作单机的50%!
我大致分析了twemproxy的源码,当MGET涉及的key分布在多个redis实例上,由于网络波动,每台实例需要提取的key数目不一样等原因,proxy不是一次性处理完整合结果反馈客户端。
而是每接受到一次以后,等待整合事件被触发,最后一次性整合然后发送结果给客户端。
消耗时间 = 消耗最多的那台redis实例 + 每次时间轮询间隔*分配的redis实例个数
这样慢也就很正常了,所以
避免在twemproxy环境下使用MGET!!!
复杂类型对象指的是hash,set之类的非string对象
这样的对象都是key field value格式的。
由于twemproxy对对象做hash,只是针对key,而不会也无法对field做hash(如果对field做hash,很多redis原生操作无法支持,而且也会造成上问中由于访问多台redis实例才能返回结果而造成的MGET坑)
所以一整个对象会存储在同一个redis实例上,如果一个对象过大,会严重影响redis的分布性能,造成存储和性能上严重的分布不均衡。
使用OBJECT指令可以查看对象的详细信息,如果核实是超大field,那么:
超大field需要进行切割,控制在每个50m左右来保证redis集群的分布均衡