如何排查Redis的网络延迟大排查步骤问题

Similar Posts
Most Commented (134) (101) (99) (89) (57) (47) (44) (41) (39) (38)
Recent Posts
Recent CommentsCategories
前几天微博发生了一起大的,很多技术的朋友都比较关心,其中的原因不会超出James Hamilton在On Designing and Deploying Internet-Scale Service(1)概括的那几个范围,James第一条经验“Design for failure”是所有互联网架构成功的一个关键。互联网系统的工程理论其实非常简单,James paper中内容几乎称不上理论,而是多条实践经验分享,每个公司对这些经验的理解及执行力决定了架构成败。
题外话说完,最近又研究了。去年曾做过一个,到目前为止,这个benchmark结果依然有效。这1年我们经历了很多眼花缭乱的key value存储产品的诱惑,从Cassandra的淡出(Twitter暂停在主业务使用)到HBase的兴起(Facebook新的邮箱业务选用HBase(2)),当再回头再去看Redis,发现这个只有1万多行源代码的程序充满了神奇及大量未经挖掘的特性。Redis性能惊人,国内前十大网站的子产品估计用1台Redis就可以满足存储及Cache的需求。除了性能印象之外,业界其实普遍对Redis的认识存在一定误区。本文提出一些观点供大家探讨。
1. Redis是什么
这个问题的结果影响了我们怎么用Redis。如果你认为Redis是一个key value store, 那可能会用它来代替MySQL;如果认为它是一个可以持久化的cache, 可能只是它保存一些频繁访问的临时数据。Redis是REmote DIctionary Server的缩写,在Redis在官方网站的的副标题是A persistent key-value database with built-in net interface written in ANSI-C for Posix systems,这个定义偏向key value store。还有一些看法则认为Redis是一个memory database,因为它的高性能都是基于内存操作的基础。另外一些人则认为Redis是一个data structure server,因为Redis支持复杂的数据特性,比如List, Set等。对Redis的作用的不同解读决定了你对Redis的使用方式。
互联网数据目前基本使用两种方式来存储,关系数据库或者key value。但是这些互联网业务本身并不属于这两种数据类型,比如用户在社会化平台中的关系,它是一个list,如果要用关系数据库存储就需要转换成一种多行记录的形式,这种形式存在很多冗余数据,每一行需要存储一些重复信息。如果用key value存储则修改和删除比较麻烦,需要将全部数据读出再写入。Redis在内存中设计了各种数据类型,让业务能够高速原子的访问这些数据结构,并且不需要关心持久存储的问题,从架构上解决了前面两种存储需要走一些弯路的问题。
2. Redis不可能比Memcache快
很多开发者都认为Redis不可能比Memcached快,Memcached完全基于内存,而Redis具有持久化保存特性,即使是异步的,Redis也不可能比Memcached快。但是测试结果基本是Redis占绝对优势。一直在思考这个原因,目前想到的原因有这几方面。
。和Memcached不同,Redis并没有选择libevent。Libevent为了迎合通用性造成代码庞大(目前Redis代码还不到libevent的1/3)及牺牲了在特定平台的不少性能。Redis用libevent中两个文件修改实现了自己的epoll event loop(4)。业界不少开发者也建议Redis使用另外一个libevent高性能替代libev,但是作者还是坚持Redis应该小巧并去依赖的思路。一个印象深刻的细节是编译Redis之前并不需要执行./configure。
CAS问题。CAS是Memcached中比较方便的一种防止竞争修改资源的方法。CAS实现需要为每个cache key设置一个隐藏的cas token,cas相当value版本号,每次set会token需要递增,因此带来CPU和内存的双重开销,虽然这些开销很小,但是到单机10G+ cache以及QPS上万之后这些开销就会给双方相对带来一些细微性能差别(5)。
3. 单台Redis的存放数据必须比物理内存小
Redis的数据全部放在内存带来了高速的性能,但是也带来一些不合理之处。比如一个中型网站有100万注册用户,如果这些资料要用Redis来存储,内存的容量必须能够容纳这100万用户。但是业务实际情况是100万用户只有5万活跃用户,1周来访问过1次的也只有15万用户,因此全部100万用户的数据都放在内存有不合理之处,RAM需要为冷数据买单。
这跟操作系统非常相似,操作系统所有应用访问的数据都在内存,但是如果物理内存容纳不下新的数据,操作系统会智能将部分长期没有访问的数据交换到磁盘,为新的应用留出空间。现代操作系统给应用提供的并不是物理内存,而是虚拟内存(Virtual Memory)的概念。
基于相同的考虑,Redis 2.0也增加了VM特性。让Redis数据容量突破了物理内存的限制。并实现了数据冷热分离。
4. Redis的VM实现是重复造轮子
Redis的VM依照之前的epoll实现思路依旧是自己实现。但是在前面操作系统的介绍提到OS也可以自动帮程序实现冷热数据分离,Redis只需要OS申请一块大内存,OS会自动将热数据放入物理内存,冷数据交换到硬盘,另外一个知名的“理解了现代操作系统(3)”的Varnish就是这样实现,也取得了非常成功的效果。
作者antirez在解释为什么要自己实现VM中提到几个原因(6)。主要OS的VM换入换出是基于Page概念,比如OS VM1个Page是4K, 4K中只要还有一个元素即使只有1个字节被访问,这个页也不会被SWAP, 换入也同样道理,读到一个字节可能会换入4K无用的内存。而Redis自己实现则可以达到控制换入的粒度。另外访问操作系统SWAP内存区域时block进程,也是导致Redis要自己实现VM原因之一。
5. 用get/set方式使用Redis
作为一个key value存在,很多开发者自然的使用set/get方式来使用Redis,实际上这并不是最优化的使用方法。尤其在未启用VM情况下,Redis全部数据需要放入内存,节约内存尤其重要。
假如一个key-value单元需要最小占用512字节,即使只存一个字节也占了512字节。这时候就有一个设计模式,可以把key复用,几个key-value放入一个key中,value再作为一个set存入,这样同样512字节就会存放10-100倍的容量。
这就是为了节约内存,建议使用hashset而不是set/get的方式来使用Redis,详细方法见参考文献(7)。
6. 使用aof代替snapshot
Redis有两种存储方式,默认是snapshot方式,实现方法是定时将内存的快照(snapshot)持久化到硬盘,这种方法缺点是持久化之后如果出现crash则会丢失一段数据。因此在完美主义者的推动下作者增加了aof方式。aof即append only mode,在写入内存数据的同时将操作命令保存到日志文件,在一个并发更改上万的系统中,命令日志是一个非常庞大的数据,管理维护成本非常高,恢复重建时间会非常长,这样导致失去aof高可用性本意。另外更重要的是Redis是一个内存数据结构模型,所有的优势都是建立在对内存复杂数据结构高效的原子操作上,这样就看出aof是一个非常不协调的部分。
其实aof目的主要是数据可靠性及高可用性,在Redis中有另外一种方法来达到目的:Replication。由于Redis的高性能,复制基本没有延迟。这样达到了防止单点故障及实现了高可用。
要想成功使用一种产品,我们需要深入了解它的特性。Redis性能突出,如果能够熟练的驾驭,对国内很多大型应用具有很大帮助。希望更多同行加入到Redis使用及代码研究行列。
(Google Groups)
(Google Groups)
(Google Groups)
(Salvatore antirez Sanfilippo)
上一篇博文在新浪微博上面有更多讨论及留言,有兴趣可以去围观。(需登录)
如想及时阅读Tim Yang的文章,可通过页面右上方扫码订阅最新更新。百万级消息推送 Redis 性能排查 - 简书
百万级消息推送 Redis 性能排查
最近查看消息推送的监控信息,日推送量到百万,但是随之而来的是 TP99 搞到 5秒,详细对日志打了埋点,监控其瓶颈,最终定位是 Redis 的性能问题。
但是令人疑惑的是,对比了其他应用系统 Redis 的性能,日吞吐量亿次的 TP99 才 5~10ms,故猜测一定是应用问题导致了 Redis 的性能下降!查看了应用服务器的网络 IO,发现网络吞吐量异常的汹涌!
查看了 Redis 服务器的配置参数,发现 Redis 内存占用 7G+,Key键值对 1200万+,Reids 的使用可见一斑!但这并不能直接影响 Redis 的性能下降!
于是,我们又查看了 Redis 的 Showlog,终于发现详细,Redis 被大量的调用 SMEMBERS 命令,而每次调用几乎耗时 5s+。
SMEMBERS 命令是从一个 Set 结构获取集合,但这个集合数据量已经很大,当这个结合被频繁的调用,会极大的占用网络 IO,当网络被占满时,其余的操作也变得慢下来!同时,查看了 Redis 的连接情况,发现连接数 1000 左右,保持的比较高,又重新查看 Redis 的配置文件!
配置文件中 timeout = 0,这表明 Redis 默认不会断开客户端的连接,这造成的问题:已经无效的连接会造成网络 IO 的浪费!
这次问题排查的反思,跟之前 502 有些相似,都是隐藏极深的问题导致的,问题不再是赤裸裸的暴露出来,而是蒙上了一层面纱!本文转载自(文/Frank)Redis时延问题分析及应对
Redis的事件循环在一个线程中处理,作为一个单线程程序,重要的是要保证事件处理的时延短,这样,事件循环中的后续任务才不会阻塞; 当redis的数据量达到一定级别后(比如20G),阻塞操作对性能的影响尤为严重;
下面我们总结下在redis中有哪些耗时的场景及应对方法;
耗时长的命令造成阻塞
keys、sort等命令
keys命令用于查找所有符合给定模式 pattern 的 key,时间复杂度为O(N), N 为数据库中 key 的数量。当数据库中的个数达到千万时,这个命令会造成读写线程阻塞数秒;
类似的命令有sunion sort等操作;
如果业务需求中一定要使用keys、sort等操作怎么办?
解决方案:
在架构设计中,有&分流&一招,说的是将处理快的请求和处理慢的请求分离来开,否则,慢的影响到了快的,让快的也快不起来;这在redis的设计中体现的非常明显,redis的纯内存操作,epoll非阻塞IO事件处理,这些快的放在一个线程中搞定,而持久化,AOF重写、Master-slave同步数据这些耗时的操作就单开一个进程来处理,不要慢的影响到快的;
同样,既然需要使用keys这些耗时的操作,那么我们就将它们剥离出去,比如单开一个redis slave结点,专门用于keys、sort等耗时的操作,这些查询一般不会是线上的实时业务,查询慢点就慢点,主要是能完成任务,而对于线上的耗时快的任务没有影响;
smembers命令
smembers命令用于获取集合全集,时间复杂度为O(N),N为集合中的数量;
如果一个集合中保存了千万量级的数据,一次取回也会造成事件处理线程的长时间阻塞;
解决方案:
和sort,keys等命令不一样,smembers可能是线上实时应用场景中使用频率非常高的一个命令,这里分流一招并不适合,我们更多的需要从设计层面来考虑;
在设计时,我们可以控制集合的数量,将集合数一般保持在500个以内;
比如原来使用一个键来存储一年的记录,数据量大,我们可以使用12个键来分别保存12个月的记录,或者365个键来保存每一天的记录,将集合的规模控制在可接受的范围;
如果不容易将集合划分为多个子集合,而坚持用一个大集合来存储,那么在取集合的时候可以考虑使用SRANDMEMBER key [count];随机返回集合中的指定数量,当然,如果要遍历集合中的所有元素,这个命令就不适合了;
save命令使用事件处理线程进行数据的持久化;当数据量大的时候,会造成线程长时间阻塞(我们的生产上,reids内存中1个G保存需要12s左右),整个redis被block;
save阻塞了事件处理的线程,我们甚至无法使用redis-cli查看当前的系统状态,造成&何时保存结束,目前保存了多少&这样的信息都无从得知;
解决方案:
我没有想到需要用到save命令的场景,任何时候需要持久化的时候使用bgsave都是合理的选择(当然,这个命令也会带来问题,后面聊到);
fork产生的阻塞
在redis需要执行耗时的操作时,会新建一个进程来做,比如数据持久化bgsave:
开启RDB持久化后,当达到持久化的阈值,redis会fork一个新的进程来做持久化,采用了操作系统的copy-on-wirte写时复制策略,子进程与父进程共享Page。如果父进程的Page(每页4K)有修改,父进程自己创建那个Page的副本,不会影响到子进程;
fork新进程时,虽然可共享的数据内容不需要复制,但会复制之前进程空间的内存页表,如果内存空间有40G(考虑每个页表条目消耗 8 个字节),那么页表大小就有80M,这个复制是需要时间的,如果使用虚拟机,特别是Xen虚拟服务器,耗时会更长;
在我们有的服务器结点上测试,35G的数据bgsave瞬间会阻塞200ms以上;
类似的,以下这些操作都有进程fork;
Master向slave首次同步数据:当master结点收到slave结点来的syn同步请求,会生成一个新的进程,将内存数据dump到文件上,然后再同步到slave结点中;
AOF日志重写:使用AOF持久化方式,做AOF文件重写操作会创建新的进程做重写;(重写并不会去读已有的文件,而是直接使用内存中的数据写成归档日志);
解决方案:
为了应对大内存页表复制时带来的影响,有些可用的措施:
控制每个redis实例的最大内存量;
不让fork带来的限制太多,可以从内存量上控制fork的时延;
一般建议不超过20G,可根据自己服务器的性能来确定(内存越大,持久化的时间越长,复制页表的时间越长,对事件循环的阻塞就延长)
新浪微博给的建议是不超过20G,而我们虚机上的测试,要想保证应用毛刺不明显,可能得在10G以下;
使用大内存页,默认内存页使用4KB,这样,当使用40G的内存时,页表就有80M;而将每个内存页扩大到4M,页表就只有80K;这样复制页表几乎没有阻塞,同时也会提高快速页表缓冲TLB(translation lookaside buffer)的命中率;但大内存页也有问题,在写时复制时,只要一个页快中任何一个元素被修改,这个页块都需要复制一份(COW机制的粒度是页面),这样在写时复制期间,会耗用更多的内存空间;
使用物理机;
如果有的选,物理机当然是最佳方案,比上面都要省事;
当然,虚拟化实现也有多种,除了Xen系统外,现代的硬件大部分都可以快速的复制页表;
但公司的虚拟化一般是成套上线的,不会因为我们个别服务器的原因而变更,如果面对的只有Xen,只能想想如何用好它;
杜绝新进程的产生,不使用持久化,不在主结点上提供查询;实现起来有以下方案:
1) 只用单机,不开持久化,不挂slave结点。这样最简单,不会有新进程的产生;但这样的方案只适合缓存;
如何来做这个方案的高可用?
要做高可用,可以在写redis的前端挂上一个消息队列,在消息队列中使用pub-sub来做分发,保证每个写操作至少落到2个结点上;因为所有结点的数据相同,只需要用一个结点做持久化,这个结点对外不提供查询;
2) master-slave:在主结点上开持久化,主结点不对外提供查询,查询由slave结点提供,从结点不提供持久化;这样,所有的fork耗时的操作都在主结点上,而查询请求由slave结点提供;
这个方案的问题是主结点坏了之后如何处理?
简单的实现方案是主不具有可替代性,坏了之后,redis集群对外就只能提供读,而无法更新;待主结点启动后,再继续更新操作;对于之前的更新操作,可以用MQ缓存起来,等主结点起来之后消化掉故障期间的写请求;
如果使用官方的Sentinel将从升级为主,整体实现就相对复杂了;需要更改可用从的ip配置,将其从可查询结点中剔除,让前端的查询负载不再落在新主上;然后,才能放开sentinel的切换操作,这个前后关系需要保证;
持久化造成的阻塞
执行持久化(AOF / RDB snapshot)对系统性能有较大影响,特别是服务器结点上还有其它读写磁盘的操作时(比如,应用服务和redis服务部署在相同结点上,应用服务实时记录进出报日志);应尽可能避免在IO已经繁重的结点上开Redis持久化;
子进程持久化时,子进程的write和主进程的fsync冲突造成阻塞
在开启了AOF持久化的结点上,当子进程执行AOF重写或者RDB持久化时,出现了Redis查询卡顿甚至长时间阻塞的问题, 此时, Redis无法提供任何读写操作;
原因分析:
Redis 服务设置了 appendfsync everysec, 主进程每秒钟便会调用 fsync(), 要求内核将数据&确实&写到存储硬件里. 但由于服务器正在进行大量IO操作, 导致主进程 fsync()/操作被阻塞, 最终导致 Redis 主进程阻塞.
redis.conf中是这么说的:
When the AOF fsync policy is set to always or everysec, and a background
saving process (a background save or AOF log background rewriting) is
performing a lot of I/O against the disk, in some Linux configurations
Redis may block too long on the fsync() call. Note that there is no fix for
this currently, as even performing fsync in a different thread will block
our synchronous write(2) call.
当执行AOF重写时会有大量IO,这在某些Linux配置下会造成主进程fsync阻塞;
解决方案:
设置 no-appendfsync-on-rewrite yes, 在子进程执行AOF重写时, 主进程不调用fsync()操作;注意, 即使进程不调用 fsync(), 系统内核也会根据自己的算法在适当的时机将数据写到硬盘(Linux 默认最长不超过 30 秒).
这个设置带来的问题是当出现故障时,最长可能丢失超过30秒的数据,而不再是1秒;
子进程AOF重写时,系统的sync造成主进程的write阻塞
我们来梳理下:
1) 起因:有大量IO操作write(2) 但未主动调用同步操作
2) 造成kernel buffer中有大量脏数据
3) 系统同步时,sync的同步时间过长
4) 造成redis的写aof日志write(2)操作阻塞;
5) 造成单线程的redis的下一个事件无法处理,整个redis阻塞(redis的事件处理是在一个线程中进行,其中写aof日志的write(2)是同步阻塞模式调用,与网络的非阻塞write(2)要区分开来)
产生1)的原因:这是redis2.6.12之前的问题,AOF rewrite时一直埋头的调用write(2),由系统自己去触发sync。
另外的原因:系统IO繁忙,比如有别的应用在写盘;
解决方案:
控制系统sync调用的时间;需要同步的数据多时,耗时就长;缩小这个耗时,控制每次同步的数据量;通过配置按比例(vm.dirty_background_ratio)或按值(vm.dirty_bytes)设置sync的调用阈值;(一般设置为32M同步一次)
2.6.12以后,AOF rewrite 32M时会主动调用fdatasync;
另外,Redis当发现当前正在写的文件有在执行fdatasync(2)时,就先不调用write(2),只存在cache里,免得被block。但如果已经超过两秒都还是这个样子,则会强行执行write(2),即使redis会被block住。
AOF重写完成后合并数据时造成的阻塞
在bgrewriteaof过程中,所有新来的写入请求依然会被写入旧的AOF文件,同时放到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秒;
解决方案:
将硬盘设置的足够大,将AOF重写的阈值调高,保证高峰期间不会触发重写操作;在闲时使用crontab 调用AOF重写命令;
Posted by: 大CC | 10DEC,2015
阅读(...) 评论()如何排查Redis的延迟问题
在你使用Redis的过程中,有时可能会遇到延迟的问题,本文将会详述导致延迟的各项原因。 在本文中,延迟就是客户端请求执行一个命令和客户端收到命令执行结果之间的最大时延。通常,Redis的处理时间几乎可以忽略不计,基本在亚微秒的范围之内。但是,某些情况可能会导致较高的延迟时间。 以下内容非常重要,能够使得Redis以一种低延迟的方式运行。然而,你的工作可能非常繁忙,也许你想要从一个快速的检查清单开始解决问题。如果以下的检查步骤不能解决你的延迟问题,那么你还可以回来阅读全文。
(1)确保Redis没有执行速度较慢的命令,这些命令有可能会阻塞服务器。你可以使用Redis的慢查询日志功能排查这个问题,请参考《Redis的慢查询日志详解》。 (2)对于亚马逊EC2的用户来说,确保你使用的HVM是基于现在的EC2实例的,例如:m3.medium。否则,fork()系统调用的速度会非常慢。 (3)你的系统内核必须禁用透明巨页(THP)。你可以使用echo never & /sys/kernel/mm/transparent_hugepage/enabled命令禁用透明巨页,然后重启你的Redis进程。 (4)如果你正在使用一台虚拟机,那么可能会存在固有延迟,这种延迟与Redis毫无关系。你可以使用./redis-cli --intrinsic-latency 100命令,在你的运行时环境中检查你能够预期的最小延迟时间。注意:你需要在Redis服务端中运行这个命令,而不是在客户端中运行。 (5)你可以使用Redis的延迟监控子系统,这样便能获得易读的延迟时间和延迟原因的相关信息。请参考《Redis延迟监控框架详解》。 通常,你可以通过下面的列表,在持久性和延迟/性能之间作出权衡,这个列表按照更好的数据安全性至更好的延迟性进行排序。 AOF + 总是调用fsync
这种方案的速度非常慢,应当只有在你知道自己要做什么的时候才能使用这个方案。 AOF + 每秒调用一次fsync
这是一个很好的妥协方案。 AOF + 每秒调用一次fsync + 将no-appendfsync-on-rewrite 选项设为yes
这个方案和上面的一样好,而且还能在重写AOF文件期间避免调用fsync操作,这样能够降低磁盘的I/O压力。 AOF + 从不调用fsync
在这个方案中,fsync操作完全是由系统内核负责调用的。这个方案的磁盘压力较小,延迟飙升的风险也较小,但是数据安全性较差。 RDB
这个方案取决于你配置的保存触发器,你可以在一个巨大的范围中作出权衡。 通过上述的检查清单,一般你只需要花费15分钟就可以排查出问题了。下文将会详述造成延迟的各种原因。 如果你正在遭受延迟问题的困扰,你有可能想要知道如何在你的应用程序的背景下测量延迟时间,或者你遇到的延迟问题有可能在宏观上看也是非常明显的。然而,你可以使用redis-cli客户端来测量Redis服务器的延迟时间(单位为毫秒),只需要运行以下命令: 从Redis 2.8.13版本以来,Redis实现了延迟监控子系统,这个子系统能够对不同的执行路径进行采样,这样便能推测服务器在何处发生阻塞。这项功能大大简化了调试本文描述的延迟问题的复杂度,因此,强烈建议尽快启用延迟监控子系统。请参考《Redis延迟监控框架详解》。 虽然你可以利用延迟监控子系统的采样和上报功能更简单地推测Redis系统产生延迟问题的原因,但是仍然建议你仔细阅读本文,这样才能更好地理解Redis延迟飙升的相关技术细节。 有一种类型的延迟是Redis的运行环境与生俱来的。通常,操作系统内核会产生这种延迟,如果你的Redis在虚拟化环境中运行,那么虚拟机管理器(hypervisor)也会产生这种延迟。 虽然这种延迟并不能被消除,但是对其展开研究却是非常重要的,因为它是Redis延迟的基线。换句话说,由于操作系统内核或虚拟机管理器的实现或配置的原因,你无论怎么对Redis延迟进行优化,都不可能低于上述的延迟基线,毕竟Redis运行环境中的每个进程都会遭受这种延迟。 这种类型的延迟被称为固有延迟,从Redis 2.8.7版本以来的redis-cli客户端能够测量这种延迟。以下示例在CentOS 6.6操作系统中运行,服务器是一台入门级的虚拟机。 注意:参数100表示测试执行的时间,单位为秒。测试运行的时间越长,就越有可能发现延迟飙升。100秒通常是合适的,然而你可能想要执行几次不同时间的测试。注意,这项测试是CPU密集型的,很有可能会导致单核心系统性能饱和。
注意:在这个特例中,应当在Redis的服务端主机中运行redis-cli程序,而不是在客户端主机中运行。在这个特殊模式中,redis-cli程序完全不会连接至任何Redis服务器,它只会尝试测量内核没有花费CPU时间来运行redis-cli进程本身的最大时间。 在上述示例中,Redis系统的固有延迟只有0.094毫秒(或94微秒),这是一个很好的结果!然而,请记住,固有延迟会随着时间而改变,取决于系统负载的情况。 虚拟化环境的延迟性能并不是很好,特别是在系统负载较高时,或者邻近的虚拟机抢占资源时。以下示例在一台VMware虚拟机实例中运行,它同时运行Redis服务和Apache服务:
上述示例的固有延迟大约为7.6毫秒,这就意味着这个Redis系统的性能差强人意。然而,在虚拟化环境中运行更多次测试时,如果使用不同的测试时间、更高的系统负载、邻近的争抢资源的虚拟机,那么固有延迟的性能数据会更差。根据多次的测量结果,只要系统的固有延迟不大于40毫秒,Redis就能够正常提供服务。 客户端会通过两种方式连接至Redis服务器:TCP/IP连接或Unix域连接。通常,1 Gbit/s带宽的网络的延迟时间大约为200μs,而Unix域套接字的延迟时间可以低至30μs。延迟时间实际上取决于你的网络和系统硬件。在网络通信之上,系统也会增加一些延迟时间(线程调度、CPU缓存、NUMA布局等,都会增加延迟)。在虚拟化环境中,系统导致的延迟时间明显高于在物理机中运行的系统。 这种延迟造成的结果便是,即使Redis处理大多数命令所耗费的时间在亚微秒的范围之内,当某个客户端正在和Redis服务器进行许多次来回通信时,这个客户端仍然会为这些网络和系统相关的延迟付出很多额外的性能开销。 因此,高效的客户端会尽量限制来回通信的次数,可以利用管道机制,将若干条命令一次性发送给Redis服务器。Redis服务器和大多数客户端完全支持上述的管道机制。类似于MSET/MGET的聚合命令也可以达到相似的效果。从Redis 2.4版本以来,很多命令还支持可变参数,适用于所有数据类型。 下面是一些指导方针: 如果你可以接受较高的成本,那么尽量使用物理机来部署Redis服务器,而不是虚拟机。 不要有计划地连接/断开Redis服务器(对于WEB应用程序尤其要注意)。尽可能长时间地保持连接不要断开。 如果你的客户端和Redis服务器在相同的宿主机上,那么请使用Unix域套接字。 如果可以的话,请尽量使用聚合命令(MSET/MGET),或者使用支持可变参数的命令,而不是使用管道机制。 如果可以的话,请尽量使用管道机制,而不是需要来回通信的命令序列。 Redis服务器支持Lua脚本编程,在不支持原生管道机制的应用场景中,你可以使用Lua脚本(例如,当某条命令的执行结果是后续命令的一个输入参数时)。 在Linux系统中,有些人会优化进程布局(任务集)、cgroup、实时优先级(chrt)、NUMA配置(numactl),或者使用低延迟的系统内核,这些措施有可能获得更好的延迟性能。请注意,Redis实际上不适合绑定至单个CPU核心。Redis会调用fork操作来创建后台任务,类似于bgsave或AOF重写的操作会极大地消耗CPU性能。这些任务绝对不能和主事件循环(也就是Redis主进程)在相同的CPU核心上运行。 在大多数情况下,不需要进行这些类型的系统级优化。只有当你需要的时候才能进行优化,同时你也得非常熟悉这些优化方案。 通常,Redis是以单线程模式工作的。这就意味着,Redis只会使用一个进程处理所有客户端的请求,这种技术被称为多路复用。这就意味着,在每个给定的时刻,Redis只需要处理一个客户端请求。因此,所有的请求都是按照顺序依次处理的。这种设计非常类似于Node.js的工作原理。然而,这两种产品都具有非常快的运行速度。有一部分原因在于处理单个请求的时间非常短,但主要原因还在于这两种产品被设计为不会阻塞系统调用(例如,从socket读取数据或向socket写入数据)。 正如先前所述,Redis只是在大多数情况下以单线程模式工作。实际上,从Redis 2.4版本以来,Redis会使用多线程来执行某些较慢的后台I/O操作(主要是磁盘I/O操作),但是这并不能否定Redis使用单线程来处理所有客户端请求的事实。 单线程模式有一个缺点,当某个请求处理较慢时,其他所有的客户端都必须等待这个请求处理完成。当执行普通命令(例如,GET、SET或LPUSH命令)时,这个缺点一点都不成问题,因为这些命令的执行时间都是常数,并且耗时非常短。然而,某些命令(例如,SORT、LREM或SUNION命令)会操作很多元素。例如,如果Redis要获得两个大集合的交集,那么耗费的时间会非常可观。 Redis的官方文档列出了所有命令的算法复杂度。如果你要使用某些不熟悉的命令,那么在使用之前,最好了解一下这些命令的技术细节。 如果你对延迟时间心存疑虑,那么你不应当使用速度较慢的命令来操作由很多元素构成的对象,你也不应当在运行较慢的查询操作时通过Redis复制功能进行数据备份。 通过Redis的慢查询日志功能,你可以监控运行速度较慢的命令。请参考《 Redis的慢查询日志详解》。 另外,你可以使用你喜欢的监控程序(top、htop、prstat等等)来快速检查Redis主进程的CPU消耗程度。如果CPU消耗较高,而网络流量却较低,那么通常Redis很有可能正在执行速度较慢的命令。 重要提示:在生产环境中,执行KEYS命令是造成延迟的一个常见原因,这个命令的运行速度很慢。正如Redis官方文档所述,KEYS命令只应当在调试程序时使用。从Redis 2.8版本以来,Redis引入了一种新的命令类型,这些命令能够增量迭代键空间和其他的大型集合。请参考SCAN、SSCAN、HSCAN和ZSCAN命令的用法。 为了在后台生成RDB文件,或者重写AOF文件(如果启用AOF持久化功能的话),Redis必须调用fork()操作来创建后台进程。fork()操作(在主线程中运行)本身就会产生延迟。 在类Unix系统中,fork()操作的性能开销比较大,因为它需要拷贝很多链接至进程的对象。对于虚拟内存机制相关的页表来说,这种性能开销显得尤为明显。 例如,在Linux/AMD64系统中,内存会被划分为4 kB大小的页。为了将虚拟地址转换为物理地址,每个进程都会存储一个页表(实际的表现形式为一棵树),这个页表至少会包含进程地址空间的每个内存页的一个指针。因此,如果某个Redis实例需要使用24 GB的内存,那么它需要24 GB / 4 kB * 8 = 48 MB的内存空间来存储相应的页表。 当某个Redis实例执行后台保存操作(BGSAVE)时,这个实例将会调用fork()操作,这项操作需要分配和拷贝48 MB的内存。fork()操作会耗费时间和CPU性能,尤其对于虚拟机来说,一个大内存块的分配和初始化会带来相当可观的开销。 现代硬件拷贝页表的速度非常快,但是Xen却不行。Xen的问题并不是虚拟化特有的问题,而只是Xen特有的问题。例如,使用VMware或Virtual Box虚拟机时,调用fork()操作的速度也是非常快的。对于多个使用不同内存大小的Redis实例,以下列表会比较它们执行fork()操作所耗费的时间。在每个Redis实例中执行BGSAVE命令就能够获得各自耗费的时间,这个时间表现为INFO命令输出信息中的latest_fork_usec字段。 然而好消息是,如果你的实例是基于新型的EC2 HVM的,那么fork()操作耗费的时间会短得多,几乎和物理机相同。因此,如果你使用m3.medium(或更好的)实例,那么就不用担心fork()操作的性能。 Linux操作系统 + VMware虚拟机 + 6.0 GB的RSS内存:fork()操作耗费77毫秒(每GB耗费12.8毫秒)。 Linux操作系统 + 物理机(未知硬件) + 6.1 GB的RSS内存:fork()操作耗费80毫秒(每GB耗费13.1毫秒)。 Linux操作系统 + 物理机(Xeon@2.27GHz) + 6.9 GB的RSS内存:fork()操作耗费62毫秒(每GB耗费9毫秒)。 Linux操作系统 + 6sync虚拟机(KVM)+ 360 MB的RSS内存:fork()操作耗费8.2毫秒(每GB耗费23.3毫秒)。 Linux操作系统 + EC2虚拟机 + 旧实例类型(Xen)+ 6.1 GB的RSS内存:fork()操作耗费1460毫秒(每GB耗费239.3毫秒)。 Linux操作系统 + EC2虚拟机 + 新实例类型(Xen)+ 1 GB的RSS内存:fork()操作耗费10毫秒(每GB耗费10毫秒)。 Linux操作系统 + Linode实例(Xen)+ 0.9 GB的RSS内存:fork()操作耗费382毫秒(每GB耗费424毫秒)。 正如你看到的,通过Xen运行的某些虚拟机的性能有着1到2个数量级的差距。对于EC2用户来说,优化建议非常简单:使用基于新型HVM的实例就可以了。 不幸的是,如果Linux内核启用透明巨页,那么当Redis调用fork()操作,以便于将数据持久化至磁盘时,Redis便会遇到很大的延迟问题。巨页是导致下列问题的原因: (1)Redis调用 fork()操作时,会创建两个共享巨页的进程。
(2)Redis的工作负载较重时,运行几次事件循环就会导致命令需要处理几千个内存页,这样便会造成Redis几乎要对整个进程内存空间执行写时拷贝(Copy On Write)操作。
(3)这将会导致较大的延迟时间和较高的内存使用率。 使用以下命令,确保操作系统禁用透明巨页: 为了能够更高效地使用系统内存,当Linux系统(以及很多其他的现代操作系统)将数据从内存交换至磁盘时,它能够重新分配内存页,反之亦然。 如果内核将Redis的某个内存页从内存交换至swap文件,那么当Redis需要使用存放在这个内存页中的数据时(例如,访问某个存放在这个内存页中的Key),为了将这个内存页移回内存之中,内核将会停止Redis进程的运行。上述操作的速度很慢(相比起访问已在内存之中的内存页来说),它牵涉到随机的I/O操作,这样会导致Redis客户端遭受异常的延迟时间。 内核会重新分配存放在磁盘上的Redis内存页,主要归结为以下三个原因: 如果正在运行的进程请求更多的物理内存,而可用的内存总量却不能满足需求,那么系统便会遭受很大的内存压力。这个问题的最简单的例子就是Redis实际需要使用的内存总量多于服务器的可用内存总量。 如果Redis实例的数据集,或者数据集的一部分,在大多数时候是完全空闲的(客户端从未访问),那么内核会将空闲的内存页交换至磁盘。这种问题非常少见,因为即使某个Redis具有中等水平的运行速度,它也会经常访问所有的内存页,这就会强迫Redis将所有的内存页保留在内存之中。 在系统中,某些进程会产生大量的读取或写入的I/O操作。由于系统通常会缓存文件,这就很有可能迫使内核增加文件系统的缓存,因此就会产生内存交换活动。注意,Redis的RDB和/或AOF后台线程也存在上述行为,这两种持久化机制都会产生大型文件。 幸运的是,Linux操作系统有一个很好的工具能够排查这个问题。因此,当Redis发生延迟问题时,最简单的方法就是检查系统是否正在进行内存交换。 首先,需要查看交换至磁盘的Redis内存总量。为了这样做,你需要获得Redis实例的PID,如下图所示:
现在,进入这个进程对应的/proc文件系统的目录,运行以下命令: 此处,你会发现一个名为smaps的文件,它会描述Redis进程的内存布局(假设你正在使用Linux 2.6.16或更新的版本)。这个文件包含系统进程的内存映射有关的非常详细的信息,其中有一个名为Swap的字段,它正是我们需要观察的字段。然而,smaps文件并不是只有Swap字段,因为这个文件会包含Redis进程的各种内存映射关系(相比起简单的内存页线性数组,进程的内存布局要复杂得多)。 因为我们对进程交换的所有内存都感兴趣,所以我们首先需要从smap文件抓取所有的Swap字段,如下图所示:
如果每一行都是0 KB,或者如果只有零星的4k条目,那么表示系统运行一切正常。实际上,在我们示例的Redis实例中(这台服务器同时运行一个网站和一个Redis实例,每秒钟为上百个用户提供服务),只有少数几个条目表示发生内存页交换。为了排查这台服务器是否有严重的问题,我们修改了先前的命令,同时输出内存映射的大小,如下图所示:
正如你可以从输出信息中看到的,最大的内存映射为1689600 kB,其中只有2000 kB被交换;第二大的内存映射为96844 kB,没有任何内存被交换:基本上只有很少量的内存被交换,因此根本不会产生任何问题。 如果有大量的进程内存被交换至磁盘,那么你遇到的延迟问题很有可能和内存交换相关。如果这是你的Redis实例发生延迟问题的原因,那么你还可以使用vmstat命令进一步验证延迟问题,如下图所示:
在vmstat命令的输出信息中,我们只对si和so列感兴趣,这两列分别表示从swap文件换出和从swap文件换入的内存总量。如果你在这两列看到非零值,那么就表示你的系统发生过内存交换活动。 最后,可以使用iostat命令检查系统的全局I/O活动,如下图所示:
如果你的延迟问题是由于Redis内存的交换操作而导致的,那么你需要降低系统的内存压力:若Redis需要使用的内存总量多于可用的内存总量,则应当添加更多内存,或者避免在同一个系统中运行其他消耗内存较多的进程。 Redis的AOF持久化功能是导致延迟问题的另一个原因。基本上,AOF持久化会使用两个系统调用来实现它的功能。其中一个是write(2)系统调用,它会将数据写入AOF文件;另一个是fdatasync(2)系统调用,它会将内核文件缓冲的数据刷入磁盘,这样便能确保用户指定的持久性等级。 write(2)和fdatasync(2)系统调用都可能导致延迟问题。例如,当正在执行涉及整个系统的数据同步(sync)时,或者当输出缓冲已满,内核需要将缓冲数据刷入磁盘,以便于接收新写入的数据时,write(2)系统内调用便会阻塞系统。 相比之下,fdatasync(2)系统调用更有可能导致延迟问题。当执行这个系统调用时,很多种内核和文件系统的组合方式都会花费几毫秒至几秒才能完成,当某些其他进程正在执行I/O操作时尤其如此。为了解决这个问题,从Redis 2.4版本以来,Redis会尽可能在一个不同的进程中执行fdatasync(2)系统调用。 经过多次修改AOF的相关配置,我们发现了AOF持久化是如何导致Redis产生延迟问题的。 通过appendfsync配置选项,你可以将AOF持久化配置为以三种不同的方式调用fsync操作,将数据同步至磁盘上(你可以在运行时修改这项配置,使用CONFIG SET命令): 当appendfsync被设置为no时,Redis就不会执行fsync操作。在这种配置中,导致延迟问题的原因只有write(2)操作。当这种情况经常发生时,因为磁盘不能应对Redis接收数据时的速率问题,所以并没有什么解决方法。然而,如果磁盘没有被其他执行I/O操作的进程拖慢速率,那么这种情况并不会经常发生。 当appendfsync被设置为everysec时,Redis会每秒钟执行一次fsync操作。Redis会使用一个不同的线程,如果正在执行fsync操作,那么Redis会使用缓冲来延迟write(2)操作的调用,最多延迟2秒钟(在Linux操作系统中,如果正在执行的fsync操作和write操作会处理相同的文件,那么write操作很有可能会发生阻塞)。然而,如果fsync操作消耗的时间过长,那么Redis最终还是会执行write(2)调用(即使fsync操作仍然未执行完成),这很有可能是导致延迟问题的原因。 当appendfsync被设置为always时,Redis会在每次写入操作完成之后和向客户端回复OK结果代码之前,执行一次fsync操作(实际上,Redis会尝试将很多命令聚集在同一时刻执行,这样便只需要为这些命令执行一次fsync操作就可以了)。在这种模式中,Redis的性能通常会很慢,强烈建议使用高速磁盘(例如:SSD)和高速文件系统,它们可以在短时间内执行完成fsync操作。 大多数的Redis用户会将appendfsync配置项设为no或everysec。为了将延迟降低至最低,建议避免在同一个系统中运行其他需要执行I/O操作的进程。使用SSD磁盘也可以降低延迟。但是,即便使用普通的机械磁盘,Redis的AOF持久化也可以获得较好的性能,只要确保Redis向AOF文件写入数据时,磁盘正处于空闲状态,那么就不用执行任何额外的寻道操作。 如果你想要排查AOF持久化相关的延迟问题,那么可以在Shell中运行strace命令: 上述命令将会显示在主线程中运行的Redis执行的所有fdatasync(2)系统调用。当appendfsync配置项被设置为everysec时,你不能通过上述命令查看后台线程执行的fdatasync系统调用。为了查看fdatasync系统调用,你只要为strace命令添加-f选项就可以了。 如果你想要同时查看fdatasync和write系统调用,那么可以使用以下命令: 然而,Redis还会使用write(2)系统调向客户端的socket写入数据,上述命令很有可能显示太多和磁盘I/O无关的信息。显然,没有任何方法可以告诉strace命令只显示速度较慢的系统调用,因此我会使用以下命令: Redis会通过以下两种方式回收已过期的键: 被动方式:当某个命令请求一个已过期的键时,会发现这个键已经过期了,然后便会执行回收操作。 主动方式:每隔100毫秒执行一次回收操作。 Redis的主动过期方式是自适应的。Redis会每隔100毫秒执行一次过期循环(每秒10次),每次循环都会执行以下操作: 采样ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP个键,回收所有已经过期的键。 如果发现超过25%的键已经过期了,那么重复上述操作。 注意,ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP的值是在Redis源码的server.h文件中定义的。ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP的默认值为20,Redis每秒钟执行10次过期循环,通常每秒钟只有200个键会主动过期。即使很长时间都没有访问已经过期的键,主动方式仍然能够快速地清理Redis数据库,因此被动方式的算法并没有任何作用。同时,每秒钟仅仅回收200个已经过期的键并不会导致Redis实例发生延迟问题。 然而,主动方式的算法是自适应的,如果它发现键的采样集合之内有超过25%的键已经过期了,那么便会重复执行。但是,假设这个算法每秒钟运行10次,这就意味着随机的采样集合之内有超过25%的键几乎在同一时刻过期,这并不是一个好现象。 换句话说,如果Redis有很多很多的键几乎在同一时刻过期,并且这些键在使用SETEX命令设置的所有键之中的比率至少达到25%,那么为了使得上述比率降低至小于25%,Redis便有可能发生阻塞。 为了避免已经过期的键占用太多的内存空间,Redis有必要使用上述的主动方式。通常,这种方式不会产生任何问题,虽然大量的键在同一时刻过期是一个很奇怪的现象,但是用户在使用EXPIREAT命令时不大可能大范围地使用相同的Unix时间戳。 简而言之:请注意,如果有很多键在同一时刻过期,那么Redis就有可能发生延迟问题。 Redis 2.6版本引入一个名为Redis软件看门狗的调试工具,它可以用来跟踪上述的各种延迟问题,你不必再使用其他工具来分析问题了。 软件看门狗是一个实验性的功能。虽然可以在生产环境中使用软件看门狗,但是在使用这项功能之前,你应当注意备份Redis数据库,因为这项功能和Redis服务器的正常操作之间可能会产生无法预料的相互作用。 只有当你通过其他所有方法都不能够跟踪延迟问题时,才能将软件看门狗作为排查问题的最后手段。 这项功能的工作流程如下所示: 用户通过CONFIG SET命令启用软件看门狗功能。 Redis不断地监控自身。 如果Redis发现服务器由于某个操作未能快速返回而导致阻塞,这个操作很有可能就是延迟问题的原因,软件看门狗便会在日志文件中生成关于服务器阻塞原因的底层报告。 在Redis的谷歌讨论组中,用户可以向Redis的开发者发送求助信息,包含看门狗生成的延迟报告。 注意,你无法通过redis.conf文件启用这项功能,因为它本来就被设计为只有正在运行的Redis实例才能够启用,并且只能用作调试用途。 若要启用这项功能,则在shell中运行以下命令: 周期时间的单位为毫秒。在上述示例中,看门狗只会在监测到Redis服务器发生500毫秒(或更长)的延迟时,才会将这个延迟问题记录至日志文件。能够配置的最小周期时间为200毫秒。 当你用完软件看门狗之后,你可以将watchdog-period参数设置为0,这样便能关闭这项功能。重要提示:用完软件看门狗之后切记要关闭这项功能,通常长时间启用不是一个好主意。 当软件看门狗监测到Redis服务器的延迟时间超过指定的阈值时,便会在日志文件中产生相应的信息,如下图所示:
注意:在示例中,为了阻塞服务器,我们会使用DEBUG SLEEP命令。如果服务器在不同的环境中发生阻塞,那么日志中的堆栈跟踪也就不同。 如果你收集到多个看门狗的堆栈跟踪,那么你最好将所有这些信息都发送至Redis的谷歌讨论组:Redis的开发者获得越多的跟踪信息,排查你的Redis实例遇到的延迟问题也就越简单。

我要回帖

更多关于 网络延迟大排查步骤 的文章

 

随机推荐