本文将有助于你找出Redis 响应延迟的问题所在。 文中出现的延迟(latency)均指从客户端发出一条命令到客户端接受到该命令的反馈所用的最长响应时间。Reids通常处理(命令的)时间非常的慢,大概在次微妙范围内,但也有更长的情况出现。 |
计算延迟时间 如果你正在经历响应延迟问题,你或许能够根据应用程序的具体情况算出它的延迟响应时间,或者你的延迟问题非常明显,宏观看来,一目了然。不管怎样吧,用redis-cli可以算出一台Redis 服务器的到底延迟了多少毫秒。踹这句: redis-cli --latency -h `host` -p `port`
|
网络和通信引起的延迟 当用户连接到Redis通过TCP/IP连接或Unix域连接,千兆网络的典型延迟大概200us,而Unix域socket可能低到30us。这完全基于你的网络和系统硬件。在通信本身之上,系统增加了更多的延迟(线程调度,CPU缓存,NUMA替换等等)。系统引起的延迟在虚拟机环境远远高于在物理机器环境。 |
实际情况是即使Redis处理大多数命令在微秒之下,客户机和服务器之间的交互也必然消耗系统相关的延迟。 一个高效的客户机因而试图通过捆绑多个命令在一起的方式减少交互的次数。服务器和大多数客户机支持这种方式。聚合命令象MSET/MGET也可以用作这个目的。从Redis 2.4版本起,很多命令对于所有的数据类型也支持可变参数。 |
这里有一些指导:
在Linux上,你可以通过process placement(taskset)、cgroups、real-time priorities(chrt)、NUMA配置(numactl)或使用低延迟内核的方式来获取较低的延迟。请注意Redis 并不适合被绑到单个CPU核上。redis会在后台创建一些非常消耗CPU的进程,如bgsave和AOF重写,这些任务是绝对不能和主事件循环进程放在一个CPU核上的。 大多数情况下上述的优化方法是不需要的,除非你确实需要并且你对优化方法很熟悉的情况下再使用上述方法。 |
! |
Redis的单线程属性Redis 使用了单线程的设计, 意味着单线程服务于所有的客户端请求,使用一种复用的技术。这种情况下redis可以在任何时候处理单个请求, 所以所有的请求是顺序处理的。这和Node.js的工作方式很像, 所有的产出通常不会有慢的感觉,因为处理单个请求的时间非常短,但是最重要的是这些产品被设计为非阻塞系统调用,比如从套接字中读取或写入数据。 我提到过Redis从2.4版本后几乎是单线程的,我们使用线程在后台运行一些效率低下的I/O操作, 主要关系到硬盘I/O,但是这不改变Redis使用单线程处理所有请求的事实。 |
! |
低效操作产生的延迟单线程的一个结果是,当一个请求执行得很慢,其他的客户端调用就必须等待这个请求执行完毕。当执行GET、SET或者LPUSH 命令的时候这不是个问题,因为这些操作可在很短的常数时间内完成。然而,对于多个元素的操作,像SORT,LREM, SUNION 这些,做两个大数据集的交叉要花掉很长的时间。 文档中提到了所有操作的算法复杂性。 在使用一个你不熟悉的命令之前系统的检查它会是一个好办法。 |
如果你对延迟有要求,那么就不要执行涉及多个元素的慢操作,你可以使用Redis的replication功能,把这类慢操作全都放到replica上执行。 可以用Redis 的Slow Log 来监控慢操作。 此外,你可以用你喜欢的进程监控程序(top, htop, prstat, 等...)来快速查看Redis进程的CPU使用率。如果traffic不高而CPU占用很高,八成说明有慢操作。 |
延迟由fork产生Redis不论是为了在后台生成一个RDB文件,还是为了当AOF持久化方案被开启时重写Append Only文件,都会在后台fork出一个进程。fork操作(在主线程中被执行)本身会引发延迟。在大多数的类unix操作系统中,fork是一个很消耗的操作,因为它牵涉到复制很多与进程相关的对象。而这对于分页表与虚拟内存机制关联的系统尤为明显 |
对于运行在一个linux/AMD64系统上的实例来说,内存会按照每页4KB的大小分页。为了实现虚拟地址到物理地址的转换,每一个进程将会存储一个分页表(树状形式表现),分页表将至少包含一个指向该进程地址空间的指针。所以一个空间大小为24GB的redis实例,需要的分页表大小为 24GB/4KB*8 = 48MB。 当一个后台的save命令执行时,实例会启动新的线程去申请和拷贝48MB的内存空间。这将消耗一些时间和CPU资源,尤其是在虚拟机上申请和初始化大块内存空间时,消耗更加明显。 |
在不同系统中的Fork时间除了Xen系统外,现代的硬件都可以快速完美的复制页表。Xen系统的问题不是特定的虚拟化,而是特定的Xen.例如使用VMware或者Virutal Box不会导致较慢的fork时间。下面的列表比较了不同Redis实例的fork时间。数据包含正在执行的BGSAVE,并通过INFO指令查看thelatest_fork_usecfiled。
|
swapping (操作系统分页)引起的延迟Linux (以及其他一些操作系统) 可以把内存页存储在硬盘上,反之也能将存储在硬盘上的内存页再加载进内存,这种机制使得内存能够得到更有效的利用。 如果内存页被系统移到了swap文件里,而这个内存页中的数据恰好又被redis用到了(例如要访问某个存储在内存页中的key),系统就会暂停redis进程直到把需要的页数据重新加载进内存。这个操作因为牵涉到随机I/O,所以很慢,会导致无法预料的延迟 |
|
系统之所以要在内存和硬盘之间置换redis页数据主要因为以下三个原因:
所幸Linux提供了很好的工具来诊断这个问题,所以当延迟疑似是swap引起的,最简单的办法就是使用Linux提供的工具去确诊。 |
首先要做的是检查swap到硬盘上的redis内存的数量,为实现这个目的要知道redis实例的进程id: $ redis-cli info | grep process_id process_id:5454 进入进程目录: $ cd /proc/5454 在这里你会发现一个名为smaps 的文件,它描述了redis进程的内存布局 (假定你使用的是Linux 2.6.16或者更新的版本)。这个文件包括了很多进程所使用内存的细节信息,其中有一项叫做Swap的正是我们所关心的。不过仅看这一项是不够的,因为smaps文件包括有redis进程的多个不同的的内存映射区域的使用情况(进程的内存布局远不是线性排列那么简单)。 |
从我们对所有进程的内存交换情况感兴趣以来,我们首先要做的事情是使用grep命令显示进程的smaps文件 $ cat smaps | grep 'Swap:' Swap: 0 kB Swap: 0 kB Swap: 0 kB Swap: 0 kB Swap: 0 kB Swap: 12 kB Swap: 156 kB Swap: 8 kB Swap: 0 kB Swap: 0 kB Swap: 0 kB Swap: 0 kB Swap: 0 kB Swap: 0 kB Swap: 0 kB Swap: 0 kB Swap: 0 kB Swap: 4 kB Swap: 0 kB Swap: 0 kB Swap: 4 kB Swap: 0 kB Swap: 0 kB Swap: 4 kB Swap: 4 kB Swap: 0 kB Swap: 0 kB Swap: 0 kB Swap: 0 kB Swap: 0 kB假如所有的数据显示为0kb或者某些数据偶尔显示为4kb,表示当前一切正常。实际上我们的例子是一个真实的运行着Redis并每秒为数百的用户提供服务的网站,会显示更多的交换页。为了研究是否存在一个严重的问题,我们改变命令打印出分配的内存尺寸 $ cat smaps | egrep '^(Swap|Size)' Size: 316 kB Swap: 0 kB Size: 4 kB Swap: 0 kB Size: 8 kB Swap: 0 kB Size: 40 kB Swap: 0 kB Size: 132 kB Swap: 0 kB Size: 720896 kB Swap: 12 kB Size: 4096 kB Swap: 156 kB Size: 4096 kB Swap: 8 kB Size: 4096 kB Swap: 0 kB Size: 4 kB Swap: 0 kB Size: 1272 kB Swap: 0 kB Size: 8 kB Swap: 0 kB Size: 4 kB Swap: 0 kB Size: 16 kB Swap: 0 kB Size: 84 kB Swap: 0 kB Size: 4 kB Swap: 0 kB Size: 4 kB Swap: 0 kB Size: 8 kB Swap: 4 kB Size: 8 kB Swap: 0 kB Size: 4 kB Swap: 0 kB Size: 4 kB Swap: 4 kB Size: 144 kB Swap: 0 kB Size: 4 kB Swap: 0 kB Size: 4 kB Swap: 4 kB Size: 12 kB Swap: 4 kB Size: 108 kB Swap: 0 kB Size: 4 kB Swap: 0 kB Size: 4 kB Swap: 0 kB Size: 272 kB Swap: 0 kB Size: 4 kB Swap: 0 kB在输出信息中,你能看到有一个720896kb的内存分配(有12kb的交换)还有一个156kb的交换是另一个进程的。基本上我们的内存只会有很小的内存交换,因此不会产生任何的问题 |
假如进程的内存有相当部分花在了swap上,那么你的延迟可能就与swap有关。假如redis出现这种情况那么可以用vmstat 命令来验证一下猜测: $ vmstat 1 procs -----------memory---------- ---swap-- -----io---- -system-- ----cpu---- r b swpd free buff cache si so bi bo in cs us sy id wa 0 0 3980 697932 147180 1406456 0 0 2 2 2 0 4 4 91 0 0 0 3980 697428 147180 1406580 0 0 0 0 19088 16104 9 6 84 0 0 0 3980 697296 147180 1406616 0 0 0 28 18936 16193 7 6 87 0 0 0 3980 697048 147180 1406640 0 0 0 0 18613 15987 6 6 88 0 2 0 3980 696924 147180 1406656 0 0 0 0 18744 16299 6 5 88 0 0 0 3980 697048 147180 1406688 0 0 0 4 18520 15974 6 6 88 0 输出中我们最感兴趣的两行是si 和 so,这两行分别统计了从swap文件恢复到内存的数量和swap到文件的内存数量。如果在这两行发现了非0值那么就说明系统正在进行swap。 |
最后,可以用iostat命令来查看系统的全局I/O行为。 $ iostat -xk 1 avg-cpu: %user %nice %system %iowait %steal %idle 13.55 0.04 2.92 0.53 0.00 82.95 Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await svctm %util sda 0.77 0.00 0.01 0.00 0.40 0.00 73.65 0.00 3.62 2.58 0.00 sdb 1.27 4.75 0.82 3.54 38.00 32.32 32.19 0.11 24.80 4.24 1.85 如果确认延迟是由于swap引起的,那么就需要减小系统的内存压力,要么给机器增加内存,要么不要在同一个机器上运行其他消耗内存的程序。 |
AOF 和硬盘I/O操作延迟另一个延迟的根源是Redis的AOF(仅附加文件)模式。AOF基本上是通过两个系统间的调用来完成工作的。 一个是写,用来写数据到AOF, 另外一个是文件数据同步,通过清除硬盘上空核心文件的缓冲来保证用户指定的持久级别。 包括写和文件数据同步的调用都可以导致延迟的根源。 写实例可以阻塞系统范围的同步操作,也可以阻塞当输出的缓冲区满并且内核需要清空到硬盘来接受新的写入的操作。 |
文件数据同步对于延迟的影响非常大,因为它涉及到好几步调用,可能要花掉几毫秒以致几秒的时间,特别是在还有其他进程后也在占用I/O的情况下。因为这个原因,从redis2.4开始用一个单独的线程来做文件数据同步。 我们来看看当使用AOF的时候如何配置来降低延迟。 |
通过设置AOF相关的appendfsync项,可以使用三种不同的方式来执行文件同步(也可以在运行时使用CONFIG SET 命令来修改这个配置)。
|
|
大多数redis用户都会把这个值设成 no 或者 everysec。要减少延迟,最好避免在同一个机器上有其他耗费I/O的程序。用SSD也有益于降低延迟,不过即使不使用SSD,如果能有冗余的硬盘专用于AOF也会减少寻址时间,从而降低延迟。 如果你想诊断AOF相关的延迟原因可以使用strace 命令: sudo strace -p $(pidof redis-server) -T -e trace=fdatasync |
上面的命令会展示redis主线程里所有的fdatasync系统调用。不包括后台线程执行的fdatasync 调用。如果appendfsync 配置为everysec,则给strace增加-f选项。 用下面命令可以看到fdatasync和write调用: sudo strace -p $(pidof redis-server) -T -e trace=fdatasync,write 不过因为write也会向客户端写数据,所以用上面的命令很可能会获得许多与磁盘I/O没有关系的结果。似乎没有办法让strace 只显示慢系统调用,所以要用下面的命令: sudo strace -f -p $(pidof redis-server) -T -e trace=fdatasync,write 2>&1 | grep -v '0.0' | grep -v unfinished |
数据过期造成的延迟 redis有两种方式来去除过期的key:
active过期模式是自适应的,每过100毫秒开始一次过期检查(每秒10次),每次作如下操作:
REDIS_EXPIRELOOKUPS_PER_CRON 默认为10, 过期检查一秒会执行10次,通常在actively模式下1秒能处理100个key。在过期的key有一段时间没被访问的情况下这个清理速度已经足够了,所以 lazy模式基本上没什么用。1秒只过期100个key也不会对redis造成多大的影响。 |
这种算法式是自适应的,如果发现有超过指定数量25%的key已经过期就会循环执行。这个过程每秒会运行10次,这意味着随机样本中超过25%的key会在1秒内过期。 通常来讲如果有很多key在同一秒过期,超过了所有key的25%,redis就会阻塞直到过期key的比例下降到25%以下。 |
使用这种策略是为了避免清除过期key的过程占用太多内存,这种方法对系统几乎不会有不良影响,因为大量key同时到期并非是一种常见现象,不过如果用户使用了 EXPIREAT 来设置过期时间的话也是有可能的。 总而言之: 要知道大量key同时过期会对系统延迟造成影响。 |
Redis 看门狗
Redis2.6版本引进了redis看门狗(watchdog)软件,这是个调试工具用于诊断Redis的延迟问题 这个看门狗软件还是一个实验性功能,当用于生产环境时,请小心并做好备份工作,可能有意想不到的问题影响正常的redis服务。 当你没有更好的工具追踪问题时,可以使用它。 |
|
当你运行完了软件看门狗,你可以通过设置时间间隔参数为0来关闭看门狗。需要注意的:记得关闭看门狗,因为开启看门狗太长时间并不是一个好主意。 以下的例子,你可以看到,当看门狗监测到延迟事件的时候,输出日志文件的内容: [8547 | signal handler] (1333114359) --- WATCHDOG TIMER EXPIRED --- /lib/libc.so.6(nanosleep+0x2d) [0x7f16b5c2d39d] /lib/libpthread.so.0(+0xf8f0) [0x7f16b5f158f0] /lib/libc.so.6(nanosleep+0x2d) [0x7f16b5c2d39d] /lib/libc.so.6(usleep+0x34) [0x7f16b5c62844] ./redis-server(debugCommand+0x3e1) [0x43ab41] ./redis-server(call+0x5d) [0x415a9d] ./redis-server(processCommand+0x375) [0x415fc5] ./redis-server(processInputBuffer+0x4f) [0x4203cf] ./redis-server(readQueryFromClient+0xa0) [0x4204e0] ./redis-server(aeProcessEvents+0x128) [0x411b48] ./redis-server(aeMain+0x2b) [0x411dbb] ./redis-server(main+0x2b6) [0x418556] /lib/libc.so.6(__libc_start_main+0xfd) [0x7f16b5ba1c4d] ./redis-server() [0x411099] ------ 注意:例子中 DEBUG SLEEP 命令是用于阻塞服务器的。在不同的阻塞背景下,堆栈信息会有不同。 如果收集到多个看门狗的监测堆栈信息,我们鼓励你把这些信息发送到Redis Google Group:我们获得越多的信息,我们就越容易分析得到你的服务器到底有什么问题。 |
附录A:大内存页的实验 fork产生的延迟,可以通过大内存页来减缓,只是需要耗费更大的内存。下面的附录将详细描述在Linux内核中实现的这个功能。 虽然某些CPU会使用不同大小的页面。AMD和Intel CPU可以支持2MB的页面大小。这些页面有个别名,叫做“大页面”。某些操作系统可以实时地优化页面大小,透明地把小页面聚合成大页面。 |
在Linux系统,显式的huge page管理在2.6.16中得到支持,并且隐式透明的huge page管理也在2.6.38中得到支持。如果你的是最近的Linux发行版本(例如 RH6或者其派生版本),透明的huge page可以被开启,并且你可以使用包含这项够能的Redis版本。 这个是在Linux中,实验/使用huge page的最佳方法。 现在,如果运行旧版本的Linux(RH5, SLES 10-11, 或者其派生版本),不要害怕使用一些技巧,Redis可以通过补丁来支持huge page。 |
第一步,阅读Mel Gorman's primer on huge pages 现在有两个方法给Redis打补丁,让它支持huge page
然后,系统必须配置为支持huge page 以下命令分配和创建 N个huge page: $ sudo sysctl -w vm.nr_hugepages= 以下命令挂载huge page到文件系统 $ sudo mount -t hugetlbfs none /mnt/hugetlbfs在所有的情况下,一旦Redis运行huge page(透明或者非透明),将会得到如下的好处:
|
很不幸,除了更多的复杂操作,还有redis使用huge page会带来一个明显的缺陷。COW机制的粒度是页面。伴随2MB页面,页面被后台存储操作修改的可能性是4KB页面的512倍。实际的内存需要后台存储,所以可能性会增加很多,尤其是,当写操作很随机,并且伴随很差的定位。通过huge page,使用两倍的内存,而存储将不再是理论的突发事件。它真的会发生。 完整的性能评估结果可以参阅这里. |