redis二三事
最近redis集群的研究比较多,把心得稍微梳理一下
redis客户端
redis的c客户端可以用hiredis,完全开源
API主要是以下四个
1 | redisContext* redisConnect(const char *ip, int port); |
当需要在命令中传递一个二进制安全的字符串时,可以使用%b,一个字符串指针,和size_t类型的字符串长度。
1 | reply = redisCommand(context, "SET foo %b", value, valuelen); |
reply是如下的结构
1 | /* This is the reply object returned by redisCommand() */ |
如何用好redis
slowlog
redis的slowlog是参考redis集群瓶颈优化的一个点
当业务出现性能瓶颈的时候,可以分析slowlog里占用时间较长较多的command。
在我的经验中HGETALL,DEL对于存储较多value的key,hash,会阻塞较长时间,这方面业务需要极力去规避,因为一个命令可能造成多个客户端timeout,或者是连接池timeout,在使用连接池的场合中,read timeout或者connect timeout都是灾难性的,会极大的拖垮业务系统的TPS
BGREWRITEAOF
这个机制主要是为了减少AOF对业务的影响,在执行这个命令的时候,redis服务器会创建一个aof重写缓冲区,在重写的过程中,所有写命令会写入这个缓冲区
当数据集非常大的时候,遍历内存来重写AOF会占用很多时间,在这段时间如果有高并发写,就会占用很多很多内存,所以一定要注意大数据集是否有足够内存来做操作,或者BGREWRITEAOF命令执行时是否有很高的写并发
value的优化
实例共享:
最近遇到有个业务的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密集型应用来说显然是划得来的
内存利用率的优化
他提出一种叫做trick RedisHL的方法,可以显著的提高redis的内存利用率。
稍微翻译一下这篇文章:
假如要存储一个key "foo",做以下三件事情:
1。计算"foo"的十六进制SHA1校验和:0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33
2。使用头四个字节0bee作为存储的(真实)key
3。把"foo"当作0bee的field存储
所以"SET foo bar"相当于"HSET 0bee foo bar"
具体ruby代码如下:
1 | require 'rubygems' |
1000W的短K使用常规方式如果需要存储空间1.7G的话,使用这种方式只需要300M内存!
文章末还附带了一张测试数据的图片
分析:
我来分析一下如此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下redis使用的注意点
twemproxy是由twitter开发的一个Redis 和 Memcached 代理。
鉴于目前redis cluster是unstable版本,机制的实现还有待商榷。(不是特别看好使用gossip算法的广播机制,对网络环境要求很高,cassandra就是这种机制)
在大规模的redis集群使用中,twemproxy为代表的proxy方式成为了主流,在这之中twemproxy脱颖而出,甚至在redis作者的blog上被点名分析了,据他说proxy的性能损耗在20%左右。
Antirez weblog - Twemproxy, a Redis proxy from Twitter
twemproxy的机制和源码分析会在下一篇文章中放出。这一篇只是讨论一下twemproxy下redis使用的注意点。
避免使用MGET
这个是被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!!!
避免使用超大field的复杂类型对象
复杂类型对象指的是hash,set之类的非string对象
这样的对象都是key field value格式的。
由于twemproxy对对象做hash,只是针对key,而不会也无法对field做hash(如果对field做hash,很多redis原生操作无法支持,而且也会造成上问中由于访问多台redis实例才能返回结果而造成的MGET坑)
所以一整个对象会存储在同一个redis实例上,如果一个对象过大,会严重影响redis的分布性能,造成存储和性能上严重的分布不均衡。
使用OBJECT指令可以查看对象的详细信息,如果核实是超大field,那么:
超大field需要进行切割,控制在每个50m左右来保证redis集群的分布均衡
未完待续