Redis详解

 

 

 

1、与mencache比较


  • 性能方面:没有必要过多的关心性能,因为二者的性能都已经足够高了。由于Redis只使用单核,而Memcached可以使用多核,所以在比较上,平均每一个核上Redis在存储小数据时比Memcached性能更高。而在100k以上的数据中,Memcached性能要高于Redis,虽然Redis最近也在存储大数据的性能上进行优化,但是比起Memcached,还是稍有逊色。说了这么多,结论是,无论你使用哪一个,每秒处理请求的次数都不会成为瓶颈。(比如瓶颈可能会在网卡)
  • 内存使用效率:使用简单的key-value存储的话,Memcached的内存利用率更高,而如果Redis采用hash结构来做key-value存储,由于其组合式的压缩,其内存利用率会高于Memcached。当然,这和你的应用场景和数据特性有关。
  • 数据持久化:如果你对数据持久化和数据同步有所要求,那么推荐你选择Redis,因为这两个特性Memcached都不具备。即使你只是希望在升级或者重启系统后缓存数据不会丢失,选择Redis也是明智的。
  • 数据结构:当然,最后还得说到你的具体应用需求。Redis相比Memcached来说,拥有更多的数据结构和并支持更丰富的数据操作,通常在Memcached里,你需要将数据拿到客户端来进行类似的修改再set回去。这大大增加了网络IO的次数和数据体积。在Redis中,这些复杂的操作通常和一般的GET/SET一样高效。所以,如果你需要缓存能够支持更复杂的结构和操作,那么Redis会是不错的选择。
  • 网络IO模型方面:Memcached是多线程,分为监听线程、worker线程,引入锁,带来了性能损耗。Redis使用单线程的IO复用模型,将速度优势发挥到最大,也提供了较简单的计算功能 

  • 内存管理方面:Memcached使用预分配的内存池的方式,带来一定程度的空间浪费 并且在内存仍然有很大空间时,新的数据也可能会被剔除,而Redis使用现场申请内存的方式来存储数据,不会剔除任何非临时数据 Redis更适合作为存储而不是cache 

  • 数据的一致性方面:Memcached提供了cas命令来保证.而Redis提供了事务的功能,可以保证一串 命令的原子性,中间不会被任何操作打断 

    如果简单地比较Redis与Memcached的区别,大多数都会得到以下观点:

     1 、Redis不仅仅支持简单的k/v类型的数据,同时还提供list,set,zset,hash等数据结构的存储。

     2 、Redis支持数据的备份,即master-slave模式的数据备份。

     3 、Redis支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用。

     4、Redis可以实现主从复制,实现故障恢复。

     5、Redis的Sharding技术: 很容易将数据分布到多个Redis实例中

下面我们简单一项一项详解

 

 

2、数据结构说明


http://redis.io/topics/data-types

2.1 Key

1、key越短越好Redis是个内存数据库Key键越短你需要的空间就越少,因此key不能太长,比如1024字节。

举个例子:在一个32位的Redis服务器上,如果储存一百万个键,每个值的长度是32-character,那么在使用6-character长度键名时,将会消耗大约96MB的空间,但是如果使用12-character长度的键名时,空间消耗则会提升至111MB左右。随着键的增多,15%的额外开销将产生重大的影响。

2、key命名要表达清楚意思。建议用”:”分隔域划分键名,用”.”作为单词间的连接,如”comment:1234:reply.to”。

使用合适的命名方法会简化你的数据库管理,当你通过你的应用程序或者服务做键的命名空间时,你就可以在数据迁移、转换或者删除时轻松的识别。

Redis另一个常见用例是作为热数据项作的第二数据存储,大部分的数据被保存在其他的数据库中,比如PostgreSQL或MongoDB。在这些用例中,当数据从主存储移除时,开发者经常会忘记删除Redis中对应的数据。这种存在跨数据存储的情况下,通常需要做级联删除,这种情况下,可以通过在Redis配置保存特定数据项的所有识别符来实现,从而保证数据在主数据库被删除后,系统会调用一个清理程序来删除所有相关副本和信息。

 

 

2.2 String

String是最简单的类型,一个key对应一个value,string类型是二进制安全的。Redis的string可以包含任何数据,比如jpg图片或者序列化的对象(PHP中对象序列化函数serialize)

       内部实现,其本质是一个byte数组,字符串的大小被限制在512M以内

 

struct sdshdr {  
      long len; //buf数组的长度  
      long free; //buf数组中剩余可用字节数  
      char buf[]; //存储实际字符串内容  
}  

 

 

所有常用命令的复杂度都是O(1),普通的Get/Set方法,可以用来做Cache,存Session,为了简化架构甚至可以替换掉Memcached。

Incr/IncrBy/IncrByFloat/Decr/DecrBy,可以用来做计数器,做自增序列。key不存在时会创建并贴心的设原值为0。IncrByFloat专门针对float,没有对应的decrByFloat版本?用负数啊。

SetNx, 仅当key不存在时才Set。可以用来选举Master或做分布式锁:所有Client不断尝试使用SetNx master myName抢注Master,成功的那位不断使用Expire刷新它的过期时间。如果Master倒掉了key就会失效,剩下的节点又会发生新一轮抢夺。

2.3 Hash

Key-HashMap结构,相比String类型将这整个对象持久化成JSON格式,Hash将对象的各个属性存入Map里,可以只读取/更新对象的某些属性。这样有些属性超长就让它一边呆着不动,另外不同的模块可以只更新自己关心的属性而不会互相并发覆盖冲突。

2.4 List

List是一个双向链表,支持双向的Pop/Push,江湖规矩一般从左端Push,右端Pop——LPush/RPop,而且还有Blocking的版本BLPop/BRPop,客户端可以阻塞在那直到有消息到来,所有操作都是O(1)的好孩子,可以当Message Queue来用。当多个Client并发阻塞等待,有消息入列时谁先被阻塞谁先被服务。任务队列系统Resque是其典型应用。

有RPopLPush/ BRPopLPush,弹出来返回给client的同时,把自己又推入另一个list,LLen获取列表的长度。

2.5 Set

Set就是Set,可以将重复的元素随便放入而Set会自动去重,底层实现也是hash table。

2.6 Sorted Set

有序集,元素放入集合时还要提供该元素的分数。

Sorted Set的实现是hash table(element->score, 用于实现ZScore及判断element是否在集合内),和skip list(score->element,按score排序)的混合体。 skip list有点像平衡二叉树那样,不同范围的score被分成一层一层,每层是一个按score排序的链表。

ZAdd/ZRem是O(log(N)),ZRangeByScore/ZRemRangeByScore是O(log(N)+M),N是Set大小,M是结果/操作元素的个数。可见,原本可能很大的N被很关键的Log了一下,1000万大小的Set,复杂度也只是几十不到。当然,如果一次命中很多元素M很大那谁也没办法了。

2.8 Lua Script

Redis2.6内置的Lua Script支持,可以在Redis的Server端一次过运行大量逻辑,就像存储过程一样,避免了海量中间数据在网路上的传输。

Lua自称是在Script语言里关于快的标准,Redis选择了它而不是流行的JavaScript。
因为Redis的单线程架构,整个Script默认是在一个事务里的。
Script里涉及的所有Key尽量用变量,从外面传入,使Redis一开始就知道你要改变哪些key。(but why?)
Eval每次传输一整段Script比较费带宽,可以先用Script Load载入script,返回哈希值。然后用EvalHash执行。因为就是SHA-1,所以任何时候执行返回的哈希值都是一样的。
内置的Lua库里还很贴心的带了CJSON,可以处理json字符串。
一段用Redis做Timer的示例代码,下面的script被定期调用,从以触发时间为score的sorted set中取出已到期的Job,放到list中给Client们blocking popup。
 
— KEYS: [1]job:sleeping, [2]job:ready
— ARGS: [1]currentTime
— Comments: result is the  job id
local jobs=redis.call(‘zrangebyscore’, KEYS[1], ‘-inf’, ARGV[1])
local count = table.maxn(jobs)
 
if count>0  then
  — Comments: remove from Sleeping Job sorted set
  redis.call(‘zremrangebyscore’, KEYS[1], ‘-inf’, ARGV[1])
 
  — Comments: add to the Ready Job list
  — Comments: can optimize to use lpush id1,id2,… for better performance
  for i=1,count do
    redis.call(‘lpush’, KEYS[2], jobs[i])
  end
end

 

在Redis使用过程中,Lua脚本的支持无疑给开发者提供一个非常友好的开发环境,从而大幅度解放用户的创造力。如果使用得当,Lua脚本可以给性能和资源消耗带来非常大的改善。取代将数据传送给CPU,脚本允许你在最接近数据的地方执行逻辑,从而减少网络延时和数据的冗余传输。

在Redis中,Lua一个非常经典的用例就是数据过滤或者将数据聚合到应用程序。通过将处理工作流封装到一个脚本中,你只需要调用它就可以在更短的时间内使用很少的资源来获取一个更小的答案。

提示:Lua确实非常棒,但是同样也存在一些问题,比如很难进行错误报告和处理。一个明智的方法就是使用Redis的Pub/Sub功能,并且让脚本通过专用信道来推送日志消息。然后建立一个订阅者进程,并进行相应的处理。


2.9使用合适的数据结构

 

不管是内存使用或者是性能,有的时候数据结构将产生很大的影响,下面是一些可以参考的最佳实践:

1、使用hash取代将数据存储为数千(或者数百万)独立的字符串。哈希表是非常有效率的,并且可以减少你的内存使用(因为小Hashes会被编码成一个非常小的空间);同时,哈希还更有益于细节抽象和代码可读。

2、合适时候,使用list代替set。如果你不需要使用set特性,List在使用更少内存的情况下可以提供比set更快的速度。

3、Sorted sets是最昂贵的数据结构,不管是内存消耗还是基本操作的复杂性。如果你只是需要一个查询记录的途径,并不在意排序这样的属性,那么轻建议使用哈希表。

4、Redis中一个经常被忽视的功能就是bitmaps或者bitsets(V2.2之后)。Bitsets允许你在Redis值上执行多个bit-level操作,比如一些轻量级的分析。

 

5、使用bit位级别操作和byte字节级别操作来减少不必要的内存使用

 

 

 

 

 

3、性能


 

3.1 测试结果

测试环境: RHEL 6.3 / HP Gen8 Server/ 2 * Intel Xeon 2.00GHz(6 core) / 64G DDR3 memory / 300G RAID-1 SATA / 1 master(writ AOF), 1 slave(write AOF & RDB)

数据准备: 预加载两千万条数据,占用10G内存。 

测试工具:自带的redis-benchmark,默认只是基于一个很小的数据集进行测试,调整命令行参数如下,就可以开100条线程(默认50),SET 1千万次(key在0-1千万间随机),key长21字节,value长256字节的数据。
1
redis-benchmark -t SET -c 100 -n 10000000 -r 10000000 -d 256
测试结果(QPS):

1.SET:4.5万,

2.GET:6万 ,

3.INCR:6万,

4.真实混合场景: 2.5万SET & 3万GET
单条客户端线程时6千TPS,50与100条客户端线程差别不大,200条时会略多。
Get/Set操作,经过了LAN,延时也只有1毫秒左右,可以反复放心调用,不用像调用REST接口和访问数据库那样,每多一次外部访问都心痛。
资源监控:
1.CPU: 占了一个处理器的100%,总CPU是4%(因为总共有2CPU*6核*超线程 = 24个处理器),可见单线程下单处理器的能力是瓶颈。 AOF rewrite时另一个处理器占用50-70%。
2.网卡:15-20 MB/s receive, 3Mb/s send(no slave) or 15-20 MB/s send (with slave) 。当把value长度加到4K时,receive 99MB/s,已经到达千兆网卡的瓶颈,TPS降到2万。
3.硬盘:15MB/s(AOF append), 100MB/s(AOF rewrite/AOF load,普通硬盘的瓶颈),
 

3.2 为什么快

1、纯ANSI C编写。
不依赖第三方类库,没有像memcached那样使用libevent,因为libevent迎合通用性而造成代码庞大,所以作者用libevent中两个文件修改实现了自己的epoll event loop。微软的兼容Windows补丁也因为同样原因被拒了。
快,原因之一是Redis多样的数据结构,每种结构只做自己爱做的事,当然比数据库只有Table,MongogoDB只有JSON一种结构快了。

2、虽然是采用单线程,但是单线程避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU;虽然作者认为CPU不是瓶颈,内存与网络带宽才是。但实际测试时并非如此,见上。
3、使用多路I/O复用模型,非阻塞IO;
    多路I/O复用模型是利用 select、poll、epoll 可以同时监察多个流的 I/O 事件的能力,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有 I/O 事件时,就从阻塞态中唤醒,于是程序就会轮询一遍所有的流(epoll 是只轮询那些真正发出了事件的流),并且只依次顺序的处理就绪的流,这种做法就避免了大量的无用操作。这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程。

 

3.3 性能调优

官方文档关于各种产生Latency的原因的详细分析, 中文版
正视网络往返时间:
1.MSet/LPush/ZAdd等都支持一次输入多个Key。
2.PipeLining模式 可以一次输入多个指令。
3.更快的是Lua Script模式,还可以包含逻辑,直接在服务端又get又set的,见2.8 Lua Script。
发现执行缓慢的命令,可配置执行超过多少时间的指令算是缓慢指令(默认10毫秒,不含IO时间),可以用slowlog get 指令查看(默认只保留最后的128条)。单线程的模型下,一个请求占掉10毫秒是件大事情,注意设置和显示的单位为微秒。

CPU永远是瓶颈,但top看到单个CPU 100%时,就是垂直扩展的时候了。
持久化对性能的影响很大,见5.1持久化。
要熟悉各指令的复杂度,不过只要不是O(N)一个超大集合,都不用太担心。

 

4、内存使用与管理


4.1 最大内存

所有的数据都必须在内存中,原来2.0版的VM策略(将Value放到磁盘,Key仍然放在内存),2.4版后嫌麻烦又不支持了。
一定要设置最大内存,否则物理内存用爆了就会大量使用Swap,写RDB文件时的速度慢得你想死。我们可以通过配置redis.conf中的maxmemory这个值设置最大内存

# maxmemory <bytes>
多留一倍内存是最安全的。重写AOF文件和RDB文件的进程(即使不做持久化,复制到Slave的时候也要写RDB)会fork出一条新进程来,采用了操作系统的Copy-On-Write策略(子进程与父进程共享Page。如果父进程的Page-每页4K有修改,父进程自己创建那个Page的副本,不会影响到子进程,父爱如山)。留意Console打出来的报告,如”RDB: 1215 MB of memory used by copy-on-write”。在系统极度繁忙时,如果父进程的所有Page在子进程写RDB过程中都被修改过了,就需要两倍内存。
按照Redis启动时的提醒,设置 vm.overcommit_memory = 1 ,使得fork()一条10G的进程时,因为COW策略而不一定需要有10G的free memory。
其他需要考虑的内存包括:
1.AOF rewrite过程中对新写入命令的缓存(rewrite结束后会merge到新的aof文件),留意”Background AOF buffer size: 80 MB”的字样。
2.负责与Slave同步的Client的缓存,默认设置master需要为每个slave预留不高于256M的缓存(见5.1持久化)。
当最大内存到达时,按照配置的Policy进行处理, 默认策略为volatile-lru,对设置了expire time的key进行LRU清除(不是按实际expire time)。如果沒有数据设置了expire time或者policy为noeviction,则直接报错,但此时系统仍支持get之类的读操作。 另外还有几种policy,比如volatile-ttl按最接近expire time的,allkeys-lru对所有key都做LRU。

4.2 内存占用

测试表明,string类型需要90字节的额外代价,就是说key 1个字节,value 1个字节时,还是需要占用92字节的长度,而上面的benchmark的记录就占用了367个字节。其他类型可根据文档自行计算或实际测试一下。
使用jemalloc分配内存,删除数据后,内存并不会乖乖还给操作系统而是被Redis截留下来重用到新的数据上,直到Redis重启。因此进程实际占用内存是看INFO里返回的used_memory_peak_human。
Redis内部用了ziplist/intset这样的压缩结构来减少hash/list/set/zset的存储,默认当集合的元素少于512个且最长那个值不超过64字节时使用,可配置。
用make 32bit可以编译出32位的版本,每个指针占用的内存更小,但只支持最大4GB内存。

4.4 水平分区,Sharding

其实,大内存加上垂直分区也够了,不一定非要沙丁一把。
Jedis支持在客户端做分区,局限是不能动态re-sharding, 有分区的master倒了,不能减少分区必须用slave顶上。要增加分区的话,呃…..
antire在博客里提到了Twemproxy,一个Twitter写的Proxy,但它在发现节点倒掉后,只会重新计算一致性哈希环,把数据存到别的master去,而不是集成Sentinel指向新由slave升级的master,像Memcached一样的做法也只适合做Cache的场景。
Redis-Cluster是今年工作重点,支持automatic re-sharding, 采用和Hazelcast类似的算法,总共有N个分区(eg.N=1024),每台Server负责若干个分区。

在客户端先hash出key 属于哪个分区,随便发给一台server,server会告诉它真正哪个Server负责这个分区,缓存下来,下次还有该分区的请求就直接发到地儿了。
Re-sharding时,会将某些分区的数据移到新的Server上,完成后各Server周知分区<->Server映射的变化,因为分区数量有限,所以通讯量不大。 在迁移过程中,客户端缓存的依然是旧的分区映射信息,原server对于已经迁移走的数据的get请求,会返回一个临时转向的应答,客户端先不会更新Cache。等迁移完成了,就会像前面那样返回一条永久转向信息,客户端更新Cache,以后就都去新server了。

内存优化总结:

1、首先最重要的一点是不要开启Redis的VM选项,即虚拟内存功能,这个本来是作为Redis存储超出物理内存数据的一种数据在内存与磁盘换入换出的一个持久化策略,但是其内存管理成本也非常的高,并且我们后续会分析此种持久化策略并不成熟,所以要关闭VM功能,请检查你的redis.conf文件中 vm-enabled 为 no。

2、其次最好设置下redis.conf中的maxmemory选项,该选项是告诉Redis当使用了多少物理内存后就开始拒绝后续的写入请求,该参数能很好的保护好你的Redis不会因为使用了过多的物理内存而导致swap,最终严重影响性能甚至崩溃。

 

5、高可用(1) - 数据持久化机制


1、Redis持久化方式:

Redis由于支持非常丰富的内存数据结构类型,如何把这些复杂的内存组织方式持久化到磁盘上是一个难题,所以Redis的持久化方式与传统数据库的方式有比较多的差别,Redis一共支持四种持久化方式,分别是:

  • RDB定时快照方式(snapshot) RDB 将数据库的快照(snapshot)以二进制的方式保存到磁盘中。
  • AOF基于语句追加文件的方式:则以协议文本的方式,将所有对数据库进行过写入的命令(及其参数)记录到 AOF 文件,以此达到记录数据库状态的目的。
  • 虚拟内存(vm) (被废弃)
  • Diskstore方式 (被废弃)

 

在设计思路上,前两种是基于全部数据都在内存中,即小数据量下提供磁盘落地功能,而后两种方式则是作者在尝试存储数据超过物理内存时,即大数据量的数据存储,截止到本文,后两种持久化方式仍然是在实验阶段,并且vm方式基本已经被作者放弃,所以实际能在生产环境用的只有前两种,换句话说Redis目前还只能作为小数据量存储(全部数据能够加载在内存中),海量数据存储方面并不是Redis所擅长的领域。

很多人开始会想象两者是互相结合的,即dump出一个snapshot到RDB文件,然后在此基础上记录变化日志到AOF文件。实际上两者毫无关系,完全独立运行,因为作者认为简单才不会出错。如果使用了AOF,重启时只会从AOF文件载入数据,不会再管RDB文件。
正确关闭服务器:redis-cli shutdown 或者 kill,都会graceful shutdown,保证写RDB文件以及将AOF文件fsync到磁盘,不会丢失数据。 如果是粗暴的Ctrl+C,或者kill -9 就可能丢失。

下面分别介绍下这几种持久化方式:

1、定时快照方式(snapshot):

该持久化方式实际是在Redis内部一个定时器事件,每隔固定时间去检查当前数据发生的改变次数与时间是否满足配置的持久化触发的条件,如果满足则通过操作系统fork调用来创建出一个子进程,这个子进程默认会与父进程共享相同的地址空间,这时就可以通过子进程来遍历整个内存来进行存储操作,而主进程则仍然可以提供服务,当有写入时由操作系统按照内存页(page)为单位来进行copy-on-write保证父子进程之间不会互相影响。

流程:

1)RDB写入时,会连内存一起Fork出一个新进程(子进程默认会与父进程共享相同的地址空间),遍历新进程内存中的数据写文件,这样就解决了些Snapshot过程中又有新的写入请求进来的问题。

2)RDB会先写到临时文件,完了再Rename成,这样外部程序对RDB文件的备份和传输过程是安全的。而且即使写新快照的过程中Server被强制关掉了,旧的RDB文件还在。

3)可配置是否进行压缩,压缩方法是字符串的LZF算法,以及将string形式的数字变回int形式存储。
4)动态所有停止RDB保存规则的方法:redis-cli config set save “”

该持久化的主要缺点是定时快照只是代表一段时间内的内存映像,所以系统重启会丢失上次快照与重启之间所有的数据。

 

2、基于语句追加方式(AOF):

AOF方式实际类似mysql的基于语句的binlog方式,即每条会使Redis内存数据发生改变的命令都会追加到一个log文件中,也就是说这个log文件就是Redis的持久化数据。

AOF的方式的主要缺点是追加log文件可能导致体积过大,当系统重启恢复数据时如果是AOF的方式则加载数据会非常慢,几十G的数据可能需要几小时才能加载完,当然这个耗时并不是因为磁盘文件读取速度慢,而是由于读取的所有命令都要在内存中执行一遍。另外由于每条命令都要写log,所以使用AOF的方式,Redis的读写性能也会有所下降。

========================================

 

 

一 般配置成每秒调用一次fdatasync将kernel的文件缓存刷到磁盘。当操作系统非正常关机时,文件可能会丢失不超过2秒的数据(更严谨的定义见 后)。 如果设为fsync always,性能只剩几百TPS,不用考虑。如果设为no,靠操作系统自己的sync,Linux系统一般30秒一次。
AOF文件持续增长而过大时,会fork出一条新进程来将文件重写(也是先写临时文件,最后再rename,), 遍历新进程的内存中数据,每条记录有一条的Set语句。默认配置是当AOF文件大小是上次rewrite后大小的一倍,且文件大于64M时触发。
Redis 协议,如set mykey hello, 将持久化成*3 $3 set $5 mykey $5 hello, 第一个数字代表这条语句有多少元,其他的数字代表后面字符串的长度。这样的设计,使得即使在写文件过程中突然关机导致文件不完整,也能自我修复,执行 redis-check-aof即可。
综上所述,RDB的数据不实时,同时使用两者时服务器重启也只会找AOF文件。那要不要只使用AOF呢?作者建议不要,因为RDB更适合用于备份数据库(AOF在不断变化不好备份),快速重启,而且不会有AOF可能潜在的bug,留着作为一个万一的手段。

2、Redis持久化磁盘IO方式及其带来的问题

 

     有Redis线上运维经验的人会发现Redis在物理内存使用比较多,但还没有超过实际物理内存总容量时就会发生不稳定甚至崩溃的问题,有人认为是基于快照方式持久化的fork系统调用造成内存占用加倍而导致的,这种观点是不准确的,因为fork 调用的copy-on-write机制是基于操作系统页这个单位的,也就是只有有写入的脏页会被复制,但是一般你的系统不会在短时间内所有的页都发生了写入而导致复制,那么是什么原因导致Redis崩溃的呢?

答案是Redis的持久化使用了Buffer IO造成的,所谓Buffer IO是指Redis对持久化文件的写入和读取操作都会使用物理内存的Page Cache,而大多数数据库系统会使用Direct IO来绕过这层Page Cache并自行维护一个数据的Cache,而当Redis的持久化文件过大(尤其是快照文件),并对其进行读写时,磁盘文件中的数据都会被加载到物理内存中作为操作系统对该文件的一层Cache,而这层Cache的数据与Redis内存中管理的数据实际是重复存储的,虽然内核在物理内存紧张时会做Page Cache的剔除工作,但内核很可能认为某块Page Cache更重要,而让你的进程开始Swap ,这时你的系统就会开始出现不稳定或者崩溃了。我们的经验是当你的Redis物理内存使用超过内存总容量的3/5时就会开始比较危险了。
下图是Redis在读取或者写入快照文件dump.rdb后的内存数据图:

 

3、Redis持久化读写性能

 

AOF重写和RDB写入都是在fork出新进程后,遍历新进程的内存顺序写的,既不阻塞主进程继续处理客户端请求,顺序写的速度也比随机写快。
测试把刚才benchmark的11G数据写成一个1.3的RDB文件,或者等大的AOF文件rewrite,需要80秒,在redis-cli info中可查看。启动时载入一个AOF或RDB文件的速度与上面写入时相同,在log中可查看。
Fork一个使用了大量内存的进程也要时间,大约10ms per GB的样子,但Xen在EC2上是让人郁闷的239ms (KVM和VMWare貌似没有这个毛病),各种系统的对比,Info指令里的latest_fork_usec显示上次花费的时间。
在 bgrewriteaof过程中,所有新来的写入请求依然会被写入旧的AOF文件,同时放到buffer中,当rewrite完成后,会在主线程把这部分 内容合并到临时文件中之后才rename成新的AOF文件,所以rewrite过程中会不断打印”Background AOF buffer size: 80 MB, Background AOF buffer size: 180 MB”,计算系统容量时要留意这部分的内存消耗。注意,这个合并的过程是阻塞的,如果你产生了280MB的buffer,在100MB/s的传统硬盘 上,Redis就要阻塞2.8秒!!!
NFS或者Amazon上的EBS都不推荐,因为它们也要消耗带宽。
bgsave和bgaofrewrite不会被同时执行,如果bgsave正在执行,bgaofrewrite会自动延后。
2.4版以后,写入AOF时的fdatasync由另一条线程来执行,不会再阻塞主线程。
2.4版以后,lpush/zadd可以输入一次多个值了,使得AOF重写时可以将旧版本中的多个lpush/zadd指令合成一个,每64个key串一串。

 

4、Redis持久化性能调整

因为RDB文件只用作后备用途,建议只在Slave上持久化RDB文件,而且只要15分钟备份一次就够了,只保留save 900 1这条规则。

 

如 果Enalbe AOF,好处是在最恶劣情况下也只会丢失不超过两秒数据,启动脚本较简单只load自己的AOF文件就可以了。代价一是带来了持续的IO,二是AOF rewrite的最后将rewrite过程中产生的新数据写到新文件造成的阻塞几乎是不可避免的。只要硬盘许可,应该尽量减少AOF rewrite的频率,AOF重写的基础大小默认值64M太小了,可以设到5G以上。默认超过原大小100%大小时重写可以改到适当的数值,比如之前的 benchmark每个小时会产生40G大小的AOF文件,如果硬盘能撑到半夜系统闲时才用cron调度bgaofrewrite就好了。

如 果不Enable AOF ,仅靠Master-Slave Replication 实现高可用性也可以。能省掉一大笔IO也减少了rewrite时带来的系统波动。代价是如果Master/Slave同时倒掉,会丢失十几分钟的数据,启 动脚本也要比较两个Master/Slave中的RDB文件,载入较新的那个。新浪微博就选用了这种架构,见Tim的博客

 

5 Trouble Shooting —— Enable AOF可能导致整个Redis被Block住,在2.6.12版之前

现象描述:当AOF rewrite 15G大小的内存时,Redis整个死掉的样子,所有指令甚至包括slave发到master的ping,redis-cli info都不能被执行。

 

原因分析:

官方文档,由IO产生的Latency详细分析, 已经预言了悲剧的发生,但一开始没留意。
Redis为求简单,采用了单请求处理线程结构。
打开AOF持久化功能后, Redis处理完每个事件后会调用write(2)将变化写入kernel的buffer,如果此时write(2)被阻塞,Redis就不能处理下一个事件。
Linux规定执行write(2)时,如果对同一个文件正在执行fdatasync(2)将kernel buffer写入物理磁盘,或者有system wide sync在执行,write(2)会被block住,整个Redis被block住。
如 果系统IO繁忙,比如有别的应用在写盘,或者Redis自己在AOF rewrite或RDB snapshot(虽然此时写入的是另一个临时文件,虽然各自都在连续写,但两个文件间的切换使得磁盘磁头的寻道时间加长),就可能导致 fdatasync(2)迟迟未能完成从而block住write(2),block住整个Redis。
为了更清晰的看到fdatasync(2)的执行时长,可以使用”strace -p (pid of redis server) -T -e -f trace=fdatasync”,但会影响系统性能。
Redis 提供了一个自救的方式,当发现文件有在执行fdatasync(2)时,就先不调用write(2),只存在cache里,免得被block。但如果已经 超过两秒都还是这个样子,则会硬着头皮执行write(2),即使redis会被block住。此时那句要命的log会打印:“Asynchronous AOF fsync is taking too long (disk is busy?). Writing the AOF buffer without waiting for fsync to complete, this may slow down Redis.” 之后用redis-cli INFO可以看到aof_delayed_fsync的值被加1。
因此,对于fsync设为everysec时丢失数 据的可能性的最严谨说法是:如果有fdatasync在长时间的执行,此时redis意外关闭会造成文件里不多于两秒的数据丢失。如果fdatasync 运行正常,redis意外关闭没有影响,只有当操作系统crash时才会造成少于1秒的数据丢失。
解决方法:
最后发现,原来是AOF rewrite时一直埋头的调用write(2),由系统自己去触发sync。在RedHat Enterprise 6里,默认配置vm.dirty_background_ratio=10,也就是占用了10%的可用内存才会开始后台flush,而我的服务器有64G 内存。很明显一次flush太多数据会造成阻塞,所以最后果断设置了sysctl vm.dirty_bytes=33554432(32M),问题解决。

然后提了个issue,AOF rewrite时定时也执行一下fdatasync嘛, antirez三分钟后就回复了,新版中,AOF rewrite时32M就会重写主动调用fdatasync。

 

6、高可用(2)- 主从复制


1、概述

 

slave可以在配置文件、启动命令行参数、以及redis-cli执行SlaveOf指令来设置自己是奴隶。
测试表明同步延时非常小,指令一旦执行完毕就会立刻写AOF文件和向Slave转发,除非Slave自己被阻塞住了。
比较蠢的是,即使在配置文件里设了slavof,slave启动时依然会先从数据文件载入一堆没用的数据,再去执行slaveof。
“Slaveof no one”,立马变身master。
2.8 版本将支持PSYNC部分同步,master会拨出一小段内存来存放要发给slave的指令,如果slave短暂的断开了,重连时会从内存中读取需要补读 的指令,这样就不需要断开两秒也搞一次全同步了。但如果断开时间较长,已经超过了内存中保存的数据,就还是要全同步。
Slave也可以接收Read-Only的请求。

 

2、slaveof执行过程

 

完全重用已有功能,非常经济
先执行一次全同步 — 请求master BgSave出自己的一个RDB Snapshot文件发给slave,slave接收完毕后,清除掉自己的旧数据,然后将RDB载入内存。
再进行增量同步 — master作为一个普通的client连入slave,将所有写操作转发给slave,没有特殊的同步协议。

3 Trouble Shooting again

有时候明明master/slave都活得好好的,突然间就说要重新进行全同步了:

 

1.Slave显示:# MASTER time out: no data nor PING received…

slave 会每隔repl-ping-slave-period(默认10秒)ping一次master,如果超过repl-timeout(默认60秒)都没有收 到响应,就会认为Master挂了。如果Master明明没挂但被阻塞住了也会报这个错。可以适当调大repl-timeout。

2.Master 显示:# Client addr=10.175.162.123:44670 flags=S oll=104654 omem=2147487792 events=rw cmd=sync scheduled to be closed ASAP for overcoming of output buffer limits.

当 slave没挂但被阻塞住了,比如正在loading Master发过来的RDB, Master的指令不能立刻发送给slave,就会放在output buffer中(见oll是命令数量,omem是大小),在配置文件中有如下配置:client-output-buffer-limit slave 256mb 64mb 60, 这是说负责发数据给slave的client,如果buffer超过256m或者连续60秒超过64m,就会被立刻强行关闭!!! Traffic大的话一定要设大一点。否则就会出现一个很悲剧的循环,Master传输一个大的RDB给Slave,Slave努力的装载,但还没装载 完,Master对client的缓存满了,再来一次。

平时可以在master执行 redis-cli client list 找那个cmd=sync,flag=S的client,注意OMem的变化。

7、高可用(3)- Fail-over


Redis-sentinel是2.6版开始加入的另一组独立运行的节点,提供自动Fail Over的支持。

官方文档 与 Redis核心解读–集群管理工具(Redis-sentinel)
antirez 对 Sentinel的反驳,与下篇

1 、主要执行过程

 

Sentinel每秒钟对所有master,slave和其他sentinel执行Ping,redis-server节点要应答+PONG或-LOADING或-MASTERDOWN.
如果某一台Sentinel没有在30秒内(可配置得短一些哦)收到上述正确应答,它就会认为master处于sdown状态(主观Down)
它向其他sentinel询问是否也认为该master倒了(SENTINEL is-master-down-by-addr ), 如果quonum台(默认是2)sentinel在5秒钟内都这样认为,就会认为master真是odown了(客观Down)。
此时会选出一台sentinel作为Leader执行fail-over, Leader会从slave中选出一个提升为master(执行slaveof no one),然后让其他slave指向它(执行slaveof new master)。

 

2、 master/slave 及 其他sentinel的发现

master 地址在sentinel.conf里, sentinel会每10秒一次向master发送INFO,知道master的slave有哪些。 如果master已经变为slave,sentinel会分析INFO的应答指向新的master。以前,sentinel重启时,如果master已经 切换过了,但sentinel.conf里master的地址并没有变,很可能有悲剧发生。另外master重启后如果没有切换成slave,也可能有悲 剧发生。新版好像修复了一点这个问题,待研究。

 

另 外,sentinel会在master上建一个pub/sub channel,名为”sentinel:hello”,通告各种信息,sentinel们也是通过接收pub/sub channel上的+sentinel的信息发现彼此,因为每台sentinel每5秒会发送一次自己的host信息,宣告自己的存在。

 

3、自定义reconfig脚本

sentinel在failover时还会执行配置文件里指定的用户自定义reconfig脚本,做用户自己想做的事情,比如让master变为slave并指向新的master。
脚 本的将会在命令行按顺序传入如下参数: <master-name> <role(leader/observer)> <state(上述三种情况)> <from-ip> <from-port> <to-ip> <to-port>
脚本返回0是正常,如果返回1会被重新执行,如果返回2或以上不会。 如果超过60秒没返回会被强制终止。
觉得Sentinel至少有两个可提升的地方:

 

一是如果master 主动shutdown,比如系统升级,有办法主动通知sentinel提升新的master,减少服务中断时间。
二是比起redis-server太原始了,要自己丑陋的以nohup sentinel > logfile 2>&1 & 启动,也不支持shutdown命令,要自己kill pid。

4、Client的高可用性

 

基 于Sentinel的方案,client需要执行语句SENTINEL get-master-addr-by-name mymaster 可获得当前master的地址。 Jedis正在集成sentinel,已经支持了sentinel的一些指令,但还没发布,但sentinel版的连接池则暂时完全没有,在公司的项目里 我参考网友的项目自己写了一个。

淘 宝的Tedis driver,使用了完全不同的思路,不基于Sentinel,而是多写随机读, 一开始就同步写入到所有节点,读的话随便读一个还活着的节点就行了。但有些节点成功有些节点失败如何处理? 节点死掉重新起来后怎么重新同步?什么时候可以重新Ready? 所以不是很敢用。

另外如Ruby写的redis_failover,也是抛开了Redis Sentinel,基于ZooKeeper的临时方案。

Redis作者也在博客里抱怨怎么没有人做Dynamo-style 的client。

 

8、数据一致性:事务


      用Multi(Start Transaction)、Exec(Commit)、Discard(Rollback)实现。 在事务提交前,不会执行任何指令,只会把它们存到一个队列里,不影响其他客户端的操作。在事务提交时,批量执行所有指令。《Redis设计与实现》中的详述。

注意,Redis里的事务,与我们平时的事务概念很不一样:

它仅仅是保证事务里的操作会被连续独占的执行。因为是单线程架构,在执行完事务内所有指令前是不可能再去同时执行其他客户端的请求的。
它没有隔离级别的概念,因为事务提交前任何指令都不会被实际执行,也就不存在”事务内的查询要看到事务里的更新,在事务外查询不能看到”这个让人万分头痛的问题。
它不保证原子性——所有指令同时成功或同时失败,只有决定是否开始执行全部指令的能力,没有执行到一半进行回滚的能力。在redis里失败分两种,一种是明显的指令错误,比如指令名拼错,指令参数个数不对,在2.6版中全部指令都不会执行。另一种是隐含的,比如在事务里,第一句是SET foo bar, 第二句是LLEN foo,对第一句产生的String类型的key执行LLEN会失败,但这种错误只有在指令运行后才能发现,这时候第一句成功,第二句失败。还有,如果事务执行到一半redis被KILL,已经执行的指令同样也不会被回滚。
Watch指令,类似乐观锁,事务提交时,如果Key的值已被别的客户端改变,比如某个list已被别的客户端push/pop过了,整个事务队列都不会被执行。

9、内存淘汰清理机制


官方文档 与 《Redis设计与实现》中的详述,过期数据的清除从来不容易,为每一条key设置一个timer,到点立刻删除的消耗太大,每秒遍历所有数据消耗也大,Redis使用了一种相对务实的做法: 当client主动访问key会先对key进行超时判断,过时的key会立刻删除。 如果clien永远都不再get那条key呢? 它会在Master的后台,每秒10次的执行如下操作: 随机选取100个key校验是否过期,如果有25个以上的key过期了,立刻额外随机选取下100个key(不计算在10次之内)。可见,如果过期的key不多,它最多每秒回收200条左右,如果有超过25%的key过期了,它就会做得更多,但只要key不被主动get,它占用的内存什么时候最终被清理掉只有天知道。

 

redis为了更好地实现这个功能,必须为不同的应用场景提供不同的策略,内存淘汰策略讲的是为实现内存淘汰我们具体怎么做,要解决的问题包括淘汰键空间如何选择?在键空间中淘汰键如何选择?

Redis提供了下面几种淘汰策略供用户选择,其中默认的策略为noeviction策略:

  • noeviction:当内存使用达到阈值的时候,所有引起申请内存的命令会报错。

  • allkeys-lru:在主键空间中,优先移除最近未使用的key。

  • volatile-lru:在设置了过期时间的键空间中,优先移除最近未使用的key。

  • allkeys-random:在主键空间中,随机移除某个key。

  • volatile-random:在设置了过期时间的键空间中,随机移除某个key。

  • volatile-ttl:在设置了过期时间的键空间中,具有更早过期时间的key优先移除。

提示:主键空间和设置了过期时间的键空间,举个例子,假设我们有一批键存储在Redis中,则有那么一个哈希表用于存储这批键及其值,如果这批键中有一部分设置了过期时间,那么这批键还会被存储到另外一个哈希表中,这个哈希表中的值对应的是键被设置的过期时间。设置了过期时间的键空间为主键空间的子集。

 

如何选择淘汰策略

我们了解了Redis大概提供了这么几种淘汰策略,那么如何选择呢?淘汰策略的选择可以通过下面的配置指定:

# maxmemory-policy noeviction

但是这个值填什么呢?为解决这个问题,我们需要了解我们的应用请求对于Redis中存储的数据集的访问方式以及我们的诉求是什么。同时Redis也支持Runtime修改淘汰策略,这使得我们不需要重启Redis实例而实时的调整内存淘汰策略。

下面看看几种策略的适用场景:

 

  • allkeys-lru:如果我们的应用对缓存的访问符合幂律分布(也就是存在相对热点数据),或者我们不太清楚我们应用的缓存访问分布状况,我们可以选择allkeys-lru策略。

  • allkeys-random:如果我们的应用对于缓存key的访问概率相等,则可以使用这个策略。

  • volatile-ttl:这种策略使得我们可以向Redis提示哪些key更适合被eviction。

另外,volatile-lru策略和volatile-random策略适合我们将一个Redis实例既应用于缓存和又应用于持久化存储的时候,然而我们也可以通过使用两个Redis实例来达到相同的效果,值得一提的是将key设置过期时间实际上会消耗更多的内存,因此我们建议使用allkeys-lru策略从而更有效率的使用内存。

 

非精准的LRU

上面提到的LRU(Least Recently Used)策略,实际上Redis实现的LRU并不是可靠的LRU,也就是名义上我们使用LRU算法淘汰键,但是实际上被淘汰的键并不一定是真正的最久没用的,这里涉及到一个权衡的问题,如果需要在全部键空间内搜索最优解,则必然会增加系统的开销,Redis是单线程的,也就是同一个实例在每一个时刻只能服务于一个客户端,所以耗时的操作一定要谨慎。为了在一定成本内实现相对的LRU,早期的Redis版本是基于采样的LRU,也就是放弃全部键空间内搜索解改为采样空间搜索最优解。自从Redis3.0版本之后,Redis作者对于基于采样的LRU进行了一些优化,目的是在一定的成本内让结果更靠近真实的LRU。

10、总结


  1. 根据业务需要选择合适的数据类型,并为不同的应用场景设置相应的紧凑存储参数。
  2. 当业务场景不需要数据持久化时,关闭所有的持久化方式可以获得最佳的性能以及最大的内存使用量。
  3. 如果需要使用持久化,根据是否可以容忍重启丢失部分数据在快照方式与语句追加方式之间选择其一,不要使用虚拟内存以及diskstore方式。
  4. 不要让你的Redis所在机器物理内存使用超过实际内存总量的3/5。

 

 

 

 

 

参考http://www.hishenyi.com/archives/248

 

  • 6
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

hguisu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值