Redis官方文档-诊断“延迟”问题

83 阅读11分钟

一、概述

这里我们讨论的“延迟”是客户端从发出命令到接收命令的最大耗时,一般在亚微秒,但在某些场景下这个耗时会增大。

首先看下为了确保低耗时运行,我们必须遵守哪些“Redis规范”:

  • 确保没有执行慢命令,慢命令会导致其他读写请求阻塞;
  • 使用EC2(亚马逊云)的用户,请确保使用基于HVM的现代EC2实例(如m3.medium),否则fork()太慢;
  • 必须在内核中禁用透明的大页面。可以使用echo never > /sys/kernel/mm/transparent_hugepage/enabled关闭它们,并重新启动Redis进程。
  • 在虚拟机上运行的Redis会天然存在一个内在延迟,可以在服务端使用./redis-cli——intrinsic-latency 100检查运行时环境可预期的最小延迟;
  • 可以启用和使用Redis的延迟监视器功能(Latency monitor),以便获得人类可读的延迟事件和原因的描述;

虽然我们预期可以拥有最高的安全性和最低的延迟,但现实情况是二者不可完美兼备,需要在数据安全(持久性) 和 业务性能(延迟) 之间做好取舍。 可以如下做权衡:

  • AOF + fsync always: 开启AOF持久化,每次写入命令都必须触发fsync
  • AOF + fsync every second: 开启AOF持久化,每秒触发一次fsync
  • AOF + fsync every second + no-appendfsync-on-rewrite选项设置为yes:开启AOF持久化,每秒触发一次fsync,但避免在重写过程中触发fsync,以降低磁盘压力(推荐)
  • AOF + fsync never:开启AOF持久化,但不主动fsync,同步由内核决定,磁盘压力和延迟高峰的风险更小
  • RDB:不开启AOF持久化,只做RDB持久化

二、如何发现延迟?

(一)redis-cli --latency命令

如下命令,测量Redis服务器是否有延迟(毫秒粒度):

redis-cli --latency -h `host` -p `port`

(二)内部Redis延时监控子系统(latency monitoring)

原生Redis在2.8.13版引入时延监控(Latency Monitoring)特性,基于事件机制帮助您发现和排查可能的时延问题。该功能仅支持获取最近160秒的数据,且只存取每秒内时延最高的事件。

1、哪些事件会触发延迟采样?

基于redis 6.2.3分析,触发延迟采样的有以下事件:

  1. aof-write-pending-fsync:AOF写入过程中存在“后台进程正在执行fsync”。

the write(2) system call when there is a pending fsync.

当后台的fsync正在执行时,会导致AOF write操作阻塞,所以在write之前需要检查下BIO中是否有fsync任务pending,如果有则需要记录这个事件。

  1. aof-write-active-child:AOF写入过程中存在“其他子进程也在向磁盘写数据”等情况。

the write(2) system call when there are active child processes

如果触发AOF write时存在一些活跃子进程(例如:正在执行RDB),此时AOF write可能会慢一些。

  1. aof-write-alone:一次正常的AOF写入(后台没有正在执行的fsync,也没有其他子进程写磁盘)

the write(2) system call when no pending fsync and no active child process. 数据写入量较大,或磁盘性能存在瓶颈时可能会慢一些。

  1. aof-write:一次AOF写入的耗时,如果延迟很大需要根据aof-write-pending-fsyncaof-write-active-childaof-write-alone三者事件耗时具体分析。

writing to the AOF - a catchall event for write(2) system calls.

AOF成功时会记录两个事件:

1)aof-write-pending-fsyncaof-write-active-childaof-write-alone其中一个事件;

2)aof-write事件

  1. command:常规命令(未被标为fast)的耗时。

如果该事件触发延迟采样,通常是特殊命令造成,例如执行KEYS命令,遍历所有数据。

  1. fast-command:被标记为fask的命令,命令的时间负责度为O(1) 或 O(log N)

如果该事件触发延迟采样,通常是对大Key执行命令产生,例如执行GET命令,拷贝大量数据。

  1. fork:调用Fork操作的耗时。

the fork(2) system call. 通常在AOF Rewrite(重写)时产生。

  1. rdb-unlink-temp-file:调用unlink的耗时

the unlink(2) system call.

BGSAVE的过程是这样的:子进程把数据写入临时文件,结束时向父进程发送信号;父进程接收到信号以后更新属性,然后rename覆盖原来的dump.rdb。

如果子进程创建RDB被中断,需要删除这个临时的RDB文件(temp-pid-.rdb),就会触发rdb-unlink-temp-file事件

  1. aof-rename: 完成BGREWRITEAOF后,rename RDB临时文件的延迟。

the rename(2) system call for renaming the temporary file after completing BGREWRITEAOF.

  1. aof-fsync-always:记录appendfsync allways 策略时的aof fsync延迟,注意:every second策略时不会记录该事件。

the fsync(2) system call when invoked by the appendfsync allways policy.

  1. aof-fstat:fstat的时延统计

the fstat(2) system call.

  1. aof-rewrite-diff-write:writing the differences accumulated while performing BGREWRITEAOF. 13.active-defrag-cycle: redis增量内存碎片整理过程消耗的时间

the active defragmentation cycle. 内存碎片整理对CPU资源的消耗非常高,而且在Redis实例比较大的时候持续的时间也非常长。

  1. expire-cycle: the expiration cycle.
  2. eviction-cycle: the eviction cycle.
  3. eviction-del: deletes during the eviction cycle.
  4. aof-rewrite-write-data-to-child:用于AOF重写结束后向aof_pipe_write_data_to_child管道中发送重写期间接受到到命令的缓存。
  5. aof-rewrite-done-fsync:记录AOF策略为always时fsync的时延,而everysecond时fsync被扔到BIO了。
  6. command-unblocking:在server完成了一些阻塞操作的时候进行记录
  7. while-blocked-cron:略

如果慢查日志里出现如上latency事件慢命令,可参考如下处理:可参考:常见Latency(时延)事件的处理建议

2、如何启用延迟监控?

默认情况下会关闭延迟监控,(阈值设置为0),有需要排查延迟问题时可如下方式开启(开启的成本损耗较小)

CONFIG SET latency-monitor-threshold 100
3、如何查看延迟监控报告?
  • LATENCY LATEST - returns the latest latency samples for all events.
  • LATENCY HISTORY - return3s latency time series for a given event.
  • LATENCY RESET - resets latency time series data for one or more events.
  • LATENCY GRAPH - renders an ASCII-art graph of an event's latency samples.
  • LATENCY DOCTOR - replies with a human-readable latency analysis report.

三、引起延迟的因素

(一)延迟基线

有一种延迟是运行Redis的环境固有的一部分,这是由操作系统内核提供的延迟(如果Redis使用虚拟化,则由使用的管理程序提供,延迟会更高),我们称这种延迟为内在延迟,业务读写请求请求耗时肯定要大于内在延迟的。 从Redis 2.8.13开始,我们可以通过在服务端执行redis-cli --latency命令,测量内在延迟:

# 参数100是测试执行的秒数,运行测试的时间越长,就越有可能发现延迟峰值
# 内在延迟可能会随着时间的变化而变化,这取决于系统的负载。
$ ./redis-cli --intrinsic-latency 100
Max latency so far: 1 microseconds.
Max latency so far: 16 microseconds.
Max latency so far: 50 microseconds.
Max latency so far: 53 microseconds.
Max latency so far: 83 microseconds.
Max latency so far: 115 microseconds.

请注意:该测试是CPU密集型的,可能会占用系统中的单个内核。 在这种特殊模式下,redis-cli根本不连接到Redis服务器,它只是试图测量内核不提供CPU时间运行到Redis -cli进程本身的最大时间。

(二)网络和通信引起的延迟

网络通信带来一些延迟:

  • 客户端通过TCP/IP连接或Unix域连接连接到Redis,1Gbit /s网络的典型延迟约为200 us。
  • Unix域套接字的典型延迟可低至30 us。

在通信本身之上,系统增加了一些更多的延迟(由于线程调度,CPU缓存,NUMA放置等…),虚拟化环境上的延迟更是明显高于物理机器。

现状就是:即使Redis在亚微秒范围内处理大多数命令,执行多次到服务器的往返的客户端也必须为这些网络和系统相关的延迟买单

因此,高效的客户端将尝试通过将几个命令连接在一起来限制往返次数。服务器和大多数客户端都完全支持这一点。MSET/MGET等聚合命令也可用于此目的MSET/MGET等聚合命令也可用于此目的。从Redis 2.4开始,许多命令也支持所有数据类型的可变参数。

四、如何定位延迟问题?

有如下一些建议:

  • 最好使用物理机而非虚拟机来托管Redis服务;
  • 使用长连接,减少建连开销;
  • 如果客户端与服务器在同一台主机,建议使用Unix域套接字通信;
  • 尽可能使用聚合命令(MSET/MGET) 或 pipeline批处理命令;
  • Redis支持Lua服务器端脚本,以替代不适合使用pipeline的情况(例如:一个命令的结果是下一个命令的入参)。

在Linux上,有些人可能通过使用进程放置(taskset)、cgroups、实时优先级(chrt)、NUMA配置(numactl),或使用低延迟内核来实现更低的延迟。请注意:vanilla Redis并不适合绑定在单个CPU核心上。Redis可以fork后台任务,这些任务可能非常消耗CPU,比如BGSAVE或BGREWRITEAOF。这些任务绝不能在与主事件循环相同的核心上运行。

(一)慢命令导致的延迟

Redis是单线程处理读写请求,单线程的后果是:当一个请求处理得很慢时(例如:keys命令),所有其他客户端都将等待这个请求被处理。可以通过命令文档查看算法复杂度。

可以通过如下方式监控慢命令:

  • 慢日志功能(slowlog get命令)
  • 系统指标监控(top、htop、prstat等),如果CPU很高但QPS不高,可能有慢命令;

(二)fork导致的延迟

为了在后台生成RDB文件,或者在启用AOF持久化的情况下重写追加文件,Redis必须fork后台进程。fork操作(在主线程中运行)本身会引起延迟

在大多数类unix系统上,fork是一项昂贵的操作,因为它涉及复制大量主进程相关的对象链接,与虚拟内存机制相关联的页表尤其如此。

例如,在Linux/AMD64系统上,内存被划分为4 kB的页(page)。为了将虚拟地址转换为物理地址,每个进程存储一个页表(实际上表示为树),其中每个页包含进程地址空间的至少一个指针。所以一个大的24GB的Redis实例需要一个24KB / 4KB * 8 = 48MB的页表。

当执行后台持久化时,这个实例必须被fork,这将涉及分配和复制48 MB的内存,这需要花费时间和CPU,特别是在虚拟机上,分配和初始化一个大内存块可能是昂贵的。

下面比较了不同Redis实例大小的fork时间,数据是通过执行BGSAVE并查看INFO命令回显中的latest_fork_usec字段获得的。

系统环境Redis实例容量fork总耗时每GBfork耗时
VMware6.0GB RSS77毫秒12.8毫秒
Linux在物理机上运行(未知HW)6.1GB RSS80毫秒13.1毫秒
Linux在物理机上运行(Xeon @ 2.27Ghz)6.9GB RSS62毫秒9毫秒
Linux虚拟机在6sync (KVM)360 MB RSS8.2毫秒23.3毫秒
EC2上的Linux VM 旧实例类型(Xen)6.1GB RSS1460毫秒239.3毫秒
EC2上的Linux VM 新实例类型(Xen)1GB RSS10毫秒10毫秒
Linux虚拟机 Linode (Xen)9GBRSS382毫秒424毫秒

正如上面看到的:某些运行在Xen上的虚拟机的性能受到了一个到两个数量级的影响,对于EC2用户,建议很简单:使用基于HVM的现代实例。

(三)透明大页导致的延迟

当Linux内核启用了透明大页面(transparent huge pages)时,持久化调用fork后会导致Redis很大的延迟。主要原因如下:

fork调用后,两个进程会共享透明大页,在一个繁忙的Redis实例中,几个事件循环的运行将导致命令针对几千个页面,导致写时复制(copy-on-write)几乎占用整个进程内存,这将导致较大的延迟和内存占用。

使用以下命令禁用透明大页:

echo never > /sys/kernel/mm/transparent_hugepage/enabled

(四)swapping(交换)导致的延迟

Linux(以及许多其他现代操作系统)能够将内存页从内存重新定位到磁盘,反之亦然,以便有效地使用系统内存。

如果一个Redis页面被内核从内存移动到swap文件中,当存储在这个内存页中的数据被Redis使用时(例如访问存储在这个内存页面中的键),内核将停止Redis进程,以便将该页面移回主内存中。这是一个涉及随机I/ o的缓慢操作(与访问内存页相比),并将导致Redis客户端经历异常延迟。

内核将Redis内存页面迁移到磁盘上(swap文件)主要有以下三个原因:

  • 由于正在运行的进程所需要的物理内存超过了可用的物理内存,导致系统处于内存高压下,这个问题最简单的例子就是Redis使用了比可用内存更多的内存。
  • Redis实例数据集,或者数据集的一部分,大部分是完全空闲的(客户端从来没有访问过),所以内核可以交换磁盘上空闲的内存页。这个问题非常罕见,因为即使是中等速度的实例也会经常接触所有的内存页,迫使内核将所有的页保留在内存中。
  • 系统某些进程产生大量的读写I/O。因为文件通常是缓存的,所以它倾向于给内核增加文件系统缓存的压力,从而产生交换活动。请注意:它包括Redis RDB 和/或 AOF后台线程,可以产生大文件。

如果怀疑是内存页交换(swapping)导致的延迟,可以按如下方法排查确认:

  • 方法一:检查磁盘上交换的Redis内存的数量
# 1)获取redis进程
$ redis-cli info | grep process_id
process_id:5454

# 2)进入这个进程的/proc文件系统目录
$ cd /proc/5454

# 3)发现samps文件,并过滤'Swap' 和 Size 关键字段,交换Size较大说明延迟可能与swapping相关
# 分析:从输出中看到的,有一个720896 kB的映射(仅交换了12 kB),在另一个映射中交换了156 kB:基本上交换了非常少量的内存
$ 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  # 720896 kB的映射
Swap:                 12 kB  # 仅交换了12 KB
Size:               4096 kB
Swap:                156 kB  # 最大也就交换了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
  • 方法二:wmstat命令和iostat命令,检查是否磁盘交换了大量进程内存
# 关注si和so两列,它计算交换文件的内存进/出量,如果在这两列中看到非零计数,则系统中存在交换活动
$ 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
 
# 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

如果你的延迟问题是由于Redis内存在磁盘上wapping文件导致的,则需要降低系统中的内存压力,如果Redis使用的内存比可用的内存多,要么增加更多的RAM,要么避免在同一个系统中运行其他内存饥饿进程。

(五)AOF和磁盘I/O导致的延迟

AOF使用两个系统调用来完成它的工作:

  • write(2):用来将数据写入仅追加的文件;
  • fdatasync(2):用来刷新磁盘上的内核文件缓冲区,以确保用户指定的持久性级别;

write(2) 和 fdatasync(2) 调用都可能是延迟的来源,例如:

  • 一个sync进程正在进行中,或者当输出缓冲区已满、内核需要刷新磁盘以接受新的写入时,此时调用write(2)可能阻塞。
  • fdatasync(2)调用可能会引发更糟的延迟,因为它内部使用了许多内核和文件系统的组合调用,可能需要几毫秒到几秒才能完成,特别是一些其他进程进行I/O的情况下,基于以上原因,从Redis2.4开始,Redis会尽可能在不同的线程中调用fdatasync(2)

AOF可以使用appendfsync配置选项,以三种不同的方式对磁盘执行fsync:

  • appendfsync = no:不执行fsync,这种配置下延迟的唯一来源是write(2),这种情况下的延迟通常没有解决方案,因为磁盘无法应对Redis接收数据的速度。一般很少发生write(2)调用导致的阻塞,除非磁盘还有其他进程占用I/O导致性能衰减。
  • appendfsync = everysec: 每秒钟执行一次fsync,会使用不同的线程来调用fsync,如果当前有fsync正在进行,则Redis会使用一个缓冲区来延迟write(2)调用(最多延迟2秒,因为在Linux上,如果一个文件的fsync正在进行,写将被阻塞),如果fsync花费太长时间,即使fsync仍在进行中,Redis最终也会执行write(2)调用,这可能是延迟的来源。
  • appendfsync = always:每个写操作都执行一个fsync,然后用OK代码回复客户端(实际上Redis会场景将同时执行的多个命令聚类为一个fsync),这种配置下性能很低,强烈建议使用快速磁盘和可以在短时间内执行fsync的文件系统实现。

大多数Redis用户使用noeverysec配置,对于最小延迟的建议是:

  • 避免其他进程在同一系统中执行I/O;
  • 使用SSD磁盘

可以在Linux下使用strace命令,来调查与追加文件相关的延迟问题:

# 显示Redis在主线程中执行的所有fdatasync(2)系统调用
sudo strace -p $(pidof redis-server) -T -e trace=fdatasync
sudo strace -p $(pidof redis-server) -T -e trace=fdatasync -f

# 显示Redis在主线程中执行的所有fdatasync和写操作系统调用
sudo strace -p $(pidof redis-server) -T -e trace=fdatasync,write

# 上述命令会显示出很多与I/O无关的东西,可以通过如下命令过滤,只显示调用缓慢的命令
sudo strace -f -p $(pidof redis-server) -T -e trace=fdatasync,write 2>&1 | grep -v '0.0' | grep -v unfinished

(六)key过期导致的延迟

Redis以两种方式清理过期key:

  • 惰性删除:当命令请求一个key时,才检查该key是否过期;
  • 定期删除:后台周期过期

以上过期key清理策略,详见:Redis内存过期策略分析

这里需要关注:key集群过期可能会导致延迟。

(七)Redis软件看门狗

Redis 2.6引入了Redis软件看门狗(Redis Software Watchdog),这是一个调试工具,用于跟踪那些由于某种原因而无法使用普通工具进行分析的延迟问题。

软件看门狗是一个实验性的功能。虽然它被设计用于生产环境,但在使用之前应该注意备份数据库,因为它可能与Redis服务器的正常执行有意外的交互。

重要的是:当无法通过其他方式跟踪问题时,仅将其作为最后的手段。

下面是这个功能的工作原理:

  1. 用户使用CONFIG SET命令开启软件看门狗。
  2. Redis开始不断地监控自己。
  3. 如果Redis检测到服务器被阻塞在一些返回速度不够快的操作中,这可能是延迟问题的根源,那么一个关于服务器被阻塞位置的低级报告就会转储到日志文件中。
  4. 用户在Redis谷歌组中联系开发人员,在消息中写入看门狗报告。

注意:不能使用redis.conf文件启用该特性,因为它被设计为仅在已经运行的实例中启用,并且仅用于调试目的。

通过如下参数启用软件看门狗:

# 周期以毫秒为单位,这里设置的500表示:当服务器检测到延迟500毫秒或更大时才记录延迟问题,最小可配置周期为200毫秒,[0值表示关闭看门狗](url)
CONFIG SET watchdog-period 500

参考

redis.io/docs/manage… redis.io/docs/manage… help.aliyun.com/document_de… www.cnblogs.com/lizhaolong/…