1. Overview
1.1 资料
1.2 优缺点
很是很是的快,有测评说比Memcached还快(当你们都是单CPU的时候),并且是无短板的快,读写都通常的快,全部API都差很少快,也没有MySQL Cluster、MongoDB那样更新同一条记录如Counter时慢下去的毛病。php
丰富的数据结构,超越了通常的Key-Value数据库而被认为是一个数据结构服务器。组合各类结构,限制Redis用途的是你本身的想象力,做者本身捉刀写的用途入门html
由于是我的做品,Redis目前只有2.3万行代码,Keep it simple的死硬作法,使得普通公司而不需淘宝那个级别的文艺公司也能够吃透它。 Redis宣言就是做者的自白,我最喜欢其中的“代码像首诗”,”设计是一场与复杂性的战斗“,“Coding是一件艰苦的事情,惟一的办法是享受它。如 果它已不能带来快乐就中止它。为了防止这一天的出现,咱们要尽可能避免把Redis往乏味的路上带。”java
让人又爱又恨的单线程架构,使得代码不用处理平时最让人头痛的并发而大幅简化,但也带来CPU的瓶颈,并且单线程被慢操做所阻塞时,其余请求的延时变得不肯定。mysql
那Redis不是什么?git
- Redis 不是Big Data,数据都在内存中,没法以T为单位。
- 在Redis-Cluster发布并被稳定使用以前,Redis没有真正的平滑水平扩展能力。
- Redis 不支持Ad-Hoc Query,提供的只是数据结构的API,没有SQL同样的查询能力。
1.3 Feature速览
- 全部数据都在内存中。
- 五种数据结构:String / Hash / List / Set / Ordered Set。
- 数据过时时间支持。
- 不彻底的事务支持。
- 服务端脚本:使用Lua Script编写,相似存储过程的做用。
- PubSub:捞过界的消息一对多发布订阅功能,起码Redis-Sentinel使用了它。
- 持久化:支持按期导出内存的Snapshot 与 记录写操做日志的Append Only File两种模式。
- Replication:Master-Slave模式,Master可链接多个只读Slave,暂无专门的Geographic Replication支持。
- Fail-Over:Redis-Sentinel节点负责监控Master节点,在master失效时提高slave,独立的仲裁节点模式有效防止脑裂。
- Sharding:开发中的Redis-Cluser。
- 动态配置:全部参数可用命令行动态配置不需重启,并从新写回配置文件中,对云上的大规模部署很是合适。
1.4 八卦
- 做者是意大利的Salvatore Sanfilippo(antirez),又是VMWare大善人聘请了他专心写Redis。
- antirez和我同样不喜欢搞什么咨询服务,不过最近VMWare旗下的Pivotal公司开始招聘Redis Commericial Engineer。
- 默认端口6379,是手机按键上MERZ对应的号码,意大利歌女Alessia Merz是antirez和朋友们认为愚蠢的代名词。
数据结构
2.1 Key
- Key 不能太长,好比1024字节,但antirez也不喜欢过短如”u:1000:pwd”,要表达清楚意思才好。他私人建议用”:”分隔域,用”.”做为单词间的链接,如”comment:1234:reply.to”。
- Keys,返回匹配的key,支持通配符如 “keys a*” 、 “keys a?c”,但不建议在生产环境大数据量下使用。
- Sort,对集合按数字或字母顺序排序后返回或另存为list,还能够关联到外部key等。由于复杂度是最高的O(N+M*log(M))(N是集合大小,M 为返回元素的数量),有时会安排到slave上执行。
- Expire/ExpireAt/Persist/TTL,关于Key超时的操做。默认以秒为单位,也有p字头的以毫秒为单位的版本, Redis的内部实现见2.9 过时数据清除。
2.2 String
最普通的key-value类型,说是String,实际上是任意的byte[],好比图片,最大512M。 全部经常使用命令的复杂度都是O(1),普通的Get/Set方法,能够用来作Cache,存Session,为了简化架构甚至能够替换掉Memcached。github
Incr/IncrBy/IncrByFloat/Decr/DecrBy,能够用来作计数器,作自增序列。key不存在时会建立并贴心的设原值为0。IncrByFloat专门针对float,没有对应的decrByFloat版本?用负数啊。web
SetNx, 仅当key不存在时才Set。能够用来选举Master或作分布式锁:全部Client不断尝试使用SetNx master myName抢注Master,成功的那位不断使用Expire刷新它的过时时间。若是Master倒掉了key就会失效,剩下的节点又会发生新一轮抢 夺。redis
其余Set指令:算法
- SetEx, Set + Expire 的简便写法,p字头版本以毫秒为单位。
- GetSet, 设置新值,返回旧值。好比一个按小时计算的计数器,能够用GetSet获取计数并重置为0。这种指令在服务端作起来是举手之劳,客户端便方便不少。
- MGet/MSet/MSetNx, 一次get/set多个key。
- 2.6.12版开始,Set命令已融合了Set/SetNx/SetEx三者,SetNx与SetEx可能会被废弃。
GetBit/SetBit/BitOp,与或非/BitCount, BitMap的玩法,好比统计今天的独立访问用户数时,每一个注册用户都有一个offset,他今天进来的话就把他那个位设为1,用BitCount就能够得出今天的总人数。spring
Append/SetRange/GetRange/StrLen,对文本进行扩展、替换、截取和求长度,只对特定数据格式如字段定长的有用,json就没什么用。
2.3 Hash
Key-HashMap结构,相比String类型将这整个对象持久化成JSON格式,Hash将对象的各个属性存入Map里,能够只读取/更新对象的某些属性。这样有些属性超长就让它一边呆着不动,另外不一样的模块能够只更新本身关心的属性而不会互相并发覆盖冲突。
另外一个用法是土法建索引。好比User对象,除了id有时还要按name来查询。能够有以下的数据记录:
- (String) user:101 -> {“id”:101,”name”:”calvin”…}
- (String) user:102 -> {“id”:102,”name”:”kevin”…}
- (Hash) user:index-> “calvin”->101, “kevin” -> 102
底层实现是hash table,通常操做复杂度是O(1),要同时操做多个field时就是O(N),N是field的数量。
2.4 List
List是一个双向链表,支持双向的Pop/Push,江湖规矩通常从左端Push,右端Pop——LPush/RPop,并且还有Blocking的版本BLPop/BRPop,客户端能够阻塞在那直到有消息到来,全部操做都是O(1)的好孩子,能够当Message Queue来用。当多个Client并发阻塞等待,有消息入列时谁先被阻塞谁先被服务。任务队列系统Resque是其典型应用。
还有RPopLPush/ BRPopLPush,弹出来返回给client的同时,把本身又推入另外一个list,LLen获取列表的长度。
还有按值进行的操做:LRem(按值删除元素)、LInsert(插在某个值的元素的先后),复杂度是O(N),N是List长度,由于List的值不惟一,因此要遍历所有元素,而Set只要O(log(N))。
按下标进行的操做:下标从0开始,队列从左到右算,下标为负数时则从右到左。
- LSet ,按下标设置元素值。
- LIndex,按下标返回元素。
- LRange,不一样于POP直接弹走元素,只是返回列表内一段下标的元素,是分页的最爱。
- LTrim,限制List的大小,好比只保留最新的20条消息。
复杂度也是O(N),其中LSet的N是List长度,LIndex的N是下标的值,LRange的N是start的值+列出元素的个数,由于是链表而不是数组,因此按下标访问其实要遍历链表,除非下标正好是队头和队尾。LTrim的N是移除元素的个数。
在消息队列中,并无JMS的ack机制,若是消费者把job给Pop走了又没处理完就死机了怎么办?
- 解决方法之一是加多一个sorted set,分发的时候同时发到list与sorted set,以分发时间为score,用户把job作完了以后要用ZREM消掉sorted set里的job,而且定时从sorted set中取出超时没有完成的任务,从新放回list。
- 另外一个作法是为每一个worker多加一个的list,弹出任务时改用RPopLPush,将job同时放到worker本身的list中,完成时 用LREM消掉。若是集群管理(如zookeeper)发现worker已经挂掉,就将worker的list内容从新放回主list。
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.7 事务
用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过了,整个事务队列都不会被执行。
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
2.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,它占用的内存何时最终被清 理掉只有天知道。
3. 性能
3.1 测试结果
3.2 为何快
- 纯ANSI C编写。
- 不依赖第三方类库,没有像memcached那样使用libevent,由于libevent迎合通用性而形成代码庞大,因此做者用libevent中两个文件修改实现了本身的epoll event loop。微软的兼容Windows补丁也由于一样缘由被拒了。
- 快,缘由之一是Redis多样的数据结构,每种结构只作本身爱作的事,固然比数据库只有Table,MongogoDB只有JSON一种结构快了。
- 惋惜单线程架构,虽然做者认为CPU不是瓶颈,内存与网络带宽才是。但实际测试时并不是如此,见上。
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文件时的速度慢得你想死。
- 多留一倍内存是最安全的。重写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了。
5. 高可用性
高可用性关乎系统出错时到底会丢失多少数据,多久不能服务。要综合考虑持久化,Master-Slave复制及Fail-Over配置,以及具体Crash情形,好比Master死了,但Slave没死。或者只是Redis死了,操做系统没死等等。
5.1 持久化
- 综述: 解密Redis持久化(中文归纳版), 英文原版,《Redis设计与实现》: RDB 与 AOF。
- 不少人开始会想象二者是互相结合的,即dump出一个snapshot到RDB文件,而后在此基础上记录变化日志到AOF文件。实际上二者毫无关系,彻底独立运行,由于做者认为简单才不会出错。若是使用了AOF,重启时只会从AOF文件载入数据,不会再管RDB文件。
- 正确关闭服务器:redis-cli shutdown 或者 kill,都会graceful shutdown,保证写RDB文件以及将AOF文件fsync到磁盘,不会丢失数据。 若是是粗暴的Ctrl+C,或者kill -9 就可能丢失。
5.1.1 RDB文件
- RDB是整个内存的压缩过的Snapshot,RDB的数据结构,能够配置复合的快照触发条件,默认是1分钟内改了1万次,或5分钟内改了10次,或15分钟内改了1次。
- RDB写入时,会连内存一块儿Fork出一个新进程,遍历新进程内存中的数据写文件,这样就解决了些Snapshot过程当中又有新的写入请求进来的问题。 Fork的细节见4.1最大内存。
- RDB会先写到临时文件,完了再Rename成,这样外部程序对RDB文件的备份和传输过程是安全的。并且即便写新快照的过程当中Server被强制关掉了,旧的RDB文件还在。
- 可配置是否进行压缩,压缩方法是字符串的LZF算法,以及将string形式的数字变回int形式存储。
- 动态全部中止RDB保存规则的方法:redis-cli config set save “”
5.1.2 AOF文件
- 操做日志,记录全部有效的写操做,等于mysql的binlog,格式就是明文的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,留着做为一个万一的手段。
5.1.3 读写性能
- 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串一串。
5.1.4 性能调整
由于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.1.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。
5.2 Master-Slave复制
5.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的请求。
5.2.2 slaveof执行过程,彻底重用已有功能,很是经济
- 先执行一次全同步 — 请求master BgSave出本身的一个RDB Snapshot文件发给slave,slave接收完毕后,清除掉本身的旧数据,而后将RDB载入内存。
- 再进行增量同步 — master做为一个普通的client连入slave,将全部写操做转发给slave,没有特殊的同步协议。
5.2.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的变化。
5.3 Fail-Over
Redis-sentinel是2.6版开始加入的另外一组独立运行的节点,提供自动Fail Over的支持。
5.3.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)。
5.3.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信息,宣告本身的存在。
5.3.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。
5.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。
6. 运维
6.1 安装
- 安装包制做:没有现成,须要本身编译,本身写rpm包的脚本,可参考utils中的install_server.sh与redis_init_script。
但RHEL下设定script runlevel的方式不同,redis_init_script中要增长一句 “# chkconfig: 345 90 10″ ,而install_server.sh能够删掉后面的那句“chkconfig –level 345 reis”
- 云服务:http://redis-cloud.com/ 在Amazon、Heroku、Windows Azure、App Frog上提供云服务,供一样部署在这些云上的应用使用。
- CopperEgg统计本身的用户在AWS上的数据库部署:mysqld占了50%半壁江山, redis占了18%排第二, mongodb也有11%, cassandra是3%,Oracle只有可怜的2%。
- Chef Recipes:brianbianco/redisio,活跃,同步更新版本。
6.2 部署模型
- Redis只能使用单线程,为了提升CPU利用率,有提议在同一台服务器上启动多个Redis实例,但这会带来严重的IO争用,除非Redis不须要持久化,或者有某种方式保证多个实例不会在同一个时间重写AOF。
- 一组sentinel能同时监控多个Master。
- 有提议说环形的slave结构,即master只连一个slave,而后slave再连slave,此部署有两个前提,一是有大量的只读需求须要在slave完成,二是对slave传递时的数据不一致性不敏感。
6.3 配置
约30个配置项,全都有默认配置,对redif.conf默认配置的修改见附录1。
6.3.1 三条路
- 能够配置文件中编写。
- 能够在启动时的命令行配置,redis-server –port 7777 –slaveof 127.0.0.1 8888。
- 云时代大规模部署,把配置文件满街传显然不是好的作法, 能够用redis-cli执行Config Set指令, 修改全部的参数,达到维护人员最爱的不重启服务而修改参数的效果,并且在新版本里还能够执行 Config Rewrite 将改动写回到文件中,不过所有默认值都会打印出来,可能会破坏掉原来的文件的排版,注释。
6.3.2 安全保护
- 在配置文件里设置密码:requirepass foobar。
- 禁止某些危险命令,好比残暴的FlushDB,将它rename成”":rename-command FLUSHDB “”。
6.4 监控与维护
综述: Redis监控技巧
6.4.1 监控指令
Info指 令将返回很是丰富的信息。 着重监控检查内存使用,是否已接近上限,used_memory是Redis申请的内存,used_memory_rss是操做系统分配给Redis的物 理内存,二者之间隔着碎片,隔着Swap。 还有重点监控 AOF与RDB文件的保存状况,以及master-slave的关系。Statistic 信息还包括key命中率,全部命令的执行次数,全部client链接数量等, CONFIG RESETSTAT 可重置为0。
Monitor指令能够显示Server收到的全部指令,主要用于debug,影响性能,生产环境慎用。
SlowLog 检查慢操做(见2.性能)。
6.4.2 Trouble Shooting支持
- 日志能够动态的设置成verbose/debug模式,但不见得有更多有用的log可看,verbose还会很烦的每5秒打印当前的key状况和client状况。指令为config set loglevel verbose。
- 最爱Redis的地方是代码只有2.3万行,并且编码优美,并且huangz同窗还在原来的注释上再加上了中文注释——Redis 2.6源码中文注释版 ,因此虽然是C写的代码,虽然有十年没看过C代码,但这几天trouble shooting毫无难度,一看就懂。
- Trobule shotting的经历证实antirez处理issue的速度很是快(若是你的issue言之有物的话),比Weblogic之类的商业支持还好。
6.4.3 持久化文件维护
- 若是AOF文件在写入过程当中crash,能够用redis-check-aof修复,见5.1.2
- 若是AOF rewrite和 RDB snapshot的过程当中crash,会留下无用的临时文件,须要按期扫描删除。
6.4.4 三方工具
官网列出了以下工具,但暂时没发现会直接拿来用的:
- Redis Live,基于Python的web应用,使用Info和Monitor得到系统状况和指令统计分析。 由于Monitor指令影响性能,因此建议用cron按期运行,每次偷偷采样两分钟的样子。
- phpRedisAdmin,基于php的Web应用,目标是MysqlAdmin那样的管理工具,能够管理每一条Key的状况,但它的界面应该只适用于Key的数量不太多的状况,Demo。
- Redis Faina,基于Python的命令行,Instagram出品,用户自行得到Monitor的输出后发给它进行统计分析。因为Monitor输出的格式在Redis版本间不同,要去github下最新版。
- Redis-rdb-tools 基于Python的命令行,能够分析RDB文件每条Key对应value所占的大小,还能够将RDB dump成普通文本文件而后比较两个库是否一致,还能够将RDB输出成JSON格式,多是最有用的一个了。
- Redis Sampler,基于Ruby的命令行,antirez本身写的,统计数据分布状况。
7. Java Driver
7.1 Driver选择
各个Driver好像只有Jedis比较活跃,但也5个月没提交了,也是Java里惟一的Redis官方推荐。
Spring Data Redis的 封装并不太必要,由于Jedis已足够简单,没有像Spring Data MongoDB对MongoDB java driver的封装那样大幅简化代码,顶多就是增强了一点点点pipeline和transaction状态下的coding,禁止了一些此状态下不能用 的命令。而所谓屏蔽各类底层driver的差别并不太吸引人,由于我就没打算选其余几种driver。有兴趣的能够翻翻它的JedisConnection代码。
因此,SpringSide直接在Jedis的基础上,按Spring的风格封装了一个JedisTemplate,负责从池中获取与归还Jedis实例,处理异常。
7.2 Jedis的细节
Jedis基于Apache Commons Pool作的链接池,默认MaxActive最大链接数只有8,必须从新设置。并且MaxIdle也要相应增大,不然全部新建的链接用完即弃,而后会不停的从新链接。
另外Jedis设定了每30秒对全部链接执行一次ping,以发现失效的链接,这样每30秒会有一个拿不到链接的高峰。但效果如何须要独立分析。比 如系统高峰以后可能有一长段时间很闲,并且Redis Server那边作了Timeout控制会把链接断掉,这时候作idle checking是有意义的,但30秒一次也太过频繁了。不然关掉它更好。
Jedis的blocking pop函数,应用执行ExecutorService.shutdownNow()中断线程时并不能把它中断,见讨论组。两个解决方法:
- 不要用不限时的blocking popup,传多一个超时时间参数,如5秒。
- 找地方将调用blocking popup的jedis保存起来,shutdown时主动调用它的close。
7.3 Redis对Client端链接的处理
- Redis默认最大链接数是一万。
- Redis默认不对Client作Timeout处理,能够用timeout 项配置,但即便配了也不会很是精确。
8. Windows的版本
Windows版本方便对应用的本地开发调试,但Redis并无提供,好在微软提供了一个依赖LibUV实现兼容的补丁,https://github.com/MSOpenTech/redis,但redis做者拒绝合并到master中,微软只好苦憋的时时人工同步。 目前的稳定版是2.6版本,支持Lua脚本。
由于github如今已经没有Download服务了,因此编译好的可执行文件藏在这里:
9. 成功案例
注:下文中的连接都是网站的架构描述文档。
Twitter和新浪微博, 都属于将Redis各类数据结构用得出神入化的那种,如何发布大V如奥巴马的消息是它们最头痛的问题。
Tumblr: 11亿美刀卖给Yahoo的图片日志网站,22 台Redis server,每台运行8 – 32个实例,总共100多个Redis实例在跑。有着Redis has been completely problem free and the community is great的崇高评价。Redis在里面扮演了八爪鱼多面手的角色:
- Dashboard的海量通知的存储。
- Dashboard的二级索引。
- 存储海量短连接的HBase前面的缓存。
- Gearman Job Queue的存储。
- 正在替换另外30台memcached。
Instagram ,曾经,Redis powers their main feed, activity feed, sessions system, and other services。但惋惜目前已迁往Cassandra,说新架构只需1/4的硬件费用,是的,就是那个致使Digg CTO辞职的Canssandra。
Flickr , 依然是asynchronous task system and rudimentary queueing system。以前Task system放在mysql innodb,根本,撑不住。
The Others:
- Pinterest,混合使用MySQL、Membase与Redis做为存储。
- Youporn.com,100%的Redis,MySQL只用于建立新需求用到的sorted set,300K QPS的大压力。
- 日本微信 ,Redis在前负责异步Job Queue和O(n)的数据,且做为O(n*t)数据的cache,HBase在后,负责O(n*t)数据, n是用户,t是时间。
- StackOverflow ,2 Redis servers for distribute caching,好穷好轻量。
- Github,任务系统Resque的存储。
- Discourge,号称是为下一个十年打造的论坛系统, We use Redis for our job queue, rate limiting, as a cache and for transient data,恰好和我司的用法同样。
10. In SpringSide
extension modules项目封装了经常使用的函数与场景,showcase example的src/demo/redis目录里有各场景的benchmark测试。
10.1 Jedis Template
典型的Spring Template风格,和JdbcTemplate,HibernateTemplate同样,封装从JedisPool获取与归还Connecton的 代码,有带返回值与无返回值两种返回接口。同时,对最经常使用的Jedis调用,直接封装了一系列方法。
10.2 Scheduler与Master Elector
Scheduler实现了基于Redis的高并发单次定时任务分发。具体选型见Scheduler章节。
Master Elector基于redis setNx()与expire()两个api实现,与基于Zookeeper,Hazelcast实现的效果相似。
10.3 Showcase中的Demo
计有Session,Counter,Scheduler 与 Master Elector四款。
附录
附录1: 对redis.conf默认配置的修改
Master上
- daemonize no -> yes ,启动daemonize模式,注意若是用daemon工具启动redis-server时设回false。
- logfile stdout -> /var/log/redis/redis.log ,指定日志文件
- 注释掉RDB的全部触发规则,在Master不保存RDB文件。
- dir ./ -> /var/data/redis,指定持久化文件及临时文件目录.
- maxmemory,设置为可用内存/2.
- (可选)appendonly no->yes,打开AOF文件.
- auto-aof-rewrite-percentage 100, 综合考虑硬盘大小,可接受重启加载延时等尽可能的大,减小AOF rewrite频率.
- auto-aof-rewrite-min-size 64mb,同上,起码设为5G.
- client-output-buffer-limit slave 256mb 64mb 60. 考虑Traffic及Slave同步是RDB加载所需时间,正确设置避免buffer撑爆client被关掉后又要从新进行全同步。
- 安全配置,可选。
Slave上
- 设置RDB保存频率,由于RDB只做为Backup工具,只保留15分钟的规则,设置为15分钟保存一次就够了save 900 1。
- (可选)slaveof 设置master地址,也可动态设定。
- repl-timeout 60, 适当加大好比120,避免master实际还没倒掉就认为master倒了。