在过去的几个月里,我的 Linux 系统出现了一个非常恼人的问题:Firefox 音频播放、鼠标移动等都会卡顿,每隔几秒钟就会出现一小段时间(但仍然很明显)的卡顿。当内存缓存填满时,或者当我运行大量占用磁盘/内存的程序(例如备份软件restic
)时,这个问题会变得更糟。但是,当缓存未满时(例如在非常轻的负载下),一切都运行得非常顺畅。
通过查看perf top
输出,我发现list_lru_count_one
在这些滞后期间开销很高(约 20%)。htop
还显示kswapd0
使用 50-90% 的 CPU(尽管感觉影响远大于此)。在极端滞后期间,htop
CPU 计量表通常由内核 CPU 使用率主导。
我发现的唯一解决方法是强制内核保留可用内存(sysctl -w vm.min_free_kbytes=1024000
)或通过 持续删除内存缓存echo 3 > /proc/sys/vm/drop_caches
。当然,这两种方法都不是理想的,也不能完全解决卡顿问题;它只是使卡顿不那么频繁。
有谁知道为什么会发生这种情况?
系统信息
- 配备 20 GB(不匹配)DDR3 RAM 的 i7-4820k
- 在 NixOS 不稳定版 Linux 4.14-4.18 上重现
- 在后台运行 Docker 容器和 Kubernetes(我觉得这不应该产生微卡顿?)
我已经尝试过的方法
- 更改 I/O 调度程序 (bfq),使用多队列 I/O 调度程序
- 使用
-ck
Con Kolivas 的补丁集(没有帮助) - 禁用交换,更改交换性,使用 zram
编辑:为了清晰起见,下面是此类延迟峰值期间的图片。请注意高htop
CPU负载和高内核 CPU 使用率。perf
list_lru_count_one
kswapd0
答案1
听起来你已经尝试了我最初建议的很多事情(调整交换配置、更改 I/O 调度程序等)。
除了您已经尝试更改的内容之外,我建议您研究更改 VM 写回行为的有些愚蠢的默认值。这由以下六个 sysctl 值管理:
vm.dirty_ratio
:控制触发写回之前必须等待多少写入。处理前台(每个进程)写回,并以 RAM 的整数百分比表示。默认为 RAM 的 10%vm.dirty_background_ratio
:控制触发写回之前必须等待多少写入。处理后台(系统范围)写回,并以 RAM 的整数百分比表示。默认为 RAM 的 20%vm.dirty_bytes
:与 相同vm.dirty_ratio
,但以总字节数表示。这或者vm.dirty_ratio
将被使用,以最后写入的为准。vm.dirty_background_bytes
:与 相同vm.dirty_background_ratio
,但以总字节数表示。这或者vm.dirty_background_ratio
将被使用,以最后写入的为准。vm.dirty_expire_centisecs
:当上述四个 sysctl 值尚未触发挂起写回时,必须经过百分之几秒才能开始挂起写回。默认为 100(一秒)。vm.dirty_writeback_centisecs
:内核评估脏页是否写回的频率(以百分之一秒为单位)。默认值为 10(十分之一秒)。
因此,使用默认值,每十分之一秒,内核将执行以下操作:
- 如果最后修改时间是在一秒之前,则将任何已修改的页面写入持久存储中。
- 如果某个进程的未写出的已修改内存总量超过 RAM 的 10%,则写出该进程的所有已修改页面。
- 如果尚未写出的已修改内存总量超过 RAM 的 20%,则写出系统中所有已修改的页面。
因此,应该很容易理解为什么默认值可能会给你带来问题,因为你的系统可能会尝试写出最多 4千兆字节的数据永久存储第十一秒钟。
目前普遍的共识是调整vm.dirty_ratio
为 RAM 的 1% 和vm.dirty_background_ratio
2%,对于 RAM 小于 64GB 的系统,其行为与最初的预期相同。
其他一些值得关注的事项:
- 尝试
vm.vfs_cache_pressure
稍微增加 sysctl。这控制内核在需要 RAM 时从文件系统缓存中回收内存的积极程度。默认值为 100,不要将其降低到 50 以下(您将要如果低于 50,则会出现非常糟糕的行为,包括 OOM 情况),并且不要将其提高到超过 200(太高,内核将浪费时间尝试回收它实际上无法回收的内存)。我发现,如果您拥有相当快的存储,将其提高到 150 实际上可以明显提高响应能力。 - 尝试更改内存过量使用模式。这可以通过更改 sysctl 的值来实现
vm.overcommit_memory
。默认情况下,内核将使用启发式方法来尝试预测它实际上可以负担得起多少 RAM。将其设置为 1 会禁用启发式方法,并告诉内核表现得像它有无限内存一样。将其设置为 2 会告诉内核不要提交超过系统上交换空间总量加上实际 RAM 百分比(由 控制vm.overcommit_ratio
)的内存。 - 尝试调整
vm.page-cluster
sysctl。这可控制一次换入或换出的页面数(这是一个以 2 为底的对数值,因此默认值 3 表示 8 个页面)。如果您确实在进行交换,这可以帮助提高换入和换出页面的性能。
答案2
问题已经找到!
事实证明,当有大量容器/内存 cgroup 时,这是 Linux 内存回收器中的一个性能问题。(免责声明:我的解释可能有缺陷,我不是内核开发人员。)该问题已在 4.19-rc1+ 中修复此补丁集:
此补丁集解决了在具有许多收缩器和内存 cgroup(即具有许多容器)的机器上 shrink_slab() 速度缓慢的问题。问题是 shrink_slab() 的复杂度为 O(n^2),并且随着容器数量的增长而增长得太快。
假设我们有 200 个容器,每个容器有 10 个挂载点和 10 个 cgroup。所有容器任务都是隔离的,不会接触外部容器挂载点。
如果是全局回收,任务必须遍历所有 memcg,并调用所有 memcg 感知的收缩器。这意味着,任务必须为每个 memcg 访问 200 * 10 = 2000 个收缩器,由于有 2000 个 memcg,do_shrink_slab() 的总调用次数为 2000 * 2000 = 4000000。
由于我运行了大量容器,因此我的系统受到的打击特别严重,这很可能是导致问题出现的原因。
我的故障排除步骤,希望对遇到类似问题的人有帮助:
- 当我的电脑卡顿时,会注意到
kswapd0
CPU 占用率很高 - 尝试停止 Docker 容器并再次填充内存 → 计算机不再卡顿!
- 运行
ftrace
(以下Julia Evan 的精彩解释博客)来获取踪迹,请参见kswapd0
往往会卡在shrink_slab
、super_cache_count
和中list_lru_count_one
。 - 谷歌
shrink_slab lru slow
,找到补丁集! - 切换到 Linux 4.19-rc3 并验证问题是否已修复。