Redis 响应延时问题排查
本文将有助于你找出 Redis 响应延迟的问题所在。
文中出现的延迟(latency)均指从客户端发出一条命令到客户端接受到该命令的反馈所用的最长响应时间。Reids 通常处理(命令的)时间非常的慢,大概在次微妙范围内,但也有更长的情况出现。
计算延迟时间
如果你正在经历响应延迟问题,你或许能够根据应用程序的具体情况算出它的延迟响应时间,或者你的延迟问题非常明显,宏观看来,一目了然。不管怎样吧,用 redis-cli 可以算出一台 Redis 服务器的到底延迟了多少毫秒。踹这句:
网络和通信引起的延迟
当用户连接到 Redis 通过 TCP/IP 连接或 Unix 域连接,千兆网络的典型延迟大概 200us,而 Unix 域 socket 可能低到 30us。这完全基于你的网络和系统硬件。在通信本身之上,系统增加了更多的延迟(线程调度,CPU 缓存,NUMA 替换等等)。系统引起的延迟在虚拟机环境远远高于在物理机器环境。
实际情况是即使 Redis 处理大多数命令在微秒之下,客户机和服务器之间的交互也必然消耗系统相关的延迟。一个高效的客户机因而试图通过捆绑多个命令在一起的方式减少交互的次数。服务器和大多数客户机支持这种方式。聚合命令象 MSET/MGET 也可以用作这个目的。从 Redis 2.4 版本起,很多命令对于所有的数据类型也支持可变参数。
这里有一些指导:
如果你负担的起,尽可能的使用物理机而不是虚拟机来做服务器
不要经常的 connect/disconnect 与服务器的连接(尤其是对基于 web 的应用),尽可能的延长与服务器连接的时间。
如果你的客户端和服务器在同一台主机上,则使用 Unix 域套接字
尽量使用聚合命令(MSET/MGET)或可变参数命令而不是 pipelining
如果可以尽量使用 pipelining 而不是序列的往返命令。
针对不适合使用原始 pipelining 的情况,如某个命令的结果是后续命令的输入,在以后的版本中 redis 提供了对服务器端的 lua 脚本的支持,实验分支版本现在已经可以使用了。
在 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。
Linux beefy VM on VMware 6.0GB RSS forked 77 微秒 (每 GB 12.8 微秒 ).
Linux running on physical machine (Unknown HW) 6.1GB RSS forked 80 微秒(每 GB 13.1 微秒)
Linux running on physical machine (Xeon @ 2.27Ghz) 6.9GB RSS forked into 62 微秒 (每 GB 9 微秒).
Linux VM on 6sync (KVM) 360 MB RSS forked in 8.2 微秒 (每 GB 23.3 微秒).
Linux VM on EC2 (Xen) 6.1GB RSS forked in 1460 微秒 (每 GB 239.3 微秒).
Linux VM on Linode (Xen) 0.9GBRSS forked into 382 微秒 (每 GB 424 微秒).
你能看到运行在 Xen 上的 VM 的 Redis 性能相差了一到两个数量级。我们相信这是 Xen 系统的一个验证问题,我们希望这个问题能尽快处理。
swapping (操作系统分页)引起的延迟
Linux (以及其他一些操作系统) 可以把内存页存储在硬盘上,反之也能将存储在硬盘上的内存页再加载进内存,这种机制使得内存能够得到更有效的利用。
如果内存页被系统移到了 swap 文件里,而这个内存页中的数据恰好又被 redis 用到了(例如要访问某个存储在内存页中的 key),系统就会暂停 redis 进程直到把需要的页数据重新加载进内存。这个操作因为牵涉到随机 I/O,所以很慢,会导致无法预料的延迟。
系统之所以要在内存和硬盘之间置换 redis 页数据主要因为以下三个原因:
系统总是要应对内存不足的压力,因为每个运行的进程都想申请更多的物理内存,而这些申请的内存的数量往往超过了实际拥有的内存。简单来说就是 redis 使用的内存总是比可用的内存数量更多。
redis 实例的数据,或者部分数据可能就不会被客户端访问,所以系统可以把这部分闲置的数据置换到硬盘上。需要把所有数据都保存在内存中的情况是非常罕见的。
一些进程会产生大量的读写 I/O。因为文件通常都有缓存,这往往会导致文件缓存不断增加,然后产生交换(swap)。请注意,redis RDB 和 AOF 后台线程都会产生大量文件。
所幸 Linux 提供了很好的工具来诊断这个问题,所以当延迟疑似是 swap 引起的,最简单的办法就是使用 Linux 提供的工具去确诊。
首先要做的是检查 swap 到硬盘上的 redis 内存的数量,为实现这个目的要知道 redis 实例的进程 id:
进入进程目录:
在这里你会发现一个名为 smaps 的文件,它描述了 redis 进程的内存布局 (假定你使用的是 Linux 2.6.16 或者更新的版本)。这个文件包括了很多进程所使用内存的细节信息,其中有一项叫做 Swap 的正是我们所关心的。不过仅看这一项是不够的,因为 smaps 文件包括有 redis 进程的多个不同的的内存映射区域的使用情况(进程的内存布局远不是线性排列那么简单)。
从我们对所有进程的内存交换情况感兴趣以来,我们首先要做的事情是使用 grep 命令显示进程的 smaps 文件
假如所有的数据显示为 0kb 或者某些数据偶尔显示为 4kb,表示当前一切正常。实际上我们的例子是一个真实的运行着 Redis 并每秒为数百的用户提供服务的网站,会显示更多的交换页。为了研究是否存在一个严重的问题,我们改变命令打印出分配的内存尺寸
在输出信息中,你能看到有一个 720896kb 的内存分配(有 12kb 的交换)还有一个 156kb 的交换是另一个进程的。基本上我们的内存只会有很小的内存交换,因此不会产生任何的问题
假如进程的内存有相当部分花在了 swap 上,那么你的延迟可能就与 swap 有关。假如 redis 出现这种情况那么可以用 vmstat 命令来验证一下猜测:
输出中我们最感兴趣的两行是 si 和 so,这两行分别统计了从 swap 文件恢复到内存的数量和 swap 到文件的内存数量。如果在这两行发现了非 0 值那么就说明系统正在进行 swap。
最后,可以用 iostat 命令来查看系统的全局 I/O 行为。
如果确认延迟是由于 swap 引起的,那么就需要减小系统的内存压力,要么给机器增加内存,要么不要在同一个机器上运行其他消耗内存的程序。
AOF 和硬盘 I/O 操作延迟
另一个延迟的根源是 Redis 的 AOF(仅附加文件)模式。AOF 基本上是通过两个系统间的调用来完成工作的。 一个是写,用来写数据到 AOF, 另外一个是文件数据同步,通过清除硬盘上空核心文件的缓冲来保证用户指定的持久级别。
包括写和文件数据同步的调用都可以导致延迟的根源。 写实例可以阻塞系统范围的同步操作,也可以阻塞当输出的缓冲区满并且内核需要清空到硬盘来接受新的写入的操作。
文件数据同步对于延迟的影响非常大,因为它涉及到好几步调用,可能要花掉几毫秒以致几秒的时间,特别是在还有其他进程后也在占用 I/O 的情况下。因为这个原因,从 redis2.4 开始用一个单独的线程来做文件数据同步。
我们来看看当使用 AOF 的时候如何配置来降低延迟。
通过设置 AOF 相关的 appendfsync 项,可以使用三种不同的方式来执行文件同步(也可以在运行时使用 CONFIG SET 命令来修改这个配置)。
appendfsync 的值设置为 no,redis 不执行 fsync。这种情况下造成延迟的唯一原因就是写操作。这种延迟没有办法可以解决,因为 redis 接收到数据的速度是不可控的,不过这种情况也不常见,除非有其他的进程占用 I/O 使得硬盘速度突然下降。
appendfsync 的值设置为 everysec,每秒都会执行 fsync。fsync 由一个单独线程执行,如果需要写操作的时候有 fsync 正在执行 redis 就会用一个 buffer 来延迟写入 2 秒(因为在 Linux 如果一个 fsync 正在运行那么对该文件的写操作就会被堵塞)。如果 fsync 耗时过长(译者注:超过了 2 秒),即使 fsync 还在进行 redis 也会执行写操作,这就会造成延迟。
appendfsync 的值设置为 always ,fsync 会在每次写操作返回成功代码之前执行(事实上 redis 会积累多个命令在一次 fsync 过程中执行)。这种模式下的性能表现是非常差劲的,所以最好使用一个快速的磁盘和文件系统以加快 fsync 的执行。
大多数 redis 用户都会把这个值设成 no 或者 everysec。要减少延迟,最好避免在同一个机器上有其他耗费 I/O 的程序。用 SSD 也有益于降低延迟,不过即使不使用 SSD,如果能有冗余的硬盘专用于 AOF 也会减少寻址时间,从而降低延迟。
如果你想诊断 AOF 相关的延迟原因可以使用 strace 命令:
上面的命令会展示 redis 主线程里所有的 fdatasync 系统调用。不包括后台线程执行的 fdatasync 调用。如果 appendfsync 配置为 everysec,则给 strace 增加-f 选项。
用下面命令可以看到 fdatasync 和 write 调用:
不过因为 write 也会向客户端写数据,所以用上面的命令很可能会获得许多与磁盘 I/O 没有关系的结果。似乎没有办法让 strace 只显示慢系统调用,所以要用下面的命令:
数据过期造成的延迟
redis 有两种方式来去除过期的 key:
lazy 方式,在 key 被请求的时候才检查是否过期。 to be already expired.
active 方式,每 0.1 秒进行一次过期检查。
active 过期模式是自适应的,每过 100 毫秒开始一次过期检查(每秒 10 次),每次作如下操作:
根据 REDIS_EXPIRELOOKUPS_PER_CRON 的值去除已经过期的 key(是指如果过期的 key 数量超过了 REDIS_EXPIRELOOKUPS_PER_CRON 的值才会启动过期操作,太少就不必了。这个值默认为 10), evicting all the keys already expired.
假如超过 25%(是指 REDIS_EXPIRELOOKUPS_PER_CRON 这个值的 25%,这个值默认为 10,译者注)的 key 已经过期,则重复一遍检查失效的过程。
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 服务。
当你没有更好的工具追踪问题时,可以使用它。
这个功能是这样工作的:
用户通过命令 CONFIG SET 开启软件看门狗
Redis 启动监测程序监测自己的状态
如果 Redis 检测到服务器被某些操作阻塞了,并运行速度不够快,也许是因为延迟导致的,Redis 就会在 log 文件中写入一份关于被阻塞服务器的底层监测数据报表
用户通过 Redis Google Group 发送消息给开发人员,消息包括看门狗报表。
请注意,这项功能不能通过 redis.conf 文件开启,因为这项够能设计之初就是面向正在运行的服务器,而且只是为了调试程序。
如果要开启该功能,只需运行如下命令:
时间间隔以毫秒为单位。在上面的例子中,我指定了,当服务器检测到 500 毫秒或更大的延迟的时候,才记录延迟事件。最小的时间间隔是 200 毫秒。
当你运行完了软件看门狗,你可以通过设置时间间隔参数为 0 来关闭看门狗。需要注意的:记得关闭看门狗,因为开启看门狗太长时间并不是一个好主意。
以下的例子,你可以看到,当看门狗监测到延迟事件的时候,输出日志文件的内容:
注意:例子中 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
对于 Redis 2.4,内置的 jemalloc 分配器需要打上补丁。Pieter Noordhuis 的补丁patch 。需要注意,这个补丁依赖于匿名 mmap huge page 的支持,这项功能只能从 2.6.32 之后才得到支持,所以这个方法不能用于旧的版本(RH5 ,SLES 10, 和其派生版本)
对于 Redis 2.2 或者 2.4,附带 libc 分配器,必须修改 redis makefile,使 Redis 和the libhugetlbfs library进行连接。这个是最直接的更改
然后,系统必须配置为支持 huge page
以下命令分配和创建 N 个 huge page:
以下命令挂载 huge page 到文件系统
在所有的情况下,一旦 Redis 运行 huge page(透明或者非透明),将会得到如下的好处:
由于 fork 引起的延迟将得到缓解。尤其是对超大的实例,尤其是在 VM 上运行的实例。
Redis 速度得到提够,是因为 CPU 的转换旁视缓冲(TLB)更有效的缓存页面(例如命中率会更高)。不要期望有奇迹发生,性能至多只能提高一点。
Redis 内存不会再被换走,这样就能避免由于虚拟内存造成的不必的延迟。很不幸,除了更多的复杂操作,还有 redis 使用 huge page 会带来一个明显的缺陷。COW 机制的粒度是页面。伴随 2MB 页面,页面被后台存储操作修改的可能性是 4KB 页面的 512 倍。实际的内存需要后台存储,所以可能性会增加很多,尤其是,当写操作很随机,并且伴随很差的定位。通过 huge page,使用两倍的内存,而存储将不再是理论的突发事件。它真的会发生。
完整的性能评估结果可以参阅这里.
评论