Docker容器中大文件操作引发OOM的坑

问题引出

Docker容器已进化成为技术开发者必须掌握的技能之一,容器级别最关键的几项技术:cgroup资源限制、多命名空间隔离、标准化镜像。今天我们要分析的问题就是来源于cgroup对于容器内存的统计与限制。

由于业务的需要,我们需要进行大量的大文件进行zip打包和压缩。功能实现后上线开始测试,问题来了,容器内存、CPU飞涨,导致宿主机负载直线上升。立即top查看了下进程情况,很奇怪,当前服务进程内存使用量并不大。那么为什么容器监控显示内存100%占用呢?

问题分析

我们的服务采用golang语言实现,并使用golang官方zip包定制实现了携带文件权限的zip压缩和解压方案(不跨平台)。出现内存泄漏首先怀疑是否代码写得有问题,于是乎pprof一查,奇怪,内存分配并不大。golang服务没有产生内存泄漏。容器中只有这一个进程,top也显示进程使用内存很少。那么容器内存占用为什么就100%呢?

带着这个问题回到容器cgroup原理上,回想了一下想到了不同点,cgroup统计的内存会比top RES统计的内存多一部分,是什么呢 Page cache.

宿主机上 free -m一看究竟,果然 buff/cache 部分占用非常大。尝试手动释放一下内核的 Page cache

sync; echo 1 > /proc/sys/vm/drop_caches

容器内存统计马上恢复正常,问题基本定位。那么这个问题如何产生的呢? 那又该如何解决呢?

首先我们回顾一下操作系统读写文件的呢,这里不得不提到IO模型:

“DIRECT IO”的图片搜索ç"“æžœ

引用一下上图,最常用的IO模型是Buffered IO 它会经过Page Cache层进行缓存,加快用户进程的读写速度。Page Cache不会立即清理,系统内核在内存资源紧张时进行清理工作。以前我们没有使用容器技术的时候可能忽略了Page Cache的管理和维护。回到上文当我们在容器内进行大文件操作的时候是否一定会产生这个问题呢,我们做下列实验:

  1. 容器中cp一个大文件
  2. 容器中tar 打包一个大文件

实验证明,普通的IO模型一定会导致容器内存占用统计持续增加,如何达到了cgroup的limit限制值,就会导致频繁的GC或者直接被OOM kill。

问题如何解决

虽然Page cache可以通过手动的方式释放掉,但是我们的服务需要持续的进行大文件打包和解压操作。显然上诉的方式是不可行的。对于我们操作的文件对象,我们并不需要多次读写,也就意味着我们不需要内核帮我们进行缓存,回到上面的图,我们发现 Direct IO可以直接绕过Pagecache读写文件。如果我们才用直接IO,有什么弊端呢?首先的弊端就是文件读写速度相对降低,取决于磁盘性能,这对于服务稳定性来说可以忽略不计。其次就是对磁盘的读写频率相对变大,这也是可以忍受的。所以马上动手修改代码:

zipfile, err := directio.OpenFile(target, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
if err != nil {
return err
}
defer zipfile.Close()

golang语言中用上诉方式打开文件IO即可使用Direct IO 绕过Pagecache,编译打包测试,果然效果非常好。

那我们是否所有操作的文件都要使用直接IO呢,肯定不是的。对于需要多次读写、数据量较小的文件肯定还是使用缓存IO的。

总结

容器cgroup机制统计了pagecache的内存占用,但是对于用户进程来说对page cache是没有清理权的。大量文件读取以后必定造成OOM,这个坑我个人认为是比较恶心的。