限制Pod磁盘空间用量
容器在运行期间会产生临时文件、日志。如果没有任何配额机制,则某些容器可能很快将磁盘写满,影响宿主机内核和所有应用。
容器的临时存储,例如emptyDir,位于目录/var/lib/kubelet/pods下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
/var/lib/kubelet/pods/ └── ac0810f5-a1ce-11ea-9caf-00e04c687e45 # POD_ID ├── containers │ ├── istio-init │ │ └── 32390fd7 │ ├── istio-proxy │ │ └── 70ed81da │ └── zookeeper │ └── e9e21e59 ├── etc-hosts # 命名空间的Host文件 └── volumes # Pod的卷 ├── kubernetes.io~configmap # ConfigMap类型的卷 │ └── istiod-ca-cert │ └── root-cert.pem -> ..data/root-cert.pem ├── kubernetes.io~downward-api │ └── istio-podinfo │ ├── annotations -> ..data/annotations │ └── labels -> ..data/labels ├── kubernetes.io~empty-dir # Empty类型的卷 │ ├── istio-data │ └── istio-envoy │ ├── envoy-rev0.json │ └── SDS ├── kubernetes.io~rbd # RBD卷 │ └── pvc-644a7e30-845e-11ea-a4e1-70e24c686d29 # /dev/rbd0挂载到这个挂载点 ├── kubernetes.io~csi # CSI卷 └── kubernetes.io~secret # Secret类型的卷 └── default-token-jp4n8 ├── ca.crt -> ..data/ca.crt ├── namespace -> ..data/namespace └── token -> ..data/token |
持久卷的挂载点也位于/var/lib/kubelet/pods下,但是不会导致存储空间的消耗。
容器的日志,存放在/var/log/pods目录下。
使用Docker时,容器的rootfs位于/var/lib/docker下,具体位置取决于存储驱动。
具体细节参考:/kubernetes-study-note#out-of-resource。
当不可压缩资源(内存、磁盘)不足时,节点上的Kubelet会尝试驱逐掉某些Pod,以释放资源,防止整个系统受到影响。
其中,磁盘资源不足的信号来源有两个:
- imagefs:容器运行时用作存储镜像、可写层的文件系统
- nodefs:Kubelet用作卷、守护进程日志的文件系统
当imagefs用量到达驱逐阈值,Kubelet会删除所有未使用的镜像,释放空间。
当nodefs用量到达阈值,Kubelet会选择性的驱逐Pod(及其容器)来释放空间。
较新版本的K8S支持设置每个Pod可以使用的临时存储的request/limit,驱逐行为可以更具有针对性。
如果Pod使用了超过限制的本地临时存储,Kubelet将设置驱逐信号,触发Pod驱逐流程:
- 对于容器级别的隔离,如果一个容器的可写层、日志占用磁盘超过限制,则Kubelet标记Pod为待驱逐
- 对于Pod级别的隔离,Pod总用量限制,是每个容器限制之和。如果各容器用量之和+Pod的emptyDir卷超过Pod总用量限制,标记Pod为待驱逐
从K8S 1.8开始,支持本地临时存储(local ephemeral storage),ephemeral的意思是,数据的持久性(durability)不做保证。临时存储可能Backed by 本地Attach的可写设备,或者内存。
Pod可以使用本地临时存储来作为暂存空间,或者存放缓存、日志。Kubelet可以利用本地临时存储,将emptyDir卷挂载给容器。Kubelet也使用本地临时存储来保存节点级别的容器日志、容器镜像、容器的可写层。
Kubelet会将日志写入到你配置好的日志目录,默认 /var/log。其它文件默认都写入到 /var/lib/kubelet。在典型情况下,这两个目录可能都位于宿主机的rootfs之下。
Kubernetes支持跟踪、保留/限制Pod能够使用的本地临时存储的总量。
打开特性开关: LocalStorageCapacityIsolation,可以限制每个Pod能够使用的临时存储的总量。
注意:以内存为媒介(tmpfs)的emptyDir,其用量计入容器内存消耗,而非本地临时存储消耗。
使用类似限制内存、CPU用量的方式,限制本地临时存储用量:
1 2 |
spec.containers[].resources.limits.ephemeral-storage spec.containers[].resources.requests.ephemeral-storage |
单位可以是E, P, T, G, M, K,或者Ei, Pi, Ti, Gi, Mi, Ki(1024)。
下面这个例子,Pod具有两个容器,每个容器最多使用4GiB的本地临时存储:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
apiVersion: v1 kind: Pod metadata: name: frontend spec: containers: - name: db image: mysql env: - name: MYSQL_ROOT_PASSWORD value: "password" resources: requests: ephemeral-storage: "2Gi" limits: ephemeral-storage: "4Gi" - name: wp image: wordpress resources: requests: ephemeral-storage: "2Gi" limits: ephemeral-storage: "4Gi" |
如果禁用Kubelet对本地临时存储的监控,则Pod超过limit限制后不会被驱逐。但是,如果磁盘整体上容量太低,节点会被打上污点,所有不能容忍此污点的Pod都会被驱逐。
Kubelet可以执行周期性的扫描,检查emptyDir卷、容器日志目录、可写容器层,然后计算Pod/容器使用了多少磁盘。
这个模式下有个问题需要注意,Kubelet不会跟踪已删除文件的描述符。也就是说,如果你创建一个文件,打开文件,写入1GB,然后删除文件,这种情况下inode仍然存在(直到你关闭文件),空间仍然被占用,但是Kubelet却没有算这1GB.
此特性在1.15+处于Alpha状态。
Project quotas是Linux操作系统级别的特性,用于在目录级别限制磁盘用量。只有本地临时存储(例如emptyDir)的后备(Backing)文件系统支持Project quotas,才可以使用该特性。XFS、ext4都支持Project quotas。
K8S将占用从1048576开始的Project ID,占用中的ID注册在/etc/projects、/etc/projid文件中。如果系统中其它进程占用Project ID,则也必须在这两个文件中注册,这样K8S才会改用其它ID。
Quotas比周期性扫描快,而且更加精准。当一个目录被分配到一个Project中后,该目录中创建的任何文件,都是在Project中创建的。为了统计用量,内核只需要跟踪Project中创建了多少block就可以了。
如果文件被创建、然后删除,但是它的文件描述符仍然处于打开状态,这种情况下,它仍然消耗空间,不会出现周期性扫描的那种漏统计的问题。
要启用Project Quotas,你需要:
- 开启Kubelet特性开关: LocalStorageCapacityIsolationFSQuotaMonitoring
- 确保文件系统支持Project quotas:
- XFS文件系统默认支持,不需要操作
- ext4文件系统,你需要在未挂载之前,启用:
1sudo tune2fs -O project -Q prjquota /dev/vda
- 确保文件系统挂载时,启用了Project quotas。使用挂载选项 prjquota
有的时候,我们会发现磁盘写入时会报磁盘满,但是 df查看容量并没有100%使用,此时可能只是因为inode耗尽造成的。
当前k8s并不支持对Pod的临时存储设置inode的limits/requests。
但是,如果node进入了inode紧缺的状态,kubelet会将node设置为 under pressure,不再接收新的Pod请求。
Docker提供了配置项 --storage-opt,可以限制容器占用磁盘空间的大小,此大小影响镜像和容器文件系统,默认10G。
你也可以在/etc/docker/daemon.json中修改此配置项:
1 2 3 4 5 6 7 8 9 |
{ "storage-driver": "devicemapper", "storage-opts": [ // devicemapper "dm.basesize=20G", // overlay2 "overlay2.size=20G", ] } |
但是这种配置无法影响那些挂载的卷,例如emptyDir。
你可以使用Linux系统提供的任何能够限制磁盘用量的机制,为了和K8S对接,需要开发Flexvolume或CSI驱动。
前文已经介绍过,K8S目前支持基于Project quotas来统计Pod的磁盘用量。这里简单总结一下Linux磁盘配额机制。
Linux系统支持以下几种角度的配额:
- 在文件系统级别,限制群组能够使用的最大磁盘额度
- 在文件系统级别,限制单个用户能够使用的最大磁盘额度
- 限制某个目录(directory, project)能够占用的最大磁盘额度
前面2种配额,现代Linux都支持,不需要前提条件。你甚至可以在一个虚拟的文件系统上进行配额:
1 2 3 4 5 6 7 8 |
# 写一个空白文件 dd if=/dev/zero of=/path/to/the/file bs=4096 count=4096 # 格式化 ... # 挂载为虚拟文件系统 mount -o loop,rw,usrquota,grpquota /path/to/the/file /path/of/mount/point # 进行配额设置... |
第3种需要较新的文件系统,例如XFS、ext4fs。
配额可以针对Block用量进行,也可以针对inode用量进行。
配额可以具有软限制、硬限制。超过软限制后,仍然可以正常使用,但是登陆后会收到警告,在grace time倒计时完毕之前,用量低于软限制后,一切恢复正常。如果grace time到期仍然没做清理,则无法创建新文件。
启用配额,内核自然需要统计用量。管理员要查询用量,可以使用 xfs_quota这样的命令,比du这种遍历文件计算的方式要快得多。
在保证底层文件系统支持之后,你需要修改挂载选项来启用配额:
- uquota/usrquota/quota:针对用户设置配额
- gquota/grpquota:针对群组设置配额
- pquota/prjquota:针对目录设置配额
使用LVM你可以任意创建具有尺寸限制的逻辑卷,把这些逻辑卷挂载给Pod即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
volumes: - flexVolume: # 编写的flexVolume驱动放到 # /usr/libexec/kubernetes/kubelet-plugins/volume/exec/kubernetes.io~lvm/lvm driver: kubernetes.io/lvm fsType: ext4 options: size: 30Gi volumegroup: docker name: mnt volumeMounts: - mountPath: /mnt name: mnt |
这需要修改编排方式,不使用emptyDir这种本地临时存储,还需要处理好逻辑卷清理工作。
Flexvolume驱动的示例可以参考:/flexvolume-study-note#lvm。
使用Device Mapper也可以创建具有尺寸限制的卷,比起LVM的优势是thin-provisioning,不必预先分配空间。
DM是从2.6引入内核的通用设备映射机制,LVM就是基于DM的。DM为块设备驱动提供了模块化的内核架构。
术语 | 说明 |
Mapped Device |
由内核映射出的、逻辑的设备,它和Target Device的关系,由Mapping Table维护 Mapped Device可以:
|
Mapping Table | 包含字段:Mapped Device的逻辑起始地址、范围、关联Target Device所在物理设备的地址偏移量(以扇区,512字节为单位)、Target类型,等等 |
Target Device | Mapped Device所映射的物理空间段 |
Target Driver |
在内核中,DM通过模块化的Target Driver插件,实现对IO请求的过滤、重定向等操作。插件的实现包括:软Raid、加密、镜像、快照、Thin Provisioning |
使用dmsetup命令,你可以创建、删除虚拟卷,具体参考文章:Linux命令知识集锦。
下面的例子,以文件为后备,创建虚拟块设备(loopback),并以此块设备为基础,创建thin-provisioning池。然后,在池中分配限制大小的卷:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
# 创建空白文件 # 声明大小10G,实际占用空间(seek)4K dd if=/dev/zero of=/tmp/data.img bs=1K count=1 seek=10M # 声明大小100M dd if=/dev/zero of=/tmp/meta.img bs=1K count=1 seek=100K # 映射为虚拟(loopback)块设备,实际场景中,你可以考虑将整块磁盘交由DM管理 losetup /dev/loop10 /tmp/meta.img losetup /dev/loop11 /tmp/data.img # 基于上述块设备,创建一个mapped device(thin-provisioning池) dmsetup create thinpool0 \ # 数据设备起始扇区 # 数据设备结束扇区 * 512 = 10G # 元数据设备 # 数据设备 --table "0 20971522 thin-pool /dev/loop10 /dev/loop11 \ # 最小可以分配的扇区数 # 最少可用的扇区阈值 # 有1个附加参数 # 附加参数,跳过用0填充的块 128 65536 1 skip_block_zeroing" # 设备 /dev/mapper/thinpool0 现在可用 # 在thinpool0上创建一个thin-provisioning卷 # 标识符 dmsetup message thinpool0 0 "create_thin 0" dmsetup create thinvol0 --table "0 2097152 thin /dev/mapper/thinpool0 0" # 设备 /dev/mapper/thinvol0 现在可用 # 格式化 mkfs.ext4 /dev/mapper/thinvol0 # 挂载 mkdir /tmp/thinvol mount /dev/mapper/thinvol0 /tmp/thinvol |
- https://blog.spider.im/post/control-disk-size-in-docker
- https://ieevee.com/tech/2019/05/23/ephemeral-storage.html
- https://wizardforcel.gitbooks.io/vbird-linux-basic-4e/content/125.html
- https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/#local-ephemeral-storage
- https://coolshell.cn/articles/17200.html
Leave a Reply