问题描述
我正在尝试在 docker 中运行一个长时间运行的命令,我发现在命令执行期间内存消耗增加。
我正在 Laravel 应用程序的上下文中使用 PHPUnit 运行由 741 个测试和 3390 个断言组成的测试套件。大多数这些测试是集成测试,它们使用 docker-compose 中的数据库连接、redis 和 s3 服务。我正在使用此测试运行生成代码覆盖率报告。这是我运行的确切命令
docker-compose run PHPunit --verbose --testdox --colors=never --stop-on-error --stop-on-failure --coverage-clover coverage.xml
我尝试了很多方法来查看内存泄漏的位置(是 Docker、PHP 还是 PHPUnit?)。以下是我用他们的结果设置的内存报告:
- PHPUnit 默认在运行结束时报告他的内存使用情况。 -> 它总是在 80-100MB 左右,这很好。
- 我使用了
teardown
函数并打印了memory_get_peak_usage
。 -> 它与之前的 80-100 非常相似,并且在后来的测试中略微增加到 120MB。看起来不错 - 我在
passthru('ps -o pid,user,%mem,command ax | sort -b -k3 -r | head')
函数中添加了teardown
。 -> 我可以看到我的内存消耗以非常快的速度增长。我认为它与第 4 行的百分比相同,因此推断它在运行结束时接近 6.5G。唯一使用 RAM 的进程是PHPunit
。 - 从我的主机,我会运行
docker stats --format "table {{.Name}}\t{{.MemPerc}} - {{.MemUsage}}\t{{.BlockIO}}"
-> 同样的事情,在运行结束时内存增加到 6.5 GB 的 RAM。唯一一个内存使用量增加的容器是运行 PHPunit 的容器。来自 docker-compose 的其他容器没有移动(数据库、redis、s3 等)。 - 在我的主机上,我会运行
watch -n 1 free -m
来报告我整个计算机上的 RAM 使用情况 -> 同样的事情,在运行结束时内存增加到 6.5 GB 的 RAM。命令完成后,6.5 GB 的 RAM 立即释放。
我的问题与这个问题非常相似:Docker does not free memory after creating and deleting files with PHP。我已经尝试了提出的解决方案。我在 echo 3 > /proc/sys/vm/drop_caches
模式下运行 sync && sysctl -w vm.drop_caches=3
和 privileged
。内存使用量没有什么不同,但我可以看到 BlockIO 现在以与没有这些命令的情况下的内存使用量相同的速度增加。
2021 年 4 月 28 日更新:
它最终不是 Docker 的问题,而是 PHPUnit 的问题。当使用 processIsolation=false
运行测试时,PHPUnit 似乎正在泄漏。我的理解是 processIsolation=true
,为每个测试用例启动一个新进程。因此,每次测试之间都会释放内存,但执行起来需要更多时间。
processIsolation=false
:快速但内存泄漏
processIsolation=true
:缓慢但泄漏的内存在测试之间释放
解决方法
Docker 进程不是在使用内存,而是在报告使用情况的 docker 进程。内存由容器中的进程和内核模块使用。根据问题中的信息,我没有看到您有问题,而是您对内核管理内存的方式感到困惑,因此我将尝试在非常高的层次上解释发生了什么。
内核命名空间
现代内核允许创建分区(命名空间),其中分配给该分区的进程可以半隔离工作(与用户空间中的所有其他进程隔离,但不与所有内核子系统/模块隔离)。 Docker 正在使用内核的这个特性来创建容器。
容器在所谓的 kernel namespace
中运行。内核命名空间与运行 docker 应用程序的 docker 内存空间几乎没有关系。 docker 应用程序只负责命名空间的编排(创建新的命名空间、为命名空间分配容器、设置资源限制、配置网络基础设施等),因此 Docker 在创建 kernel namespace
后不参与如果进一步的内存管理。
所以当你启动一个容器时,Docker 会为容器创建一个新的命名空间,准备所有资源限制,准备网络并启动该命名空间中的进程。我特意为这次对话简化了容器的创建,所以如果你需要更多信息,你可以查看这个帖子:
- Understanding the Docker Internals
- Build Containers From Scratch in Go (Part 1: Namespaces)
- Demystifying Containers - Part I: Kernel Space
缓冲区和缓存真的是个问题
内核将尽可能多地使用“未使用的内存”作为缓存。这很有道理。未使用的内存处于空闲状态,因此将其用作缓存来加速进程是一个非常好的策略。这有助于对 I/O 绑定的进程产生巨大影响(主要是当进程有大量 IO 读取操作时)。文章 Linux Page Cache Basics 提供了很好的文本作为了解有关内核缓存的更多信息的切入点。
内核倾向于将“空闲”内存重新用于缓存和缓冲区。如前所述,这是一个非常好的功能,可以帮助所有进程运行得更快。一旦命名空间中的内存压力开始增加,这意味着在命名空间中运行的进程需要更多内存,内核将通过查找最少使用的区域并释放它们来开始缩小缓存,以便将它们分配给进程。这在内核 documentation 中进行了描述。
除非有内存压力,否则内核不会急于释放缓存。所以除非需要内存,一旦分配给缓存,内核就会保留它。从内核的角度来看,这也是一个非常好的策略,因为它避免了释放缓存和将来可能重新填充缓存的额外工作。
因此,除非我们谈论内核或内核模块中的缺陷,一般来说,缓冲区/缓存使用的内存是一件好事,而不是问题。。 >
通过使用内核文档部分中解释的 sync && echo 3 > /proc/sys/vm/drop_caches
Documentation for /proc/sys/vm/,系统被迫丢弃尽可能多的缓存对象,这将导致 I/O 性能问题,直到重新填充缓存.这对于调试和分析很有用,但在正常生产中不是一个好的做法。清除缓存后,内核将需要重新填充(因为还有其他进程确实需要此缓存),这会导致额外的 IO。
管理内存
Docker 提供了管理容器如何使用内存以及与容器相关的限制的机制。您可以在 docker 的文档 Runtime options with Memory,CPUs,and GPUs
中查看更多信息除非有特殊用例需要对内存进行特殊处理,否则 Docker 提供的资源管理功能足以确保容器的平稳运行。
内存故障排除
有时我们可以通过内核/内核模块或容器中的应用程序来处理内存泄漏问题。
故障排除的第一步是确定谁真正在使用内存以及它是如何增长的。为了识别有问题的代码,我们需要对内存分配进行故障排除,并了解是否存在不断增长且在达到内存压力时未释放的区域。除了 free
和 ps
之外,有关内存生命周期的更好信息可以在 sysfs
上找到。
sysfs
将内核内部公开为用于读取和发送信号的文件系统。 /proc/sys/vm
也是称为 procfs
的同一机制的一部分。 /sys/fs/cgroup/memory
是存放内存分配信息和统计信息的文件夹。 Docker 创建了自己的 cgroup
,称为 Docker
,因此所有 Docker 容器的统计信息都可以在 /sys/fs/cgroup/memory/docker/
中找到。此外,docker 将为 cgroup
下的每个容器创建一个 /sys/fs/cgroup/memory/docker/<<container id>>
。
要查找有关容器内存使用情况的内核统计信息,您可以按如下方式检查 sysfs
中的文件:
-
/sys/fs/cgroup/memory/docker/<<container id>>/memory.stat
关于内存使用情况的统计信息(类似于free
,但有更多详细信息 -
/sys/fs/cgroup/memory/docker/<<container id>>/memory.limit_in_bytes
当前为容器设置的字节数限制 -
/sys/fs/cgroup/memory/docker/<<container id>>/memory.kmem.slabinfo
内核内存区域以及缓存/缓冲区对象的使用情况