Docker 内存限制导致 SLUB 无法分配大页面缓存
给定一个通过 mmap'd 文件创建大型 Linux 内核页面缓存的进程,在具有内存限制的 docker 容器 (cgroup) 中运行会导致内核板分配错误:
Jul 18 21:29:01 ip-10-10-17-135 kernel: [186998.252395] SLUB: Unable to allocate memory on node -1 (gfp=0x2080020)
Jul 18 21:29:01 ip-10-10-17-135 kernel: [186998.252402] cache: kmalloc-2048(2412:6c2c4ef2026a77599d279450517cb061545fa963ff9faab731daab2a1f672915), object size: 2048, buffer size: 2048, default order: 3, min order: 0
Jul 18 21:29:01 ip-10-10-17-135 kernel: [186998.252407] node 0: slabs: 135, objs: 1950, free: 64
Jul 18 21:29:01 ip-10-10-17-135 kernel: [186998.252409] node 1: slabs: 130, objs: 1716, free: 0
观看我可以看到buffer_head的数量,radix_tree_node和kmalloc*对象在以内存限制开始的容器中受到严格限制。这似乎对应用程序中的 IO 吞吐量产生了病理后果,并且可以使用 观察到。即使页面缓存占用了在容器或无内存限制的容器外部运行的主机操作系统上的所有可用内存,也不会发生这种情况。slabtop
iostat
这似乎是内核内存记帐中的一个问题,其中内核页面缓存不计入容器内存,但支持它的 SLAB 对象才算作。该行为似乎是异常的,因为在预分配大型楼板对象池时运行,内存约束容器可以正常工作,可以自由重用现有的楼板空间。只有容器中分配的楼板才会对容器进行计数。内存和内核内存的容器选项组合似乎无法解决此问题(除了根本没有设置内存限制或限制如此之大,以至于不限制板,但这限制了可寻址空间)。我试图完全禁用kmem会计,但没有成功,通过启动。cgroup.memory=nokmem
系统信息:
- Linux ip-10-10-17-135 4.4.0-1087-aws #98-Ubuntu SMP
- AMI ubuntu/images/hvm-ssd/ubuntu-xenial-16.04-amd64-server-20190204.3
- Docker 版本 18.09.3,内部版本 774a1f4
- java 10.0.1 2018-04-17
要重现问题,您可以使用我的PageCache java代码。这是嵌入式数据库库的一个基本重现案例,它大量利用内存映射文件部署在非常快的文件系统上。该应用程序通过 ECS 部署在 AWS i3.baremetal 实例上。我正在将大量卷从主机映射到存储内存映射文件的 docker 容器。AWS ECS 代理需要为所有容器设置非零内存限制。内存限制会导致病态的平板行为,并且生成的应用程序 IO 吞吐量是完全不可接受的。
在运行之间使用 会很有帮助。这将清除页面缓存和关联的 slab 对象池。drop_caches
echo 3 > /proc/sys/vm/drop_caches
欢迎就如何修复,解决甚至报告此问题的位置提出建议。
更新似乎使用4.15内核更新到Ubuntu 18.04确实修复了观察到的kmalloc分配错误。Java的版本似乎无关紧要。这似乎是因为每个 v1 CGroup 只能分配到内存限制的页面缓存(对于多个 cgroup,则更复杂,只有一个 cgroup 通过共享页面记帐方案进行分配“ )。我相信这现在与预期的行为是一致的。在 4.4 内核中,我们发现观察到的 kmalloc 错误是在具有内存限制和非常大的页面缓存的 v1 Cgroup 中使用软件 raid0 的交集。我相信4.4内核中的cgroups能够映射无限数量的页面(我们发现有用的错误),直到内核耗尽相关slab对象的内存,但我仍然没有吸烟枪。
对于 4.15 内核,我们的 Docker 容器需要设置内存限制(通过 AWS ECS),因此我们实施了一个任务,以便在 中创建容器后立即取消设置内存限制。这似乎有效,尽管确定这不是一个好的做法。这允许我们想要的行为 - 在主机上无限制地共享页面缓存资源。由于我们运行的是具有固定堆的 JVM 应用程序,因此下行风险是有限的。/sys/fs/cgroup/memory/docker/{contarainer_id}/memory.limit_in_bytes
对于我们的用例,如果能够选择完全为 cgroup 打折页面缓存(mmap'd 磁盘空间)和关联的 slab 对象,但对于 docker 进程保持堆和堆栈的限制,那将是非常棒的。目前的共享页面记帐方案很难推理,我们更愿意允许 LRU 页面缓存(以及关联的 SLAB 资源)使用主机内存的完整范围,就像根本没有设置内存限制时的情况一样。
我已经开始关注LWN上的一些对话,但我有点在黑暗中。也许这是一个可怕的想法?我不知道。。。欢迎就如何继续或下一步去哪里提出建议。