找回密码
 骑士注册

QQ登录

微博登录

搜索
❏ 站外平台:

Linux中国开源社区 技术 查看内容

江南白衣的“关于Redis的常识”

2013-06-30 22:41    收藏: 1    

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字节的数据。
    redis-benchmark -t SET -c 100 -n 10000000 -r 10000000 -d 256 
    
  • 测试结果(TPS): 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 为什么快

  • 纯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毫秒是件大事情。
  • 持久化对性能的影响很大,见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,各种系统的对比,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。
  • 脚本的将会在命令行按顺序传入如下参数: <role(leader/observer)> <state(上述三种情况)>
  • 脚本返回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

查看其它分页:

最新评论

我也要发表评论

收藏

返回顶部

分享到微信

打开微信,点击顶部的“╋”,
使用“扫一扫”将网页分享至微信。