如何排查Redis的延迟问题
《如何排查Redis的延迟问题》要点: 在你使用Redis的过程中,有时可能会遇到延迟的问题,本文将会详述导致延迟的各项原因. 在本文中,延迟就是客户端哀求执行一个命令和客户端收到命令执行结果之间的最大时延.通常,Redis的处理时间几乎可以忽略不计,基本在亚微秒的范围之内.但是,某些情况可能会导致较高的延迟时间. 1. 问题检查清单以下内容非常重要,能够使得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延迟监控框架详解》. 通常,你可以通过下面的列表,在持久性和延迟/性能之间作出权衡,这个列表按照更好的数据平安性至更好的延迟性进行排序.
通过上述的检查清单,一般你只必要花费15分钟就可以排查出问题了.下文将会详述造成延迟的各种原因. 2. 测量延迟时间如果你正在遭受延迟问题的困扰,你有可能想要知道如安在你的应用程序的背景下测量延迟时间,或者你遇到的延迟问题有可能在宏观上看也是非常明显的.然而,你可以使用redis-cli客户端来测量Redis服务器的延迟时间(单位为毫秒),只需要运行以下命令: redis-cli --latency -h <host> -p <port> 3. 使用Redis内部的延迟监控子系统从Redis 2.8.13版本以来,Redis实现了延迟监控子系统,这个子系统能够对分歧的执行路径进行采样,这样便能推测服务器在何处发生阻塞.这项功能大大简化了调试本文描述的延迟问题的复杂度,因此,强烈建议尽快启用延迟监控子系统.请参考《Redis延迟监控框架详解》. 虽然你可以利用延迟监控子系统的采样和上报功能更简单地推测Redis系统产生延迟问题的原因,但是仍然建议你仔细阅读本文,这样能力更好地理解Redis延迟飙升的相关技术细节. 4. 延迟基线有一种类型的延迟是Redis的运行环境与生俱来的.通常,操作系统内核会发生这种延迟,如果你的Redis在虚拟化环境中运行,那么虚拟机管理器(hypervisor)也会发生这种延迟. 虽然这种延迟并不能被消除,但是对其展开研究却是非常重要的,因为它是Redis延迟的基线.换句话说,由于操作系统内核或虚拟机管理器的实现或配置的原因,你无论怎么对Redis延迟进行优化,都不可能低于上述的延迟基线,究竟Redis运行环境中的每个进程都会遭受这种延迟. 这种类型的延迟被称为固有延迟,从Redis 2.8.7版本以来的redis-cli客户端能够测量这种延迟.以下示例在CentOS 6.6操作系统中运行,服务器是一台入门级的虚拟机. 注意:参数100表现测试执行的时间,单位为秒.测试运行的时间越长,就越有可能发现延迟飙升.100秒通常是合适的,然而你可能想要执行几次不同时间的测试.注意,这项测试是CPU密集型的,很有可能会导致单核心系统性能饱和. Redis的固有延迟示例 注意:在这个特例中,应当在Redis的服务端主机中运行redis-cli程序,而不是在客户端主机中运行.在这个特殊模式中,redis-cli程序完全不会连接至任何Redis服务器,它只会尝试测量内核没有花费CPU时间来运行redis-cli进程自己的最大时间. 在上述示例中,Redis系统的固有延迟只有0.094毫秒(或94微秒),这是一个很好的成果!然而,请记住,固有延迟会随着时间而改变,取决于系统负载的情况. 虚拟化环境的延迟性能并不是很好,特别是在系统负载较高时,或者邻近的虚拟机抢占资源时.以下示例在一台VMware虚拟机实例中运行,它同时运行Redis服务和Apache服务: Redis在高负载下的固有延迟 上述示例的固有延迟大约为7.6毫秒,这就意味着这个Redis系统的性能差强人意.然而,在虚拟化环境中运行更多次测试时,如果使用分歧的测试时间、更高的系统负载、邻近的争抢资源的虚拟机,那么固有延迟的性能数据会更差.根据多次的测量结果,只要系统的固有延迟不大于40毫秒,Redis就能够正常提供服务. 5. 网络和通信导致的延迟客户端会通过两种方式连接至Redis服务器:TCP/IP连接或Unix域连接.通常,1 Gbit/s带宽的网络的延迟时间大约为200μs,而Unix域套接字的延迟时间可以低至30μs.延迟时间实际上取决于你的网络和系统硬件.在网络通信之上,系统也会增加一些延迟时间(线程调度、CPU缓存、NUMA结构等,都会增加延迟).在虚拟化环境中,系统导致的延迟时间明显高于在物理机中运行的系统. 这种延迟造成的结果就是,即使Redis处理大多数命令所耗费的时间在亚微秒的范围之内,当某个客户端正在和Redis服务器进行许多次来回通信时,这个客户端仍然会为这些网络和系统相关的延迟付出很多额外的性能开销. 因此,高效的客户端会尽量限制来回通信的次数,可以利用管道机制,将若干条命令一次性发送给Redis服务器.Redis服务器和大多数客户端完全支持上述的管道机制.类似于MSET/MGET的聚合命令也可以到达相似的效果.从Redis 2.4版本以来,很多命令还支持可变参数,适用于所有数据类型. 下面是一些指导方针:
在Linux系统中,有些人会优化进程布局(任务集)、cgroup、实时优先级(chrt)、NUMA配置(numactl),或者使用低延迟的系统内核,这些措施有可能获得更好的延迟性能.请注意,Redis实际上不适合绑定至单个CPU核心.Redis会调用fork操作来创建后台任务,类似于bgsave或AOF重写的操作会极大地消耗CPU性能.这些任务绝对不能和主事件循环(也便是Redis主进程)在相同的CPU核心上运行. 在大多数情况下,不必要进行这些类型的系统级优化.只有当你必要的时候才能进行优化,同时你也得非常熟悉这些优化方案. 6. Redis的单线程特性通常,Redis是以单线程模式工作的.这就意味着,Redis只会使用一个进程处理所有客户端的哀求,这种技术被称为多路复用.这就意味着,在每个给定的时刻,Redis只需要处理一个客户端哀求.因此,所有的哀求都是按照顺序依次处理的.这种设计非常类似于Node.js的工作原理.然而,这两种产品都具有非常快的运行速度.有一部分原因在于处理单个哀求的时间非常短,但主要原因还在于这两种产品被设计为不会阻塞系统调用(例如,从socket读取数据或向socket写入数据). 正如先前所述,Redis只是在大多数情况下以单线程模式工作.实际上,从Redis 2.4版本以来,Redis会使用多线程来执行某些较慢的后台I/O操作(主要是磁盘I/O操作),但是这并不能否定Redis使用单线程来处理所有客户端哀求的事实. 7. 慢速命令导致的延迟单线程模式有一个缺点,当某个哀求处理较慢时,其他所有的客户端都必须等待这个哀求处理完成.当执行普通命令(例如,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命令的用法. 8. fork导致的延迟为了在后台生成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性能,尤其对于虚拟机来说,一个大内存块的分配和初始化会带来相当可观的开销. 9. 不同系统的fork时间现代硬件拷贝页表的速度非常快,但是Xen却不行.Xen的问题并不是虚拟化特有的问题,而只是Xen特有的问题.例如,使用VMware或Virtual Box虚拟机时,调用fork()操作的速度也是非常快的.对于多个使用不同内存大小的Redis实例,以下列表会比拟它们执行fork()操作所耗费的时间.在每个Redis实例中执行BGSAVE命令就能够获得各自耗费的时间,这个时间表现为INFO命令输出信息中的latest_fork_usec字段. 然而好消息是,如果你的实例是基于新型的EC2 HVM的,那么fork()操作耗费的时间会短得多,几乎和物理机相同.因此,如果你使用m3.medium(或更好的)实例,那么就不消担心fork()操作的性能.
正如你看到的,通过Xen运行的某些虚拟机的性能有着1到2个数量级的差距.对于EC2用户来说,优化建议非常简单:使用基于新型HVM的实例就可以了. 10. 透明巨页(THP)导致的延迟不幸的是,如果Linux内核启用透明巨页,那么当Redis调用fork()操作,以便于将数据持久化至磁盘时,Redis便会遇到很年夜的延迟问题.巨页是导致下列问题的原因: (1)Redis调用 fork()操作时,会创立两个共享巨页的进程. (2)Redis的工作负载较重时,运行几次事件循环就会导致命令必要处理几千个内存页,这样便会造成Redis几乎要对整个进程内存空间执行写时拷贝(Copy On Write)操作. (3)这将会导致较年夜的延迟时间和较高的内存使用率. 使用以下命令,确保操作系统禁用透明巨页: echo never > /sys/kernel/mm/transparent_hugepage/enabled 11. 内存交换(操作系统分页)导致的延迟为了能够更高效地使用系统内存,当Linux系统(以及很多其他的当代操作系统)将数据从内存交换至磁盘时,它能够重新分配内存页,反之亦然. 如果内核将Redis的某个内存页从内存交换至swap文件,那么当Redis需要使用存放在这个内存页中的数据时(例如,拜访某个存放在这个内存页中的Key),为了将这个内存页移回内存之中,内核将会停止Redis进程的运行.上述操作的速度很慢(相比起拜访已在内存之中的内存页来说),它牵涉到随机的I/O操作,这样会导致Redis客户端遭受异常的延迟时间. 内核会重新分配寄存在磁盘上的Redis内存页,主要归结为以下三个原因:
幸运的是,Linux操作系统有一个很好的工具能够排查这个问题.因此,当Redis发生延迟问题时,最简单的办法就是检查系统是否正在进行内存交换. 首先,必要查看交换至磁盘的Redis内存总量.为了这样做,你必要获得Redis实例的PID,如下图所示: Redis的进程ID 现在,进入这个进程对应的/proc文件系统的目录,运行以下命令: cd /proc/1522 此处,你会发现一个名为smaps的文件,它会描述Redis进程的内存布局(假设你正在使用Linux 2.6.16或更新的版本).这个文件包括系统进程的内存映射有关的非常详细的信息,其中有一个名为Swap的字段,它正是我们需要观察的字段.然而,smaps文件并不是只有Swap字段,因为这个文件会包括Redis进程的各种内存映射关系(相比起简单的内存页线性数组,进程的内存布局要复杂得多). 因为我们对进程交换的所有内存都感兴趣,所以我们首先必要从smap文件抓取所有的Swap字段,如下图所示: smaps文件的swap字段 如果每一行都是0 KB,或者如果只有零星的4k条目,那么表现系统运行一切正常.实际上,在我们示例的Redis实例中(这台服务器同时运行一个网站和一个Redis实例,每秒钟为上百个用户提供服务),只有少数几个条目表现发生内存页交换.为了排查这台服务器是否有严重的问题,我们修改了先前的命令,同时输出内存映射的大小,如下图所示: smaps文件的swap和size字段 正如你可以从输出信息中看到的,最大的内存映射为1689600 kB,其中只有2000 kB被交换;第二大的内存映射为96844 kB,没有任何内存被交换:基本上只有很少量的内存被交换,因此根本不会发生任何问题. 如果有大量的进程内存被交换至磁盘,那么你遇到的延迟问题很有可能和内存交换相关.如果这是你的Redis实例产生延迟问题的原因,那么你还可以使用vmstat命令进一步验证延迟问题,如下图所示: vmstat命令的输出信息 在vmstat命令的输出信息中,我们只对si和so列感兴趣,这两列分别表现从swap文件换出和从swap文件换入的内存总量.如果你在这两列看到非零值,那么就表现你的系统发生过内存交换活动. 最后,可以使用iostat命令检查系统的全局I/O活动,如下图所示: iostat命令的输出信息 如果你的延迟问题是由于Redis内存的交换操作而导致的,那么你必要降低系统的内存压力:若Redis必要使用的内存总量多于可用的内存总量,则应当添加更多内存,或者避免在同一个系统中运行其他消耗内存较多的进程. 12. AOF和磁盘I/O导致的延迟Redis的AOF持久化功能是导致延迟问题的另一个原因.基本上,AOF持久化会使用两个系统调用来实现它的功能.此中一个是write(2)系统调用,它会将数据写入AOF文件;另一个是fdatasync(2) write(2)和fdatasync(2)系统调用都可能导致延迟问题.例如,当正在执行涉及整个系统的数据同步(sync)时,或者当输出缓冲已满,内核必要将缓冲数据刷入磁盘,以便于接收新写入的数据时,write(2)系统内调用便会阻塞系统. 相比之下,fdatasync(2)系统调用更有可能导致延迟问题.当执行这个系统调用时,很多种内核和文件系统的组合方式都会花费几毫秒至几秒才能完成,当某些其他进程正在执行I/O操作时尤其如此.为了办理这个问题,Redis会尽可能在一个不同的进程中执行fdatasync(2)系统调用. 经过多次修改AOF的相关配置,我们发现了AOF持久化是如何导致Redis发生延迟问题的. 通过appendfsync配置选项,你可以将AOF持久化配置为以三种分歧的方式调用fsync操作,将数据同步至磁盘上(你可以在运行时修改这项配置,使用CONFIG SET命令):
大多数的Redis用户会将appendfsync配置项设为no或everysec.为了将延迟降低至最低,建议避免在同一个系统中运行其他必要执行I/O操作的进程.使用SSD磁盘也可以降低延迟.但是,即便使用普通的机械磁盘,Redis的AOF持久化也可以获得较好的性能,只要确保Redis向AOF文件写入数据时,磁盘正处于空闲状态,那么就不用执行任何额外的寻道操作. 如果你想要排查AOF持久化相关的延迟问题,那么可以在Shell中运行strace命令: strace -p $(pidof redis-server) -T -e trace=fdatasync 上述命令将会显示在主线程中运行的Redis执行的所有fdatasync(2)系统调用.当appendfsync配置项被设置为everysec时,你不克不及通过上述命令查看后台线程执行的fdatasync系统调用.为了查看fdatasync系统调用,你只要为strace命令添加-f选项就可以了. 如果你想要同时查看fdatasync和write系统调用,那么可以使用以下命令: strace -p $(pidof redis-server) -T -e trace=fdatasync,write 然而,Redis还会使用write(2)系统调向客户端的socket写入数据,上述命令很有可能显示太多和磁盘I/O无关的信息.显然,没有任何办法可以告诉strace命令只显示速度较慢的系统调用,因此我会使用以下命令: strace -f -p $(pidof redis-server) -T -e trace=fdatasync,write 2>&1 | grep -v '0.0' | grep -v unfinished 13. 过期键导致的延迟Redis会通过以下两种方式回收已过期的键:
Redis的主动过期方式是自适应的.Redis会每隔100毫秒执行一次过期循环(每秒10次),每次循环都会执行以下操作:
注意,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 简而言之:请注意,如果有很多键在同一时刻过期,那么Redis就有可能产生延迟问题. 14. Redis的软件看门狗Redis 2.6版本引入一个名为Redis软件看门狗的调试工具,它可以用来跟踪上述的各种延迟问题,你不必再使用其他工具来阐发问题了. 软件看门狗是一个实验性的功能.虽然可以在生产环境中使用软件看门狗,但是在使用这项功能之前,你应当注意备份Redis数据库,因为这项功能和Redis服务器的正常操作之间可能会发生无法预料的相互作用. 只有当你通过其他所有办法都不能够跟踪延迟问题时,才能将软件看门狗作为排查问题的最后手段. 这项功能的工作流程如下所示:
注意,你无法通过redis.conf文件启用这项功能,因为它原来就被设计为只有正在运行的Redis实例才能够启用,并且只能用作调试用途. 若要启用这项功能,则在shell中运行以下命令: redis-cli config set watchdog-period 500 周期时间的单位为毫秒.在上述示例中,看门狗只会在监测到Redis服务器产生500毫秒(或更长)的延迟时,才会将这个延迟问题记录至日志文件.能够配置的最小周期时间为200毫秒. 当你用完软件看门狗之后,你可以将watchdog-period参数设置为0,这样便能关闭这项功能.重要提示:用完软件看门狗之后切记要关闭这项功能,通常长时间启用不是一个好主意. 当软件看门狗监测到Redis服务器的延迟时间超过指定的阈值时,便会在日志文件中发生相应的信息,如下图所示: 看门狗的堆栈跟踪信息 注意:在示例中,为了阻塞服务器,我们会使用DEBUG SLEEP命令.如果服务器在分歧的环境中发生阻塞,那么日志中的堆栈跟踪也就分歧. 如果你收集到多个看门狗的堆栈跟踪,那么你最好将所有这些信息都发送至Redis的谷歌讨论组:Redis的开发者得到越多的跟踪信息,排查你的Redis实例遇到的延迟问题也就越简单. 欢迎参与《如何排查Redis的延迟问题》讨论,分享您的想法,编程之家PHP学院为您提供专业教程。 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |