一、存储的内容
要解决存储使用过多的问题,就得先了解存储中都保存了些什么内容,否则解决不了问题,还可能带来更多的风险。
1.1、镜像
容器要在节点上运行,kubelet 首先要拉取容器镜像到节点本地,然后再根据镜像创建容器。随着 Pod 的调度和程序的升级,日积月累,节点本地就会保存大量的容器镜像,占用大量存储空间。
如果使用的是 Docker 容器运行时,这些文件保存在 /var/lib/docker/image/overlay2 目录下。
1.2、可写层
关于可写层,了解容器本质的同学应该比较熟悉,容器运行时使用的是一种联合文件系统技术,它把镜像中的多层合并起来,然后再增加一个可写层,容器中写操作的结果会保存在这一层,这一层存在于容器当前节点的本地存储中。虽然镜像中的层是容器实例共享的,但是可写层是每个容器一份。
假如我们有一个名为 mypod 的 Pod 实例,在其中创建一个文件:/hello.txt,并写入 hello k8s 的字符。
$ kubectl exec mypod -- sh -c 'echo "hello k8s" > /hello.txt'
$ kubectl exec mypod -- cat /k8s/hello.txt
hello k8s
如果使用的是 Docker 容器运行时,可以在 Docker 的相关目录中找到可写层以及刚刚创建的这个文件,它们在 /var/lib/docker/overlay2
这个目录下。
如果毫无节制的使用可写层,也会导致大量的本地磁盘空间被占用。
1.3、日志
K8S 推荐的日志输出方式是将程序日志直接输出到标准输出和标准错误,此时容器运行时会捕捉这些数据,并把它们写到本地存储,然后再由节点上的日志代理或者 Pod 中的边车日志代理转运到独立的日志处理中心,以供后续分析使用。
这些日志保存在节点本地的 /var/log/container 目录下,我们可以实际创建一个 Pod 来确认下:
apiVersion: v1
kind: Pod
metadata:
name: pod-log-stdout
spec:
containers:
- name: count
image: busybox:latest
args: [/bin/sh, -c,
'i=0; while true; do echo "$i: $(date) a log entry."; i=$((i+1)); usleep 1000; done']
这个 Pod 每隔 1 毫秒会写 1 条数据到标准输出。要找到容器运行时根据标准输出创建的日志文件,首先要找到这个 Pod 部署的节点,然后登录到这个节点,就能找到对应的文件了。
如果程序输出的日志很多,占满磁盘空间就是早晚的事。
1.4、emptyDir
emptyDir 是一种基于节点本地存储的 Volume 类型,它通过在本地存储创建一个空目录来实际承载 Volume。使用这种存储卷可以在 Pod 的多个容器之间共享数据,比如一个容器造数据,一个容器消费数据。
看下面这个例子:
apiVersion: v1
kind: Pod
metadata:
name: pod-vol-empty-dir
spec:
containers:
- name: count
image: busybox:latest
args: [/bin/sh, -c, 'echo "k8s" > /cache/k8s.txt;sleep 1800']
volumeMounts:
- mountPath: /cache
name: cache-volume
volumes:
- name: cache-volume
emptyDir: {}
在 spec.volumes[] 中只需要添加一个名为 emptyDir 的字段,它的配置都可以使用默认值,然后这个卷会被挂载到容器的 /cache 路径。
容器的启动参数是一个 shell 命令,它会在容器的 cache 目录下创建 1 个名为 k8s.txt 的文件。容器创建后稍等一会,使用下面的命令获取这个文件的内容:
$ kubectl exec pod-vol-empty-dir -- cat /cache/k8s.txt
k8s
可以看到,文件内容正是容器启动命令中写入的 k8s 字符。
K8S 会在当前的 Node 自动创建一个目录来实际承载这个卷,目录的位置在 Node 的 /var/lib/kubelet/pods 路径下。要查看这个目录中的内容,需要先找到 Pod Id 和对应的 Node,然后登录到这个 Node,就能找到这个目录了。minikube 中的查找方法如下图所示:
注意用颜色框圈出来的内容,不同的 Pod 对应的数据不同。查找 Pod Id 的命令:
$ kubectl get pods -o custom-columns=PodName:.metadata.name,PodUID:.metadata.uid,PodNode:.spec.nodeName
如果不对 emptyDir Volume 做一些限制,也是有很大的风险会使用过多的磁盘空间。
二、存储的限制方法
通过上文的介绍,我们可以看到,除了容器镜像是系统机制控制的,其它的内容都跟应用程序有关。
应用程序完全可以控制自己使用的存储空间,比如少写点日志,将数据保存到远程存储,及时删除使用完毕的临时数据,使用 LRU 等算法控制存储空间的使用量,等等。不过完全依赖开发者的自觉也不是一件很可靠的事,万一有 BUG 呢?所以 K8S 也提供了一些机制来限制容器可以使用的存储空间。
2.1、K8S 的 GC
K8S 有一套自己的 GC 控制逻辑,它可以清除不再使用的镜像和容器。这里我们重点看下对镜像的清理。
这个清理工作是 kubelet 执行的,它有三个参数来控制如何执行清理:
-
imageMinimumGCAge 未使用镜像进行垃圾回收时,其存在的时间要大于这个阈值,默认是 2 分钟。
-
imageGCHighThresholdPercent 镜像占用的磁盘空间比例超过这个阈值时,启动垃圾回收。默认 85。
-
ImageGCLowThresholdPercent 镜像占用的磁盘空间比例低于这个阈值时,停止垃圾回收。默认 80。
可以根据自己的镜像大小和数量的水平来更改这几个阈值。
2.2、日志总量限制
K8S 对写入标准输出的日志有一个轮转机制,默认情况下每个容器的日志文件最多可以有 5 个,每个文件最大允许 10Mi,如此每个容器最多保留最新的 50Mi 日志,再加上 Node 也可以对 Pod 数量进行限制,日志使用的本地存储空间就变得可控了。这个控制也是 kubelet 来执行的,有两个参数:
-
containerLogMaxSize 单个日志文件的最大尺寸,默认为 10Mi。
-
containerLogMaxFiles 每个容器的日志文件上限,默认为 5。
以上文的 pod-log-stdout 这个 Pod 为例,它的日志输出量很多就会超过 10Mi,我们可以实际验证下。
不过如果没有意外,意外将要发生了,K8S 的限制不起作用。这是因为我们使用的容器运行时是 docker,docker 有自己的日志处理方式,这套机制可能过于封闭,K8S 无法适配或者不愿意适配。可以更改 docker deamon 的配置来解决这个问题,在 K8S Node 中编辑这个文件 /etc/docker/daemon.json (如果没有则新建),增加关于日志的配置:
{
"log-opts": {
"max-size": "10m",
"max-file": "5"
}
}
然后重启 Node 上的 docker:systemctl restart docker。注意还需要重新创建这个 Pod,因为这个配置只对新的容器生效。
在 docker 运行时下,容器日志实际上位于 /var/lib/docker/containers 中,先找到容器 Id,然后就可以观察到这些日志的变化了:
2.3、emptyDir Volume 限制
对于 emptyDir 类型的卷,可以设置 emptyDir.sizeLimit,比如设置为 100Mi。
apiVersion: v1
kind: Pod
metadata:
name: pod-vol-empty-dir-limit
spec:
containers:
- name: count
image: busybox:latest
args: [/bin/sh, -c,
'while true; do dd if=/dev/zero of=/cache/$(date "+%s").out count=1 bs=5MB; sleep 1; done']
volumeMounts:
- mountPath: /cache
name: cache-volume
volumes:
- name: cache-volume
emptyDir:
sizeLimit: 100Mi
稍等几分钟,然后查询 Pod 的事件:
可以看到 kubelet 发现 emptyDir volume 超出了 100Mi 的限制,然后就把 Pod 关掉了。
2.4、临时数据的总量限制
对于所有类型的临时性本地数据,包括 emptyDir 卷、容器可写层、容器镜像、日志等,K8S 也提供了一个统一的存储请求和限制的设置,如果使用的存储空间超过限制就会将 Pod 从当前 Node 逐出,从而避免磁盘空间使用过多。
然后我们创建一个 Pod,它会每秒写 1 个 5M 的文件,同时使用 spec.containers[].resources.requests.limits 给存储资源设置了一个限制,最大 100Mi。
apiVersion: v1
kind: Pod
metadata:
name: pod-ephemeral-storage-limit
spec:
containers:
- name: count
image: busybox:latest
args: [/bin/sh, -c,
'while true; do dd if=/dev/zero of=$(date "+%s").out count=1 bs=5MB; sleep 1; done']
resources:
requests:
ephemeral-storage: "50Mi"
limits:
ephemeral-storage: "100Mi"
稍等几分钟,然后查询 Pod 的事件:
$ kubectl describe pod pod-ephemeral-storage-limit
可以看到 kubelet 发现 Pod 使用的本地临时存储空间超过了限制的 100Mi,然后就把 Pod 关掉了。
通过这些存储限制,基本上就可以说是万无一失了。当然还要在节点预留足够的本地存储空间,可以根据 Pod 的数量和每个 Pod 最大可使用的空间进行计算,否则程序也会因为总是得不到所需的存储空间而出现无法正常运行的问题。