本文是对 redis 官方文档 Redis latency problems troubleshooting 的翻译,诸位看官可结合原文食用。node
这个文档将会帮助你理解在使用 Redis 遇到延迟问题时发生了什么。ios
在这个上下文中,延迟指的是从客户端发送命令和接收到应答之间所消耗的时间。一般状况下,Redis 处理命令的时间十分短,在亚微妙之间,可是某些状况会致使高延迟。web
接下来的内容对于以低延迟方式运行 Redis 是很是重要的。然而,我理解咱们都是很忙的,因此就以快速清单开始吧。若是你在尝试了下面这些步骤失败了,请回来阅读完整的文档。redis
一般,使用下表来进行持久化和延迟/性能之间的权衡,从更好的安全性到更好的延迟排序。算法
如今,咱们花 15 分钟来了解下细节...shell
若是你遇到了延迟问题,你可能知道如何在你的程序中测量延迟,或者延迟现象十分明显。其实,redis-cli 能够在毫秒级内测量 Redis 服务的延迟,只要使用下面的命令:数据库
redis-cli --latency -h `host` -p `port`
复制代码
从 Redis 2.8.13 开始,Redis 提供了延迟监控功能,它经过对不一样执行路径(译者注:这个概念能够在延迟监控文档查看)采样来检测服务阻塞的位置。这将会使调试本文档所说明的问题更加方便,因此咱们建议尽快启用延迟监控。请查看延迟监控文档。缓存
延迟监控采样和报告功能会使你更方便的找出 Redis 系统延迟的缘由,不过咱们仍是建议你详细阅读本文以更好的理解 Redis 和延迟尖峰。安全
有一种延迟自己就是你运行 Redis 的环境的一部分,即操做系统内核以及--若是你使用了虚拟化--管理程序形成的延迟。bash
这种延迟没法消除,学习它是十分重要的,由于它是基准线,换句话说,由于内核和管理程序的存在,你不可能使 Redis 的延迟比在你的环境中运行的程序的延迟更低。
咱们将这种延迟称为内部延迟,redis-cli 从 Redis 2.8.7 开始就能够测量它了。这是一个在基于 Linux 3.11.0 的入门级服务器上运行的例子。
注意:参数 100 是测试执行的秒数。执行测试的时间越长,咱们越有可能发现延迟峰值。100 秒一般足够了,可是你可能想要运行屡次不一样时长的测试。请注意这个测试是 CPU 密集型的,将可能会使你的系统的一个核满载。
注意: redis-cli 在这个例子中须要运行在你运行 Redis 或者计划运行 Redis 的服务器上,而不是在客户端。在这个特殊模式下 redis-cli 根本不会链接 Redis 服务:它只是尝试测试内核不提供 CPU 时间去运行 redis-cli 进程本身的最大时长。
在上面的例子中,系统的内部延迟只是 0.115毫秒(或 115 微妙),这是一个好的消息,可是请记住,内部延迟可能会随运行时间变化和变化,这取决与系统的负载。
虚拟化环境表现将会差点,特别是当高负载或者受其余虚拟化环境影响。下面是在 Linode 4096 实例上运行 Redis 和 Apache 的结果:
这里咱们有 9.7 毫秒的内部延迟:这意味着咱们不能要求 Redis 作的比这个更好了。然而,在负载更高或有其它邻居的不一样虚拟化环境中运行时,能够更容易获得更差的结果。咱们可能在正常运行的系统中获得 40 毫秒的测试结果。
客户端经过 TCP/IP 或者 Unix 域来链接到 Redis。1 Gbit/s 的网络的典型延迟是 200 微妙,使用 Unix 域 socket 的延迟可能低至 30 微妙。它实际上取决于你的网络和系统硬件。在通信之上,系统添加了更多的延迟(因为线程调度,CPU 缓存,NUMA 配置,等等)。系统在虚拟环境中形成的延迟明显高于物理机。
结论是,即便 Redis 处理大部分命令只花费亚微秒级的时间,客户端和服务端之间大量往返的命令将必须为网络和系统相关延迟付出代价。
所以,高效的客户端会将多个命令组成流水线来减小往返的次数。这被服务器和大多数客户端支持。批量操做好比 MSET/MGET 也是为了这个目的。从 Redis 2.4 开始,一些命令还支持全部数据类型的可变参数。
这里是一些准则:
在 Linux,用户能够经过进程设置(taskset),cgroups,实时优先级(chrt),NUMA 设置,或者使用低延迟内核等来实现更好的延迟。请注意 Redis 并不适合绑定在单个 CPU 核心上。Redis 会 fork 后台任务像 bgsave 或 AOF,这操做会十分消耗 CPU。这些任务必须永远不和主进程在同一个核心上运行。
在大多环境中,这些系统级的优化不是必须的。只有当你须要它们或者熟悉它们的时候再去作这些操做。
Redis 被设计为大部分状况下使用单线程。这意味着一个线程处理全部客户端的请求,它使用了多路复用的技术。这意味着 Redis 在每一个是简单都只处理一个请求,因此全部的请求都是按顺序处理的。这很像 Node.js 的工做方式。然而,这两个产品一般都不会被认为很慢。这是由于他们处理任务的时间很短,不过主要是由于它们设计为不在系统调用阻塞,特别是从套接字读取数据或者往套接字写数据的时候。
我说 Redis 大多只用单线程,是由于从 Redis 2.4 开始,咱们使用多线程去在后台执行一些慢 I/O 操做,主要是和磁盘 I/O 相关,可是这不改变 Redis 使用单线程处理全部请求的事实。
一个单线程的后果是当有一个慢请求时,全部其它的客户端将会等待它完成。当执行像 GET 或 SET 或 LPUSH 这样的一般命令时是没有问题的,这些命令的执行时间都是常数的(很是短)。然而,有几个命令操做了大量的元素,像 SORT,LREM,SUNION 和其它的命令。例如,去两个大集合的交集会花费很是多的时间。
全部命令的算法复杂度都有文档记录。一个好的实践是,当你使用不熟悉的命令时先系统地测试一下它。
若是你有延迟的顾虑,那么你不该该用慢查询处理有大量元素的值,或者你应该运行 Redis 的副本去运行慢查询。
可使用 Redis 的慢日志功能来监控慢查询。
另外,你可使用你喜欢的进程监控程序(top,htop,prstat,等等)去快速的检查主 Redis 进程 所消耗的 CPU。若是它很高,但流量却不高,那么它一般表示正在执行慢查询。
重点:一个很是常见的由执行慢查询致使延迟的缘由是在生产环境执行 KEYS 命令。KEYS 命令在文档中指出只能用于调试目的。从 Redis 2.8 开始,引入了一些新的命令来迭代键空间和其它大集合,请查看 SCAN,SSCAN,HSCAN 和 ZSCAN 命令来获取更多的信息。
为了在后台生成 RDB 文件或者在启用 AOF 持久化时重写 AOF 文件,Redis 必须 fork 一个进程。fork 操做(在主线程运行)会引发延迟。fork 是在类 Unix 系统中一个开销很高的操做,由于它涉及到复制大量与进程关联的对象。对于和虚拟内存相关联的页表尤为如此。
例如,在 Linux/AMD64 系统,内存被分为每页 4 kB。为了将虚拟地址转换为物理地址,每一个进程保存了一个页表(实际是一棵树),它至少包含一个指向进程的每页地址空间的指针。因此,一个拥有 24 GB 的 Redis 实例须要 24 GB/4 KB*8 = 48 MB 的页表。
当 bgsave 执行的时候,实例将会被 fork,这将建立和拷贝 48 MB 的内存。它会花费时间和 CPU,特别是在虚拟机上建立和初始化大页面将会是很是大的开销。
现代硬件拷贝页表很是快,除了 Xen。这个问题不是出在 Xen 虚拟化,而是 Xen 自己。例如,使用 VMware 或 Virtual Box 不会致使缓慢的 fork 时间。下表是比较不一样的 Redis 实例 fork 所消耗的时间。数据来自于执行BGSAVE,并观察INFO命令输出的latest_fork_usec
信息。
然而,好消息是基于 EC2 HVM 的实例执行 fork 操做的表现很好,几乎和在物理机上执行差很少,因此使用 m3.medium (或高性能)的实例将会获得更好的结果。
正如您所看到的,在 Xen 上运行的某些虚拟机的性能损失介于一个数量级到两个数量级之间。对于 EC2 用户,建议很简单:使用基于 HVM 的现代实例。
很不幸,若是 Linux 内核开启了 transparent huge pages 功能,Redis 将会在调用 fork 来持久化到磁盘时形成很是大的延迟。大内存页致使了下面这些问题:
请确认使用如下命令禁用 transparent huge pages:
echo never > /sys/kernel/mm/transparent_hugepage/enabled
复制代码
Linux(还有不少其它现代操做系统)能够将内存页从内存缓存到磁盘,或从磁盘读入内存,这是为了更有效的使用系统内存。
若是 Redis 页被内核从内存保存到了交换文件,当保存在这个内存页中的数据被 Redis 使用到的时候(好比访问保存在这个页中的键)内核为了将这页移动到主存,会中止 Redis 进程。访问随机 I/O 是一个缓慢的操做(和访问在内存中的页相比)并且 Redis 客户端将会经历异常的延迟。
内核将 Redis 内存页保存到磁盘主要有三个缘由:
幸运的是 Linux 提供了好的工具去验证这个问题,因此,最简单的事是当怀疑是交换内存引发延迟时,那就去检查它吧。
第一件事要作的是检查 Redis 内存交换到磁盘的数量。这以前,你须要获取 Redis 实例的 pid:
$ redis-cli info | grep process_id
process_id:5454
复制代码
如今进入 /proc 目录下该进程的目录:
$ cd /proc/5454
复制代码
你将会在这里找到一个叫作 smaps 的文件,它描述了 Redis 进程的内存布局(假设你使用的是 Linux 2.6.16 以上的系统)。这个文件包含了和咱们的进程内存映射相关的很是详细的信息,一个名为 Swap 的字段就是咱们所须要的。然而,当 smaps 文件包含了不一样的关于 Redis 进程的内存映射以后,它就不是单单一个 swap 字段了(进程的内存布局比一页简单的线性表要远远复杂)。
由于咱们对进程相关的内存交换都感兴趣,因此第一件事要作的是用 grep 获得文件中全部的 Swap 字段:
$ 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
复制代码
若是全部都是 0 KB,或者零散一个 4k 的条目,那么一切都很完美。事实上,在咱们的示例中(一个运行 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
复制代码
正如你从输出所见,这里有一个映射有 720896 kB(只有 12 KB 被交换),在另外一个映射中有 156 KB 被交换:基本上占咱们内存很是少的部分被交换,因此不会形成大的问题。
相反,若是有大量进程内存页被交换到磁盘,那么你的响应延迟问题可能和内存页交换有关。若是是这样的话,你可使用vmstat
命令来进一步检查你的 Redis 实例:
输出中咱们须要的一部分是两列 si
和 so
,它们记录了内存从 swap 文件交换出和交换入的次数。若是你看到的这两列是非 0 的,那么你的系统上存在内存交换。
最后, iostat 命令能够用于检测系统的全局 I/O 活动。
若是延迟问题是因为Redis内存在磁盘上交换形成的,则须要下降系统内存压力,若是Redis使用的内存超过可用内存,则增长更多内存,或者避免在同一系统中运行其余内存耗尽的进程。
另外一个致使延迟的缘由是 Redis 支持的 AOF 功能。AOF 基本上只使用两个系统调用来完成工做。一个是 write(2)
,它用来追加数据到文件,另外一个是 fdatasync(2)
,用来刷新内核文件缓存到磁盘,用来确保用户指定的持久化级别。
write(2)
和 fdatasync(2)
都有可能致使延迟。例如,当正在进行系统大范围同步,或者输出缓冲区已满,内核须要将数据刷新到磁盘来保证接受新的写操做时,都有可能阻塞 write(2)
。
fdatasync(2)
调用是一个致使延迟的更糟糕的缘由,由于使用的内核和文件系统的许多组合可能须要几毫秒到几秒才能完成,特别是在某些其余进程执行 I/O 的状况下。出于这个缘由,Redis 2.4 可能会在不一样的线程中执行fdatasync(2)
调用。
咱们将会看到如何设置能够改善使用 AOF 文件引发的延迟问题。
可使用appendfsync
配置选项将 AOF配 置为以三种不一样方式在磁盘上执行 fsync(可使用CONFIG SET
命令在运行时修改此设置)。
appendfsync
被设置为 no
时,Redis 将不会执行 fsync。这种设置方式里,只有write(2)
会致使延迟。这种状况下发生延迟一般没有解决方法,缘由很是简单,由于磁盘拷贝的速度没法跟上 Redis 接收数据的速度,不过这种状况十分不常见,除非由于其余进程在操做 I/O 致使磁盘读写很慢。appendfsync
被设置为 everysec
时,Redis 每秒执行一次 fsync。它会使用一个不一样的线程,而且当 fsync 在运行的时候,Redis 会使用 buffer 来延迟 write(2)
的调用 2 秒左右(由于在 Linux 中,当 write 和 fsync 在进程中竞争同一个文件时会致使阻塞)。然而,若是 fsync 占用了太长的时间,Redis 最终会执行 write 调用,这将会致使延迟。appendfsync
被设置为 always
时,fsync 会在每次写操做时执行,这个操做发生在回复 OK 应答给客户端以前(事实上 Redis 会尝试将多个同时执行的命令集合到一个 fsync 中)。在这种模式下性能一般会很是慢,很是推荐使用快的磁盘和执行 fsync 很快的文件系统。大多数 Redis 用户会使用 no
或者 everysec
来设置 appendfsync
。得到最少的延迟的建议时避免其它进程在同一系统中操做 I/O。使用 SSD 硬盘能够提供很好的帮助,不过若是磁盘是空闲的,那么非 SSD 磁盘也能有很好的表现,由于 Redis 写 AOF 文件的时候不须要执行任何查找。
若是想要确认延迟是否和 AOF 文件相关,在 Linux 下你可使用 strace
命令来查看:
sudo strace -p $(pidof redis-server) -T -e trace=fdatasync
复制代码
上面的命令将会显示 Redis 在主线程执行的全部 fdatasync(2)
命令。你不能经过上面的命令查看当 appendfsync 设置为 everysec
时在后台线程执行的 fdatasync
命令。能够添加 -f 参数来查看后台执行的 fdatasync
命令。
若是你想要同时查看 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 使用下面两种方法来处理过时的键:
主动过时的方式被设计为自适应的。每 100 毫秒一个周期(每秒 10 次),它将进行下面的操做:
ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP
键,删除全部已通过期的键。ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP
的默认值是 20,这个过程每秒执行 10 次,一般没有将会有 200 个键因过时被主动删除。这已经可以快地清除 DB 了,即便有些键已经好久没有被访问了,因此被动算法是没什么帮助的。同时,每秒删除 200 个键并不会对 Redis 的延迟形成影响。
然而,这个算法是自适应的,若是在采样键集合中找到了超过 25% 的键是过时的,它会循环执行。可是运行这个算法的周期是每秒 10 次,这意味着可能发生在同一秒内被采样的键 25% 以上都过时的状况。
基本上,这意味着 若是数据库在同一秒内有很是多键过时了,并且它们构成了当前键集合 25% 的失效键集合,Redis 可能会阻塞着直到失效键的比例降到 25% 如下。
这种方式是为了不由于失效的键使用过多的内存,并且一般都是无害的,由于在同一秒内出现大量失效的键是十分奇怪的,可是用户在同一 Unix 时间普遍使用 EXPIREAT
命令也不是不可能。
简而言之:注意大量键同时过时引发的响应延迟。
Redis 2.6 引入了 Redis 软件看门狗这个调试工具,它被设计用来跟踪使用其它一般工具没法分析的致使延迟的问题。
软件看门狗是一个试验性的特性。虽然它被设计为用于开发环境,不过在继续使用以前仍是应该备份数据库,由于它可能会对 Redis 服务器的正常操做形成不可预知的影响。
它只有在经过其它方法没有办法找到缘由的状况下才能使用。
下面是这个特性的工做方式:
CONFIG SET
命令启用软件看门狗。注意,这个特性不能在 redis.conf
文件中启用,由于它被设计为只能在已经运行的实例中启用,而且只能用于测试用途。
使用下面的命令来启用这个特性:
CONFIG SET watchdog-period 500
复制代码
period 被指定为毫秒级。上面这个例子是指在检测到服务器有 500 毫秒或以上的延迟时则记录到日志文件中。最小可配置的延迟时间是 200 毫秒。
当你使用完了软件看门狗,你能够将watchdog-period
参数设置为 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:咱们收集到越多的堆栈信息,那么你的实例的问题将越容易被理解。