延迟问题故障排除
如果您在使用 KeyDB 时遇到延迟问题,本文档将帮助您了解可能的原因。
在本文中,延迟是指客户端发出命令到客户端接收到命令回复之间的最大延迟。通常 KeyDB 的处理时间极低,在亚微秒范围内,但某些情况下会导致较高的延迟。
#
我时间不多,请给我一份检查清单以下文档对于以低延迟方式运行 KeyDB 非常重要。但我理解我们都很忙,所以我们从一个快速检查清单开始。如果您未能遵循这些步骤,请返回此处阅读完整文档。
- 确保您没有运行阻塞服务器的慢命令。使用 KeyDB 的慢日志功能来检查这一点。
- 对于 EC2 用户,请确保使用基于 HVM 的现代 EC2 实例,如 m3.medium。否则 fork() 会太慢。
- 必须禁用内核的透明大页。使用 `echo never > /sys/kernel/mm/transparent_hugepage/enabled` 来禁用它们,然后重启您的 KeyDB 进程。
- 如果您正在使用虚拟机,可能存在与 KeyDB 无关的内在延迟。使用 `./keydb-cli --intrinsic-latency 100` 检查您的运行时环境所能预期的最低延迟。注意:您需要在*服务器端*而不是客户端运行此命令。
- 启用并使用 KeyDB 的延迟监控功能,以获取您 KeyDB 实例中延迟事件和原因的可读描述。
总的来说,请使用下表来进行持久性与延迟/性能的权衡,按从更强安全性到更好延迟的顺序排列。
- AOF + fsync always:这非常慢,只有在您清楚自己在做什么时才应使用。
- AOF + fsync every second:这是一个很好的折中方案。
- AOF + fsync every second + no-appendfsync-on-rewrite 选项设置为 yes:这与上面类似,但在重写期间避免 fsync 以降低磁盘压力。
- AOF + fsync never:在这种设置下,fsyncing 取决于内核,磁盘压力和延迟峰值的风险更小。
- RDB:这里有广泛的权衡选择,具体取决于您配置的 save 触发器。
现在,对于有 15 分钟时间的人,以下是详细信息……
#
测量延迟如果您遇到延迟问题,您可能知道如何在您的应用程序上下文中测量它,或者您的延迟问题即使从宏观上看也非常明显。然而,keydb-cli 可以用来测量 KeyDB 服务器的延迟(以毫秒为单位),只需尝试:
#
使用 KeyDB 内部延迟监控子系统KeyDB 提供了延迟监控功能,能够对不同的执行路径进行采样,以了解服务器在何处阻塞。这使得调试本文档中说明的问题变得更加简单,因此我们建议尽快启用延迟监控。请参阅延迟监控文档。
虽然延迟监控的采样和报告功能将使您更容易理解 KeyDB 系统中延迟的来源,但仍建议您广泛阅读本文档,以更好地理解 KeyDB 和延迟峰值这一主题。
#
延迟基线有一种延迟是您运行 KeyDB 的环境所固有的,即您的操作系统内核以及(如果您使用虚拟化)您正在使用的虚拟机管理程序所提供的延迟。
虽然这种延迟无法消除,但研究它很重要,因为它是基线,换句话说,您无法实现比您环境中每个进程因内核或虚拟机管理程序实现或设置而经历的延迟更好的 KeyDB 延迟。
我们称这种延迟为内在延迟,`keydb-cli` 能够测量它。这是一个在入门级服务器上运行 Linux 3.11.0 的示例。
注意:参数 `100` 是测试将执行的秒数。我们运行测试的时间越长,我们就越有可能发现延迟峰值。100 秒通常是合适的,但您可能希望在不同时间进行几次运行。请注意,该测试是 CPU 密集型的,很可能会使您系统中的一个核心饱和。
注意:在这种特殊情况下,keydb-cli 需要**在您运行或计划运行 KeyDB 的服务器上运行**,而不是在客户端上。在这种特殊模式下,keydb-cli 根本不会连接到 KeyDB 服务器:它只会尝试测量内核不提供 CPU 时间来运行 keydb-cli 进程本身的最长时间。
在上面的例子中,系统的内在延迟仅为 0.115 毫秒(或 115 微秒),这是个好消息,但请记住,内在延迟可能会根据系统负载随时间变化。
虚拟化环境不会显示这么好的数字,尤其是在高负载或有“吵闹邻居”的情况下。以下是在运行 KeyDB 和 Apache 的 Linode 4096 实例上的运行结果:
这里我们的内在延迟为 9.7 毫秒:这意味着我们对 KeyDB 的期望不能比这更好。然而,在不同时间、不同虚拟化环境中,如果负载更高或有“吵闹邻居”,其他运行结果可能轻易地显示出更差的值。我们曾经在表面上运行正常的系统中测量到高达 40 毫秒的延迟。
#
由网络和通信引起的延迟客户端使用 TCP/IP 连接或 Unix 域套接字连接到 KeyDB。1 Gbit/s 网络的典型延迟约为 200 微秒,而使用 Unix 域套接字的延迟可以低至 30 微秒。这实际上取决于您的网络和系统硬件。在通信本身之上,系统还会增加一些延迟(由于线程调度、CPU 缓存、NUMA 放置等...)。在虚拟化环境中,系统引起的延迟明显高于物理机。
结果是,即使 KeyDB 在亚微秒范围内处理大多数命令,一个向服务器进行多次往返的客户端也必须为这些网络和系统相关的延迟付出代价。
因此,一个高效的客户端会尝试通过将多个命令一起流水线化来限制往返次数。这在服务器和大多数客户端上都得到完全支持。像 MSET/MGET 这样的聚合命令也可以用于此目的。许多命令还支持所有数据类型的可变参数。
以下是一些指导原则:
- 如果条件允许,优先选择物理机而不是虚拟机来托管服务器。
- 不要系统性地连接/断开服务器(对于基于 Web 的应用程序尤其如此)。尽可能保持连接的长久性。
- 如果您的客户端与服务器在同一台主机上,请使用 Unix 域套接字。
- 优先使用聚合命令(MSET/MGET)或带有可变参数的命令(如果可能),而不是流水线操作。
- 优先使用流水线操作(如果可能),而不是一系列的往返通信。
- KeyDB 支持 Lua 服务器端脚本,以处理不适合原始流水线操作的情况(例如,当一个命令的结果是后续命令的输入时)。
在 Linux 上,有些人可以通过调整进程放置 (taskset)、cgroups、实时优先级 (chrt)、NUMA 配置 (numactl),或使用低延迟内核来获得更好的延迟。请注意,原生 KeyDB 不太适合绑定到**单一** CPU 核心。KeyDB 可以 fork 后台任务,这些任务可能非常消耗 CPU,如 `BGSAVE` 或 `BGREWRITEAOF`。这些任务**绝不能**与主事件循环在同一个核心上运行。
在大多数情况下,这些系统级的优化是不需要的。只有在您需要它们,并且您熟悉它们时才进行。
#
慢命令产生的延迟只运行一个线程的一个后果是,当一个请求处理缓慢时,所有其他客户端都将等待这个请求被处理。当执行像 `GET`、`SET` 或 `LPUSH` 这样的普通命令时,这根本不是问题,因为这些命令是在常量(且非常小)时间内执行的。然而,有一些命令操作多个元素,如 `SORT`、`LREM`、`SUNION` 等。例如,对两个大集合取交集可能会花费相当长的时间。
建议运行多个服务器线程 `--server-threads #` 来尝试提高性能。
如果您有延迟方面的顾虑,您应该要么不对包含许多元素的值使用慢命令,要么您应该使用 KeyDB 复制运行一个副本,在该副本上运行所有慢查询。考虑运行一个主-主复制实例以利用您的资源。
可以使用 KeyDB 的慢日志功能来监控慢命令。
此外,您可以使用您喜欢的进程监控程序(top、htop、prstat 等)来快速检查主 KeyDB 进程的 CPU 消耗。如果流量不高而 CPU 消耗很高,这通常是使用慢命令的迹象。
重要提示:一个由执行慢命令产生的延迟的非常常见的来源是在生产环境中使用 `KEYS` 命令。正如 KeyDB 文档中所述,`KEYS` 应该仅用于调试目的。已经引入了新命令以增量方式迭代键空间和其他大型集合,请查看 `SCAN`、`SSCAN`、`HSCAN` 和 `ZSCAN` 命令以获取更多信息。
#
由 fork 产生的延迟为了在后台生成 RDB 文件,或者如果启用了 AOF 持久化,重写仅追加文件,KeyDB 必须 fork 后台进程。fork 操作(在主线程中运行)本身就可能引起延迟。
在大多数类 Unix 系统上,Fork 是一项昂贵的操作,因为它涉及到复制大量与进程相关的对象。对于与虚拟内存机制相关的页表尤其如此。
例如,在 Linux/AMD64 系统上,内存被分为 4kB 的页面。为了将虚拟地址转换为物理地址,每个进程都存储一个页表(实际上表示为一棵树),其中至少包含一个指向进程地址空间中每个页面的指针。因此,一个大型的 24GB KeyDB 实例需要一个 24 GB / 4 kB * 8 = 48 MB 的页表。
当执行后台保存时,这个实例将需要被 fork,这将涉及到分配和复制 48MB 的内存。这需要时间和 CPU,尤其是在虚拟机上,分配和初始化大块内存可能会很昂贵。
#
不同系统中的 fork 时间现代硬件在复制页表方面非常快,但 Xen 不是。Xen 的问题并非特定于虚拟化,而是特定于 Xen。例如,使用 VMware 或 Virtual Box 不会导致 fork 时间过慢。下表比较了不同 KeyDB 实例大小的 fork 时间。数据是通过执行 BGSAVE 并查看 `INFO` 命令输出中的 `latest_fork_usec` 字段获得的。
然而,好消息是**新型基于 HVM 的 EC2 实例在 fork 时间方面表现要好得多**,几乎与物理服务器相当,因此例如使用 m3.medium(或更好)的实例将提供良好的结果。
- 在 VMware 上的强大 Linux 虚拟机:6.0GB RSS 在 77 毫秒内 fork(每 GB 12.8 毫秒)。
- 在物理机上运行的 Linux(未知硬件):6.1GB RSS 在 80 毫秒内 fork(每 GB 13.1 毫秒)。
- 在物理机上运行的 Linux(Xeon @ 2.27Ghz):6.9GB RSS 在 62 毫秒内 fork(每 GB 9 毫秒)。
- 在 6sync 上的 Linux 虚拟机(KVM):360 MB RSS 在 8.2 毫秒内 fork(每 GB 23.3 毫秒)。
- 在 EC2 上的 Linux 虚拟机,旧实例类型(Xen):6.1GB RSS 在 1460 毫秒内 fork(每 GB 239.3 毫秒)。
- 在 EC2 上的 Linux 虚拟机,新实例类型(Xen):1GB RSS 在 10 毫秒内 fork(每 GB 10 毫秒)。
- 在 Linode 上的 Linux 虚拟机(Xen):0.9GB RSS 在 382 毫秒内 fork(每 GB 424 毫秒)。
如您所见,某些在 Xen 上运行的虚拟机性能下降了一到两个数量级。对于 EC2 用户,建议很简单:使用现代的基于 HVM 的实例。
#
由透明大页引起的延迟不幸的是,当 Linux 内核启用了透明大页时,KeyDB 在使用 `fork` 调用以持久化到磁盘后会遭受巨大的延迟惩罚。大页是以下问题的原因:
- 调用 Fork,创建了两个共享大页的进程。
- 在一个繁忙的实例中,几次事件循环运行将导致命令针对数千个页面,从而引起几乎整个进程内存的写时复制。
- 这将导致巨大的延迟和大量的内存使用。
请确保使用以下命令**禁用透明大页**:
#
由交换(操作系统分页)引起的延迟Linux(以及许多其他现代操作系统)能够将内存页面从内存重定位到磁盘,反之亦然,以有效利用系统内存。
如果一个 KeyDB 页面被内核从内存移动到交换文件,当 KeyDB 使用存储在该内存页面中的数据时(例如访问存储在该内存页面中的键),内核将停止 KeyDB 进程以便将该页面移回主内存。这是一个涉及随机 I/O 的慢操作(与访问已在内存中的页面相比),并将导致 KeyDB 客户端遇到异常的延迟。
内核将 KeyDB 内存页重定位到磁盘主要有三个原因:
- 系统面临内存压力,因为正在运行的进程要求的物理内存超过了可用量。这个问题的最简单实例就是 KeyDB 使用的内存超过了可用内存。
- KeyDB 实例的数据集或部分数据集大部分完全空闲(从未被客户端访问),因此内核可以将空闲的内存页交换到磁盘。这个问题非常罕见,因为即使是一个中等速度的实例也会经常接触所有内存页,迫使内核将所有页面保留在内存中。
- 一些进程在系统上产生大量的读或写 I/O。因为文件通常被缓存,这会给内核增加压力以增加文件系统缓存,从而产生交换活动。请注意,这包括可以产生大文件的 KeyDB RDB 和/或 AOF 后台线程。
幸运的是,Linux 提供了很好的工具来调查这个问题,所以当怀疑延迟是由交换引起时,最简单的事情就是检查是否是这种情况。
首先要做的是检查交换到磁盘的 KeyDB 内存量。为此,您需要获取 KeyDB 实例的 pid:
现在进入该进程的 /proc 文件系统目录:
在这里你会找到一个名为 **smaps** 的文件,它描述了 KeyDB 进程的内存布局(假设你使用的是 Linux 2.6.16 或更新版本)。这个文件包含了关于我们进程内存映射的非常详细的信息,其中一个名为 **Swap** 的字段正是我们所寻找的。然而,不仅仅只有一个 swap 字段,因为 smaps 文件包含了我们 KeyDB 进程的不同内存映射(一个进程的内存布局比一个简单的线性页面数组要复杂得多)。
因为我们对我们进程交换的所有内存感兴趣,所以第一件事就是在整个文件中 grep Swap 字段。
如果所有都是 0 kB,或者有零星的 4k 条目,那么一切都完全正常。实际上,在我们的示例实例中(一个运行 KeyDB 并每秒为数百名用户提供服务的真实网站),有几个条目显示了更多的交换页面。为了调查这是否是一个严重的问题,我们更改命令,以便同时打印内存映射的大小。
从输出中可以看出,有一个 720896 kB 的映射(只有 12 kB 被交换),另外还有 156 kB 在另一个映射中被交换:基本上,我们内存中被交换的量非常小,所以这根本不会造成任何问题。
如果进程的大量内存被交换到磁盘上,那么您的延迟问题很可能与交换有关。如果您的 KeyDB 实例出现这种情况,您可以使用 **vmstat** 命令进一步验证。
对我们来说,输出中有趣的部分是 **si** 和 **so** 这两列,它们统计了从/向交换文件交换的内存量。如果您在这两列中看到非零计数,那么您的系统中有交换活动。
最后,可以使用 **iostat** 命令来检查系统的全局 I/O 活动。
如果您的延迟问题是由于 KeyDB 内存被交换到磁盘上,您需要降低系统中的内存压力,要么增加更多的 RAM(如果 KeyDB 使用的内存超过了可用内存),要么避免在同一系统中运行其他消耗大量内存的进程。
#
由 AOF 和磁盘 I/O 引起的延迟另一个延迟来源是 KeyDB 上的仅追加文件(AOF)支持。AOF 基本上使用两个系统调用来完成其工作。一个是 write(2),用于将数据写入仅追加文件;另一个是 fdatasync(2),用于将内核文件缓冲区刷新到磁盘,以确保用户指定的持久性级别。
write(2) 和 fdatasync(2) 调用都可能是延迟的来源。例如,当系统范围内正在进行同步时,或者当输出缓冲区已满且内核需要刷新到磁盘以接受新的写入时,write(2) 可能会阻塞。
fdatasync(2) 调用是更糟糕的延迟源,因为在许多内核和文件系统的组合下,它可能需要几毫秒到几秒钟才能完成,尤其是在有其他进程正在进行 I/O 的情况下。因此,如果可能,KeyDB 会在不同的线程中执行 fdatasync(2) 调用。
我们将看到在使用 AOF 文件时,配置如何影响延迟的数量和来源。
AOF 可以通过 **appendfsync** 配置选项配置为以三种不同的方式在磁盘上执行 fsync(此设置可以在运行时使用 **CONFIG SET** 命令修改)。
当 appendfsync 设置为 **no** 时,KeyDB 不执行 fsync。在这种配置下,唯一的延迟源可能是 write(2)。当这种情况发生时,通常没有解决方案,因为磁盘根本无法跟上 KeyDB 接收数据的速度,但如果磁盘没有被其他进行 I/O 的进程严重拖慢,这种情况并不常见。
当 appendfsync 设置为 **everysec** 时,KeyDB 每秒执行一次 fsync。它使用一个不同的线程,如果 fsync 仍在进行中,KeyDB 会使用一个缓冲区将 write(2) 调用延迟最多两秒(因为在 Linux 上,如果对同一文件正在进行 fsync,write 会阻塞)。然而,如果 fsync 花费的时间太长,KeyDB 最终仍会执行 write(2) 调用,即使 fsync 仍在进行中,这可能成为延迟的来源。
当 appendfsync 设置为 **always** 时,每次写操作前都会执行一次 fsync,然后再向客户端回复 OK 代码(实际上,KeyDB 会尝试将同时执行的多个命令聚合成一次 fsync)。在这种模式下,性能通常非常低,强烈建议使用快速磁盘和能够短时间内完成 fsync 的文件系统实现。
大多数 KeyDB 用户会使用 **no** 或 **everysec** 设置来配置 appendfsync 指令。为了实现最低延迟,建议避免在同一系统中有其他进程进行 I/O。使用 SSD 磁盘也有帮助,但通常即使是非 SSD 磁盘,如果磁盘是空闲的,那么仅追加文件的性能也很好,因为 KeyDB 写入仅追加文件时不会执行任何寻道操作。
如果你想调查与仅追加文件相关的延迟问题,你可以在 Linux 下使用 strace 命令:
上述命令将显示 KeyDB 在主线程中执行的所有 fdatasync(2) 系统调用。使用上述命令,你将看不到当 appendfsync 配置选项设置为 **everysec** 时后台线程执行的 fdatasync 系统调用。要看到这些,只需向 strace 添加 -f 开关。
如果你愿意,你也可以用以下命令同时查看 fdatasync 和 write 系统调用:
然而,由于 write(2) 也用于向客户端套接字写入数据,这可能会显示太多与磁盘 I/O 无关的内容。显然,没有办法告诉 strace 只显示慢的系统调用,所以我使用以下命令:
#
由过期键产生的延迟KeyDB 通过两种方式驱逐过期的键:
- 一种是*懒惰*方式,当一个键被命令请求时,如果发现它已经过期,则将其过期。
- 一种是*主动*方式,每 100 毫秒过期一些键。
主动过期被设计为自适应的。一个过期周期每 100 毫秒(每秒 10 次)启动一次,并会执行以下操作:
- 抽样 `ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP` 个键,并驱逐所有已经过期的键。
- 如果发现超过 25% 的键已过期,则重复此过程。
鉴于 `ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP` 默认设置为 20,并且该过程每秒执行十次,通常每秒只有 200 个键被主动过期。即使已过期的键长时间未被访问,这也足以快速清理数据库,从而使*懒惰*算法无法提供帮助。同时,每秒仅过期 200 个键对 KeyDB 实例的延迟没有影响。
然而,该算法是自适应的,如果在抽样键集合中发现超过 25% 的键已经过期,它将循环。但鉴于我们每秒运行该算法十次,这意味着超过 25% 的键在我们随机样本中至少*在同一秒内*过期的不幸事件。
基本上,这意味着**如果数据库有大量在同一秒内过期的键,并且这些键至少占当前设置了过期时间的键总数的 25%**,KeyDB 可能会阻塞,以使已过期的键的百分比降至 25% 以下。
这种方法是必要的,以避免为已经过期的键使用过多内存,并且通常是完全无害的,因为大量键在同一秒内过期是很奇怪的,但用户并非不可能广泛使用具有相同 Unix 时间的 `EXPIREAT`。
简而言之:请注意,大量键在同一时刻过期可能是延迟的来源。
#
KeyDB 软件看门狗KeyDB 软件看门狗是一个调试工具,旨在追踪那些由于某种原因未能通过常规工具分析的延迟问题。
软件看门狗是一个实验性功能。虽然它被设计用于生产环境,但在继续之前应注意备份数据库,因为它可能会与 KeyDB 服务器的正常执行产生意外的交互。
重要的是,只有在无法通过其他方式追踪问题时才应将其用作*最后手段*。
这个功能的工作原理如下:
- 用户使用 `CONFIG SET` 命令启用软件看门狗。
- KeyDB 开始持续监控自身。
- 如果 KeyDB 检测到服务器被阻塞在某个操作中,该操作没有足够快地返回,并且可能是延迟问题的根源,那么一个关于服务器被阻塞位置的低级报告将被转储到日志文件中。
- 用户在 KeyDB Google Group 中写一条消息联系开发人员,并在消息中包含看门狗报告。
请注意,此功能不能使用 KeyDB.conf 文件启用,因为它被设计为仅在已运行的实例中启用,且仅用于调试目的。
要启用该功能,只需使用以下命令:
周期以毫秒为单位指定。在上面的例子中,我指定只有在服务器检测到 500 毫秒或更长的延迟时才记录延迟问题。最小可配置周期为 200 毫秒。
当您完成软件看门狗的使用后,可以通过将 `watchdog-period` 参数设置为 0 来关闭它。**重要提示:** 记住要这样做,因为让实例长时间开启看门狗通常不是一个好主意。
以下是当软件看门狗检测到比配置的延迟更长时,您将在日志文件中看到的打印内容示例:
注意:在示例中,使用了 **DEBUG SLEEP** 命令来阻塞服务器。如果服务器在不同的上下文中阻塞,堆栈跟踪会有所不同。