K8S节点本地存储被撑爆问题彻底解决方法
萤火架构 人气:0存储的内容
现在云原生越来越流行,很多企业都上马了K8S,但是这里边也有很多的坑要填,这篇文章就聊一下K8S节点本地存储被撑爆的问题,也就是磁盘被占满的问题。
要解决存储使用过多的问题,就得先了解存储中都保存了些什么内容,否则解决不了问题,还可能带来更多的风险。
镜像
容器要在节点上运行,kubelet 首先要拉取容器镜像到节点本地,然后再根据镜像创建容器。随着Pod的调度和程序的升级,日积月累,节点本地就会保存大量的容器镜像,占用大量存储空间。
如果使用的是Docker容器运行时,这些文件保存在 /var/lib/docker/image/overlay2 目录下。
可写层
关于可写层,了解容器本质的同学应该比较熟悉,容器运行时使用的是一种联合文件系统技术,它把镜像中的多层合并起来,然后再增加一个可写层,容器中写操作的结果会保存在这一层,这一层存在于容器当前节点的本地存储中。虽然镜像中的层是容器实例共享的,但是可写层是每个容器一份。
假如我们有一个名为 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 这个目录下。
如果毫无节制的使用可写层,也会导致大量的本地磁盘空间被占用。
日志
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部署的节点,然后登录到这个节点,就能找到对应的文件了。
如果程序输出的日志很多,占满磁盘空间就是早晚的事。
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也提供了一些机制来限制容器可以使用的存储空间。
K8S的GC
K8S有一套自己的GC控制逻辑,它可以清除不再使用的镜像和容器。这里我们重点看下对镜像的清理。
这个清理工作是 kubelet 执行的,它有三个参数来控制如何执行清理:
- imageMinimumGCAge 未使用镜像进行垃圾回收时,其存在的时间要大于这个阈值,默认是2分钟。
- imageGCHighThresholdPercent 镜像占用的磁盘空间比例超过这个阈值时,启动垃圾回收。默认85。
- ImageGCLowThresholdPercent 镜像占用的磁盘空间比例低于这个阈值时,停止垃圾回收。默认80。
可以根据自己的镜像大小和数量的水平来更改这几个阈值。
日志总量限制
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,然后就可以观察到这些日志的变化了:
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 关掉了。
临时数据的总量限制
对于所有类型的临时性本地数据,包括 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最大可使用的空间进行计算,否则程序也会因为总是得不到所需的存储空间而出现无法正常运行的问题。
加载全部内容