本文仅作为学习记录,非商业用途,侵删,如需转载需作者同意。

容器文件系统OverlayFS,有两层:

  • lowerdir:容器镜像中的文件,对于容器来说是只读的
  • upperdir:存放的是容器对文件系统里有所改动的,是可读写的

宿主机的角度看,upperdir就是一个目录。
容器不断往容器文件系统中写数据,实际上就是往宿主机的磁盘上写数据,这些数据也就是存放在宿主机的磁盘目录中。

大量的读写操作不建议写入容器文件系统中,一般给容器挂载一个volume ,用来满足大量的文件读写。

但是当配置错误时,如何避免容器中的程序把宿主机磁盘写满呢,避免影响整个宿主机的使用呢?

一、问题再现

启动一个容器,容器的根目录(/)也就是容器文件系统OverlayFS 的大小,
去宿主机上查看下,实际也是宿主机的磁盘使用情况。

容器里路径 容器文件_docker

容器里路径 容器文件_文件系统_02

在容器中写入10G的文件 test.log 发现容器和宿主机的磁盘使用都增加了,一模一样。

容器里路径 容器文件_容器里路径_03

查看下OverlayFS 对应宿主机上的 upperdir 目录中的文件情况:

容器里路径 容器文件_docker_04

查看宿主机上的挂载 /proc/mounts 中的overlay对应的upperdir和lowerdir 的路径。
因为upperdir是可读写的,写的数据都在upperdir里面。
上图显示,确实是有test.log 大小是10G。

这个例子验证了:容器中对容器文件系统OverlayFS写数据,其实就是往宿主机的一个目录(upperdir)里写数据。如果继续写,就会把宿主机写满。

二、知识详解

需求:限制容器的数据写入量,就是容器的磁盘空间大小

思路:OverlayFS没有直接限制文件写入量的特性。容器既然数据最终是写入到宿主机对应的upperdir中,那么宿主机上的文件系统是否可以支持对一个目录做容量限制呢?

Linux常用文件系统 XFS、ext4 有个Quota特性。

2.1、XFS Quota

Quota:可以为一个用户(user)、一个用户组(group)、一个项目(project)限制他们使用文件系统的额度(quota),就是限制他们写入文件系统的文件数量。

我们的需求是限制目录,多个用户多个用户组可以操作多个目录,这个不符合我们的需求。

下面介绍下project模式:

使用XFS Quota,必须在文件系统挂载的时候加上对应的Quota选项。
比如配置project Quota ,挂载参数就是"puota"。

对于根目录来说,这个参数必须作为一个内核启动的参数"rootflags=pquota",这样设置就可以保障根目录在启动挂载的时候,带上XFS Quota的特性并且支持Project模式。

如下图 /proc/mounts 信息里:则表示文件系统已经带上了支持project模式的XFS quota特性。

容器里路径 容器文件_docker_05

下一步,给指定的目录打上一个Project ID。
可以使用XFS 文件系统自带的的工具 xfs_quota 来完成,执行下面的命令就可以了。

# mkdir -p  /tmp/xfs_prjquota
# xfs_quota -x -c 'project -s -p /tmp/xfs_prjquota 101' /
Setting up project 101 (path /tmp/xfs_prjquota)...
Processed 1 (/etc/projects and cmdline) paths for project 101 with recursion depth infinite (-1).

上面命令解释:
1、新建的目录 /tmp/xfs_prjquota ,我们想对它做Quota限制,所以要在这里对它打上一个Project ID。
2、使用xfs_quota 命令给 /tmp/xfs_prjquota 打上 Project ID 值101,这个101是随意的一个数字,只是一个ID 标识。后面针对Project 进行Quota限制的时候,还会用到这个ID。

然后使用xfs_quota 对101(刚才新建的Project ID) 做Quota限制。

# xfs_quota -x -c 'limit -p bhard=10m 101' /

上面命令表示:限制101 这个project ID,限制它的数据库写入量不能超过10MB。

测试结果如下:

dd命令尝试写入20MB的数据,dd写入命令有报错,表示不能再往这个目录下写入数据了,最后写入数据的文件大小也停留在10MB。

# dd if=/dev/zero of=/tmp/xfs_prjquota/test.file bs=1024 count=20000
dd: error writing '/tmp/xfs_prjquota/test.file': No space left on device
10241+0 records in
10240+0 records out
10485760 bytes (10 MB, 10 MiB) copied, 0.0357122 s, 294 MB/s

# ls -l /tmp/xfs_prjquota/test.file
-rw-r--r-- 1 root root 10485760 Oct 31 10:00 /tmp/xfs_prjquota/test.file

如上测试结果,使用 XFS Quota 的Project 模式,确实可以限制一个目录里的写入数据量,实现的方式就是下面两步:

  • 给目标目录打上一个Project ID,这个ID 最终是写到目录对应的inode上。inode是文件系统中用来描述一个文件或者一个目录的元数据,里面包含文件大小,数据库的位置,文件所属用户/组,文件读写属性以及其他一些属性。
    目录打上ID后,这个目录下新建的目录和文件也会继承这个ID。
  • XFS文件系统中,需要给这个project ID 设置一个写入数据库的限制。

有了ID 和 限制值之后,文件系统就会统计所有带这个ID文件的数据块大小总和,并且与限制值进行比较。一旦所有文件大小的总和达到限制值,文件系统就不再允许更多的数据写入了。

XFS Quota 就是通过前面这两步限制一个目录里写入的数据量。

三、解决问题

解决办法就是:对OverlayFS的upperdir目录做XFS Quota的限流

docker也就是用XFS Quota 来限制容器的OverlayFS的大小。

dcoker run 启动容器的时候,添加参数 --storage-opt size= 就能限制住容器OverlayFS 文件系统可写入的最大数据量。

可以自己测试验证下。


Docker 里面的SetQuota()函数就是用来实现 XFS Quota 限制的,里面最重要的两步分别是:setProjectID 和 setProjectQuota。
其实这两步就是基本概念中提到的两步。

1、给目录打上ProjectID
2、为这个ProjectID 在XFS文件系统中,设置一个写入数据块的限制。

// SetQuota - assign a unique project id to directory and set the quota limits
// for that project id

func (q *Control) SetQuota(targetPath string, quota Quota) error {
        q.RLock()
        projectID, ok := q.quotas[targetPath]
        q.RUnlock()

        if !ok {
                q.Lock()
                projectID = q.nextProjectID

                //
                // assign project id to new container directory
                //

                err := setProjectID(targetPath, projectID)
                if err != nil {
                        q.Unlock()
                        return err
                }

                q.quotas[targetPath] = projectID
                q.nextProjectID++
                q.Unlock()
        }

 

        //
        // set the quota limit for the container's project id
        //

        logrus.Debugf("SetQuota(%s, %d): projectID=%d", targetPath, quota.Size, projectID)
        return setProjectQuota(q.backingFsBlockDev, projectID, quota)
}

docker中的 setProjectID 和setProjectQuota 是如何实现的?

它们分别调用了ioctl()和quotactl()这两个系统调用来修改内核中XFS的数据结构,从而完成project ID的设置和Quota值的设置

容器里路径 容器文件_数据_06

通过上面的图片可以看出来,upperdir目录对应宿主机上的目录就是:
/var/lib/docker/overlay2/<docker_id>/diff

对这个目录做磁盘空间大小的限制,就可以限制对应容器的磁盘空间大小了。

四、重点总结

问题是:容器写了大量数据到overlayFS文件系统的根目录,在这个情况下会把宿主机的磁盘空间写满。

由于overlayFS 没有专门的特性,可以限制文件数据写入量。
解决的思路是:依靠底层文件系统的Quota 特性来限制OverlayFS的upperdir目录大小,就能达到限制容器写磁盘的目的。

底层文件系统 XFS Quota 的 Project 模式,能够限制一个目录的文件写入量,这个功能具体是通过这两个步骤实现:
第一步,给目标目录打上一个 Project ID。
第二步,给这个 Project ID 在 XFS 文件系统中设置一个写入数据块的限制。

Docker 正是使用了这个方法,也就是用 XFS Quota 来限制 OverlayFS 的 upperdir 目录,通过这个方式控制容器 OverlayFS 的根目录大小。

当我们理解了这个方法后,对于不是用 Docker 启动的容器,比如直接由 containerd 启动起来的容器。

也可以自己实现 XFS Quota 限制 upperdir 目录。这样就能有效控制容器对 OverlayFS 的写数据操作,避免宿主机的磁盘被写满。

五、评论

1、
问题:
上篇的评论中提到:“我们在2019年初就不用docker了。”
这篇中,老师提到了containerd,是说你们已经用containerd替换了docker吗?有机会对containerd和docker之间的使用对比做个介绍吗?

回答:
对的,我们已经使用containerd快两年了。
这是之前我们组做的分享:
*mbrbk

2、
问题:
老师,我觉得可以补充下 现在k8s 1.14 开始,默认开启 LocalStorageCapacityIsolation, 可以通过限制resources.limits.ephemeral-storage 和resources.requests.ephemeral-storage 来保护宿主机 rootfs了。

回答:
k8s 通过du来检查ephemeral-storage相关volume/目录的大小,然后如果超出limit就evict pod。
du的开销会比较大,evict pod比较适合stateless pod。不过最新的k8s应该在用filesystem quota来限制emptyDir的大小了。

3、
问题:
老师能说下你们应用容器Quota的场景么。

回答:
如果用户需要使用大容量的磁盘空间,需要使用volume.
Quota主要来限制容器的rootfs, 这个rootfs一般是在host的磁盘会和别的容器共享,所以需要对它做限制。

4、
问题:
老师,CGroup子系统中没有对硬盘使用量进行限制的功能模块吗?除了通过文件系统的quota去限制还有其他的方法吗?

回答:
cgroup里没有对磁盘容量使用限制的模块。