1. 那些在凌晨三点炸掉的节点:复盘"磁盘爆满"的各种花式死法
做K8s运维的,谁没在大半夜被PagerDuty或者钉钉电话震醒过?一看报错:NodeNotReady,再一看Kubelet日志:DiskPressure。
这时候你大概率会心里暗骂:"鬼知道哪个业务Pod又在疯狂打日志了。"
但这回不一样。当我们把时间线拉回到上周那次大规模节点崩溃现场,情况有点诡异。节点健康度直接跌破了98%,离我们承诺的99.5%还有好大一截 。常规的kubelet驱逐机制(Eviction)确实生效了,但它太"暴力"了。它不管三七二十一,先把上面的Pod驱逐了再说,导致业务抖动巨大。
更要命的是,我们发现了一个很容易被忽略的"隐形杀手"------Node Local DNS Cache。
你可能会问,DNS缓存跟磁盘爆满有毛关系?关系大了。在设计这套高可用架构时,我们往往忽略了当DNS QPS(每秒查询率)飙升时,如果开启了详细日志,/var/log/下的日志量是惊人的。而且,一旦磁盘被占满,第一个受害的往往就是需要本地IO操作的组件。如果Node Local DNS因为磁盘写不进日志而挂起(CrashLoopBackOff),整个节点上的所有Pod解析都会超时,那这就不是驱逐几个Pod能解决的了,是整个节点实际上已经废了。
这就是为什么我们要设计一套基于"Node Local"理念的磁盘监控与清理策略。我们不能指望Prometheus的那个中心化抓取(Scrape),那玩意儿有延迟,等到它报警,节点早就凉透了。我们需要像部署Node Local DNS一样,在每个节点上部署一个"本地卫士",毫秒级响应,自己动手清理,绝不把问题上报给Master让它来调度------因为那时候已经晚了。
咱们先看看现场留下的几个"尸体"特征:
-
Docker Overlay2 膨胀 :不光是容器日志,还有那些构建失败遗留的Layer,僵尸一样躺在
/var/lib/docker里。 -
Kubelet GC 失效 :你以为配置了
image-gc-high-threshold就万事大吉?当磁盘写入速度>清理速度时,GC就是个笑话。 -
日志轮转(LogRotate)的滞后 :Linux默认的
logrotate是按天或者按大小检查的,但在突发流量面前,10分钟就能写满50G硬盘,cron任务根本来不及跑。
我们要解决的核心矛盾是:如何在Kubelet触发硬驱逐(Hard Eviction)之前,精准地、优雅地释放空间,同时保住核心组件(如Node Local DNS)的命。
2. 为什么K8s原生的驱逐策略是"猪队友"?
很多人在配置Kubelet参数时,觉得抄一下官方文档就完事了。
evictionHard:
memory.available: "100Mi"
nodefs.available: "10%"
nodefs.inodesFree: "5%"
这配置看着没毛病吧?毛病大了去了。
设想一个场景:你有一个2TB的NVMe磁盘。10%意味着要保留200GB的空间!当你的磁盘用到剩下200GB时,Kubelet就开始发疯一样驱逐Pod。这简直是极大的资源浪费。对于大盘节点,百分比阈值就是个坑。
再一个,Kubelet的驱逐是无差别攻击(虽然有QoS等级区分,但BestEffort死得最快)。但有时候,填满磁盘的可能只是一个死循环打印Debug日志的Sidecar容器,或者是一个配置错误的Fluentd。Kubelet不管这些,它可能为了腾空间,把你核心的业务网关(Gateway)给杀掉了,仅仅因为网关也是BestEffort或者Burstable。
最坑爹的是Inode耗尽。
我见过太多次,磁盘空间还有50%,但是Inode没了。原因通常是小文件过多。比如Node Local DNS如果配置了错误的缓存导出策略,或者某种应用生成了海量的临时空文件。Kubelet虽然监控Inode,但它处理Inode耗尽的方式依然是------驱逐Pod。
这根本不治本。
我们需要的是一种"手术刀"式的清理,而不是"核弹"式的驱逐。我们需要在磁盘使用率达到80%时就开始介入,而不是等到90%让Kubelet介入。
这套策略必须具备Node Local的属性:
-
本地决策:不需要请求APIServer,避免网络拥塞导致决策失败。
-
极低资源占用:不能为了监控磁盘,自己变成资源消耗大户。
-
白名单机制 :打死也不能动核心组件(比如
kube-system命名空间下的东西,尤其是CoreDNS和Node Local DNS)。
3. 架构设计:构建"双核"磁盘卫士(Watcher & Cleaner)
我们的思路是利用Kubernetes的DaemonSet,在每个节点上跑一个轻量级的Agent。你可能会说:"这不就是Prometheus Node Exporter吗?"
不,Node Exporter只负责读 (Read),它只告诉你"要死了",不负责"救人"。我们要设计的是一个既能读 也能写(Execute)的组件。
这个Agent的核心逻辑借鉴了Node Local DNS的高可用设计模式:独立于业务平面,直接与宿主机内核交互。
核心组件定义
我们需要一个DaemonSet,暂且命名为 disk-sentinel(磁盘哨兵)。它挂载了宿主机的关键目录:
-
/var/log(系统日志) -
/var/lib/docker(或者/var/lib/containerd,看你用啥运行时) -
/tmp(临时文件重灾区)
状态机流转
不要写那种死循环脚本,我们要用状态机(State Machine)的思维来写逻辑。
-
State A: 绿色 (Green)
-
磁盘使用率 < 75%。
-
动作:每30秒Check一次。休眠。
-
-
State B: 黄色 (Yellow) - 软清理阶段
-
磁盘使用率 > 75% 且 < 85%。
-
动作:清理已被标记为Dangling的Docker镜像;压缩归档日志(
.gz);清理超过3天的临时文件。 -
目标:在不影响任何运行中进程的情况下释放空间。
-
-
State C: 橙色 (Orange) - 激进清理阶段
-
磁盘使用率 > 85% 且 < 90%。
-
动作:这就是这套策略的精华所在------大日志截断。
-
不管是业务容器的标准输出(Json-file),还是Node Local DNS产生的审计日志,只要单个文件超过1GB,直接执行
truncate -s 0。
-
-
注意 :这里千万别用
rm。在Linux下,如果文件被进程占用,你rm了,空间是不会释放的!文件描述符(FD)还把着坑位呢。必须用truncate或者> file这种方式清空内容,空间立马回来。 -
State D: 红色 (Red) - 熔断保护
-
磁盘使用率 > 90%。
-
动作:这时候离Kubelet发疯不远了。为了保住Node Local DNS(保证DNS解析正常,以便告警能发出去,LB能健康检查),我们开始定向Kill掉那些IO Wait最高的非关键Pod。
-
这里的坑(Trick)
设计这个Agent时,有个巨大的坑:它自己不能因为OOM(内存溢出)或者磁盘IO过高被Kubelet干掉。
所以,disk-sentinel 的 PriorityClass 必须设置为 system-node-critical。这跟Node Local DNS是同等待遇。
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: disk-sentinel-priority
value: 1000000
globalDefault: false
description: "Ensure disk cleaner never gets evicted."
这一点非常关键。我之前有个兄弟,写的清理脚本跑在普通Pod里,结果磁盘满了,Kubelet第一个就把他的清理Pod给驱逐了,简直是笑话------救火队员被火烧死了。
4. 实战代码:编写你的"清理脚本"(还是?)
理论讲完了,咱们得落地。虽然语言写Operator很高大上,但对于这种涉及底层文件系统操作、信号处理的任务,说实话,精细编写的Shell脚本 或者Python往往更直接,尤其是当你需要快速迭代策略的时候。
但为了性能和并发控制,我推荐用 封装核心逻辑,底层调用系统Syscall。不过,为了让大家能看懂逻辑细节,我先用一段伪代码+Shell片段来展示核心的"大日志截断"逻辑。这也是基于Node Local DNS高并发日志场景下的各种血泪教训总结出来的。
场景:Node Local DNS日志导致磁盘爆炸
Node Local DNS (LocalDNS) 本质上是CoreDNS的一个变种,如果你在Corefile里开了 log 插件,并且没有很好的过滤规则,那它每秒能产生几千条查询日志。
# 这是一个典型的错误示范,千万别直接删
# rm -rf /var/log/containers/node-local-dns-*.log
我们来看看正确的"手术"姿势。
策略实现:智能截断器 (Smart Truncator)
我们需要编写一个能够识别文件句柄占用(LSOF风格)的清理器。
功能需求:
-
扫描
/var/lib/docker/containers下所有*-json.log文件。 -
找出最大的前10个。
-
关键点:判断这是否是白名单应用(例如安全审计组件)。
-
执行清空。
这里有个细节:直接清空可能会导致日志收集工具(如Filebeat或Fluentd)丢失offset,导致日志采集断流。怎么解?
没办法,保命要紧。在节点99%要挂的时候,丢几分钟日志和节点宕机相比,肯定是选前者。但是,我们可以做得优雅一点:只保留最近的10MB。
#!/bin/bash
# 这是一个运行在 DaemonSet 里的极简逻辑演示
# 真正的生产代码会有复杂的错误处理和Metrics上报
THRESHOLD=85
CURRENT=$(df -h / | awk 'NR==2 {print $5}' | sed 's/%//')
if [ "$CURRENT" -gt "$THRESHOLD" ]; then
echo "$(date) - Disk usage is ${CURRENT}%, strictly exceeding threshold ${THRESHOLD}%. Starting clean sequence."
# 第一招:清理Docker无用的Builder缓存(这玩意儿最占地)
# docker system prune -f --filter "until=24h" > /dev/null 2>&1
# 上面那条命令太慢了,容易卡死IO,我们直接操作文件系统
# 第二招:定向爆破大日志
# 找出超过500M的日志文件
find /var/lib/docker/containers/ -name "*-json.log" -size +500M -print0 | while IFS= read -r -d '' logfile; do
container_id=$(basename $(dirname "$logfile"))
# 这里需要调用CRI接口查询这个容器是不是核心组件
# 假设我们已经有了白名单列表
if grep -q "$container_id" /etc/disk-sentinel/whitelist; then
echo "Skipping critical container log: $logfile"
continue
fi
echo "Truncating massive log file: $logfile"
# 这是一个原子操作般的清空,保留最后1000行作为"遗言"
tail -n 1000 "$logfile" > "$logfile.tmp" && mv "$logfile.tmp" "$logfile"
# 或者最暴力的:
# > "$logfile"
done
fi
为什么说这是基于 Node Local DNS 的设计?
你看,Node Local DNS 的设计初衷是 Data Path Offloading(数据路径卸载),让DNS请求不经过Conntrack,直接走本地网卡。
我们的清理策略也是这个思路:Monitoring Path Offloading。
我们不把磁盘监控数据全部推送到Prometheus再去触发Alertmanager,然后再回调Webhook。这个链路太长了!当Node Local DNS本身因为conntrack表满或者磁盘满而无法解析DNS时,你的Webhook根本发不出去!
这就是个死锁。
所以,必须本地闭环。我们的 disk-sentinel 检测到磁盘满 -> 本地清理 -> 本地恢复 -> 事后异步上报Metrics。这才是高可用。
5. 进阶玩法:利用 eBPF 监控谁在"拉屎"
前面的方法是"治病",这章我们讲讲"找病因"。
很多时候,你清空了磁盘,过了10分钟又满了。简直是打地鼠。你需要知道到底是哪个进程在疯狂写盘。
传统的 iotop 能看,但如果你想自动化,还得靠 eBPF。
别听到eBPF就觉得门槛高,现在有很多现成的工具。我们可以把 bcc-tools 集成到我们的 disk-sentinel 镜像里。
监控重点不是写了多少字节,而是 写文件的频率 和 文件路径。
想象一下,如果我们在 Node Local DNS 所在的节点上,发现有一个未知的二进制文件在疯狂往 /tmp 写临时数据,eBPF 能瞬间捕获到这个 write 系统调用。
# 伪造的监控日志输出
[WARN] Process 'java' (PID 12345) in Pod 'data-processor' is writing 50MB/s to /tmp/spark-local-dir/...
一旦捕获到这种行为,我们的策略可以升级:
-
Cgroup 限速 :不是杀掉它,而是动态修改它的
blkiocgroup 限制。 -
Blkio Throttling:把它的写速度限制在 1MB/s。让它慢下来,给运维人员争取时间,同时防止它把整个节点的IO打满,导致 Kubelet 心跳超时。
这比直接驱逐要温柔得多,也更有技术含量。这才是SRE该干的事儿。
6. 告警哲学:别让 Prometheus 成为"事后诸葛亮"
接上一回。咱们已经在节点上部署了类似特种部队的 disk-sentinel,它能在毫秒级响应磁盘压力。但这就够了吗?
不够。作为架构师,你不能只依靠"自动挡"。你需要知道这套自动清理机制什么时候触发了 ,触发了多少次 ,以及是不是快兜不住了。
很多人的监控大屏那是做得花里胡哨,磁盘使用率画得跟心电图似的。但有个核心问题:当Prometheus拉取到"磁盘使用率95%"这个指标时,节点可能已经挂了。 尤其是对于运行了 Node Local DNS 这种高频网络组件的节点,一旦IO被锁死,Kubelet 甚至都无法响应 Prometheus 的 Scrape 请求。
所以,我们的监控维度必须从"状态监控"转向"事件监控"。
我们需要 disk-sentinel 暴露的一不是磁盘还剩多少,而是"我刚刚救了你几次"。
建议在 Agent 里内置一个极简的 HTTP Server(语言写这个就几行代码),暴露以下自定义 Metrics:
// 这是一个必须有的指标
// 意思是:我成功执行了多少次清理操作
disk_sentinel_clean_ops_total{type="docker_prune", status="success"} 15
disk_sentinel_clean_ops_total{type="log_truncate", status="success"} 42
// 这个指标用来衡量"危机程度"
// 意思是:这次清理释放了多少字节?
disk_sentinel_bytes_freed_total 5368709120
实战中的告警策略(AlertRule):
别去告警 disk_usage > 90%,那通常是运维人员的锅。你要告警的是:
rate(disk_sentinel_clean_ops_total[5m]) > 5
这句话翻译过来就是:兄弟,过去5分钟内,你的自动清理脚本跑了5次!这说明什么?说明你的节点处于"边拉边吃"的恶性循环中。业务写入速度极快,清理脚本在拼命擦屁股。虽然节点还没挂,但磁盘IO肯定已经炸了,Node Local DNS 的 UDP 丢包率肯定在上升。
这时候,才需要人工介入。这才是有效告警。
7. 那些年我们踩过的坑:Overlay2 的"幽灵空间"与文件系统腐烂
在设计这套方案时,我们遇到过一种极度诡异的情况:df -h 显示磁盘满了,du -sh /* 扫了一圈,加起来才用了 50%。
剩下的 50% 去哪了?
这就是 Docker (以及 Containerd) 使用 Overlay2 存储驱动时的经典问题------文件句柄泄漏(Leaked File Descriptors)。
当 Node Local DNS 或者某个业务 Pod 正在疯狂写日志时,如果你的清理策略是粗暴的 rm -rf logfile,恭喜你,你中奖了。在 Linux 内核里,如果一个文件被进程打开(Open),即使你在文件系统层删除了它(Unlink),它占用的磁盘块(Blocks)是不会释放的!
它变成了"幽灵文件"。
在 Kubernetes 这种动态环境里,Node Local DNS 这种 DaemonSet 往往长期运行,它的日志句柄可能几天都不释放。如果你只删文件不重启进程,磁盘空间永远回不来。
解决方案:Copy-on-Write (CoW) 的代价
我们在 disk-sentinel 里必须加入一个逻辑:LSOF Check。
# 找出那些被删除但仍占用的文件
lsof | grep '(deleted)' | awk '{print $2, $7}'
# $2 是 PID, $7 是文件大小
如果发现 Node Local DNS 进程持有大量 deleted 的大文件,我们的 Agent 必须做一个艰难的决定:重启它。
但这里有个大坑。Node Local DNS 是负责全节点解析的,暴力重启会导致瞬间的 DNS 解析失败,业务会报错 Unknown Host。
怎么做到无损?
这就是为什么我们在开头强调要基于"Node Local DNS"的特性来设计。Node Local DNS 通常会配置一个本地的虚拟 IP(比如 169.254.20.10)。我们可以利用 TCP/UDP 的重试机制。
-
Agent 捕获到需要重启 Node Local DNS(为了释放幽灵文件)。
-
Agent 修改
iptables规则,暂时将本地 DNS 请求 Reject(注意不是 Drop,是 Reject,让客户端立即收到错误并重试,或者回退到 Cluster DNS)。 -
极速重启 Node Local DNS Pod。
-
恢复规则。
这波操作属于"刀尖上跳舞",但为了保住节点磁盘,必须得会。记住,与其让磁盘满了导致整个节点 NotReady,不如牺牲 1-2 秒的 DNS 抖动。
8. 最后的防线:配置管理的艺术(ConfigMap热更新)
即使你的代码写得再完美,业务总是能给你整出新花样。今天他们把日志打到了 /var/log/myapp,明天可能就改成了 /data/logs。
你的 disk-sentinel 不能每次改配置都重新构建镜像发布吧?那样太慢了。
我们需要利用 Kubernetes 的 ConfigMap 挂载,并配合 inotify 机制实现策略的热更新。
这里的核心设计思想是:策略下发与执行分离。
我们在 ConfigMap 里定义一个 JSON 策略文件:
{
"policies": [
{
"name": "aggressive-log-clean",
"path": "/var/lib/docker/containers",
"pattern": "*-json.log",
"max_size_mb": 1000,
"action": "truncate",
"priority": 1
},
{
"name": "app-temp-clean",
"path": "/tmp",
"max_age_hours": 24,
"action": "delete",
"priority": 2
}
],
"whitelist": [
"k8s_POD_node-local-dns.*",
"k8s_POD_calico-node.*"
]
}
注意看那个白名单(whitelist)。这是保命符。
曾经有一次,我们的清理脚本误删了 CNI 插件(Calico)生成的临时网络配置文件,导致新创建的 Pod 全部没有 IP 地址。那天我被主管骂得狗血淋头。
从那以后,我在代码里写死了:凡是匹配到 k8s_POD_kube-system 或者核心组件名称的文件,除非它是纯文本日志,否则绝对不动。
热更新的实现细节 : 不要指望 K8s 自动更新挂载文件(那有延迟)。在 Agent 里起一个 goroutine,用 fsnotify 监听 ConfigMap 挂载的目录。一旦文件变动,立即重新加载内存里的规则对象。
这能让你在发生突发事故(比如某业务疯狂打日志)时,通过修改 ConfigMap,在 30 秒内将新的清理策略下发到全集群 1000 个节点,瞬间止血。这比你一个个去 SSH 删文件要帅多了。
9. 真实案例复盘:那个差点搞挂整个大促的"Debug开关"
理论结合实践,咱们来复盘一个真实发生的血案。
背景:某次大促活动前夕,流量预估是平时的 5 倍。我们扩容了节点,Node Local DNS 也调整了资源配额。看起来万事俱备。
爆发 :大促开始后半小时,监控群里炸了。几十个节点陆续变为 NotReady。应用的错误率飙升,全是 DNS 解析超时。
排查 : 我们登录到一个挂掉的节点,想敲个命令都卡得要死。dmesg 里全是 EXT4-fs error: device loop0: ext4_journal_start: detected aborted journal ------ 磁盘被写死了,文件系统变为只读。
谁干的?
是 Node Local DNS 自己。
原来,为了排查之前的一个偶发解析问题,有个研发兄弟在 CoreDNS 的 ConfigMap 里开启了 log 插件,并且------忘了关。
在大促的洪峰流量下,QPS 高达数万。每一条 DNS 查询(A记录 + AAAA记录)都在写日志。/var/log/syslog 和 Docker 的 json-log 以每秒几百兆的速度膨胀。
救火 : 当时我们要是有现在这套系统就好了。可惜当时没有。我们只能写了个 Ansible 脚本,批量上去 truncate 日志,并紧急修改 ConfigMap 重启 Node Local DNS。整个过程耗时 20 分钟,损失惨重。
如果用了这套策略:
-
T+1秒 :
disk-sentinel检测到磁盘使用率飙升至 85%。 -
T+2秒:识别到主要写入源是 Node Local DNS 的容器标准输出。
-
T+3秒 :虽然 Node Local DNS 在白名单里(不能杀),但策略配置允许对它的
.log文件进行truncate。 -
T+4秒 :文件被清空,报警
disk_cleaner_ops_total触发,通知 OnCall 人员"有人在疯狂打日志"。 -
结果:节点磁盘始终维持在 90% 以下,DNS 解析虽有轻微延迟(因为磁盘IO高),但服务没有中断。运维人员喝着咖啡把 Log 开关关掉即可。
这就是"自愈"的价值。
10. 日志轮转的"罗生门":Docker、Kubelet 与 Logrotate 的三角恋
既然我们已经聊了如何"救火",现在得聊聊怎么"防火"。你可能会说:"这还不简单?配置个 logrotate 不就完事了?"
呵呵,天真。
在 Kubernetes 的世界里,日志轮转(Log Rotation)简直就是一个多方甩锅的修罗场。我们来理一理这个混乱的关系:
-
应用本身:有的应用(比如 的 Log4j)自己配置了轮转。
-
容器运行时(Docker/Containerd):它们接管了 stdout/stderr,把这些输出存成 JSON 或 CRI 格式的文件。
-
Kubelet :它有个参数
container-log-max-files,但这只对 CRI 接口有效,而且有时候跟容器运行时的配置冲突。 -
Linux 系统级 Logrotate :这是传统的
/etc/logrotate.d/,通常是个 Cron Job。
最大的坑在哪里? 是竞态条件(Race Condition)。
我见过一个惨案:应用自己配置了按天轮转,Docker 也配置了 max-size 轮转。结果某天凌晨 0 点,两者同时触发。应用把 app.log 重命名为 app.log.2023-10-01,而 Docker 的 JSON Log 驱动正在疯狂往对应文件句柄里写数据。结果就是------日志丢了,或者文件指针错乱,甚至导致 Docker Daemon 卡死。
正确的姿势:
不要让应用自己轮转!不要让应用自己轮转! 重要的事情说三遍。
在容器化哲学里,应用应该把日志吐到 Stdout/Stderr,然后彻底做一个"甩手掌柜"。剩下的交给基础设施。
对于 Node Local DNS 这种关键组件,它的日志量极大。你必须在 Kubelet 层面或者 Runtime 层面做死限制。
如果你用的是 Docker,请在 /etc/docker/daemon.json 里把这个焊死:
{
"log-driver": "json-file",
"log-opts": {
"max-size": "50m",
"max-file": "3"
}
}
注意,这个配置只对新创建的容器生效!对于已经跑在那里的 Node Local DNS 容器,你得重置它。
如果你用的是 Containerd (现在的 K8s 标配),情况又不一样了。Containerd 默认不负责轮转,它把这事儿甩锅给了 Kubelet。
你得检查你的 Kubelet 启动参数:
containerLogMaxSize: "50Mi"
containerLogMaxFiles: 3
这里有个隐藏极深的 Bug :当 disk-sentinel 这种外部清理工具介入,强制 truncate 日志文件时,Kubelet 维护的日志文件大小计数器可能不会立即更新!Kubelet 以为文件还是 500MB,实际上已经是 0 了。这会导致 Kubelet 提前触发轮转,生成一堆空文件。
虽然这不致命,但这会搞乱你的日志收集系统(Fluentd/Filebeat)。所以,我们的 disk-sentinel 在执行截断后,最好能发送一个信号给 Kubelet 或者 Runtime,当然,这很难实现。所以最稳妥的办法是:依靠配置限制让系统自动轮转,disk-sentinel 只作为最后一道防线兜底。
11. 别把"清理"变成"谋杀":IO 节流的艺术
回到我们的 disk-sentinel。
假设现在磁盘真的爆满了,你的脚本决定删除 50GB 的废弃镜像层,或者截断一个 20GB 的日志文件。
你直接运行 rm -rf 或者 truncate。
下一秒,你的监控报警:Node Local DNS Latency Spike (DNS 延迟飙升)。
为什么?因为 删除文件也是重 IO 操作。
在 ext4 文件系统上,删除大量小文件或者操作巨型文件,会占用大量的 Journal(日志)写入带宽,并且可能导致文件系统锁。对于 Node Local DNS 这种对延迟极度敏感(毫秒级)的组件,磁盘 IO 飙升会导致它读取本地缓存变慢,甚至导致它的健康检查超时。
结果就是:你为了救节点清理磁盘,结果把节点上的网络搞挂了。
所以,我们的代码必须"温柔"。
利用 ionice 进行降级:
在调用清理命令时,必须把 IO 优先级调到最低(Idle Class)。
# 这是一个懂礼貌的清理命令
ionice -c 3 rm -rf /var/lib/docker/overlay2/dead-layer-sha256...
-c 3 意味着:只有当别人都不用磁盘时,我才用。
对于大文件的截断,不要一步到位:
如果你直接 truncate -s 0 bigfile.log,文件系统需要更新 inode 信息,释放大量 block。虽然比 rm 快,但依然有冲击。
更高级的写法是 Chunk Truncate(分块截断),虽然脚本写起来麻烦点,但为了稳定性是值得的:
# 伪代码思路
# 每次减小 1GB,中间 sleep 100ms,给 Node Local DNS 喘息的机会
current_size=$(stat -c%s bigfile.log)
target_size=0
step=$((1024*1024*1024)) # 1GB
while [ $current_size -gt $target_size ]; do
new_size=$((current_size - step))
if [ $new_size -lt 0 ]; then new_size=0; fi
truncate -s $new_size bigfile.log
sleep 0.1
current_size=$new_size
done
这看起来很繁琐?是的。但这就是 SRE(站点可靠性工程) 和 普通运维 的区别。普通运维只管结果,SRE 关注过程中的影响面(Blast Radius)。
12. 终极隔离:Ephemeral Storage Quota(把锅甩回给研发)
作为平台方,我们做得再好,也架不住研发写出"毒代码"。
比如某个实习生写了个死循环,疯狂打印 System.out.println("Debug: " + i)。一晚上就能把 100G 磁盘打满。
我们的 disk-sentinel 会疯狂清理,但清理速度赶不上写入速度怎么办?节点还是会挂。
这时候,必须祭出 Kubernetes 的原生大杀器:本地临时存储限制(Ephemeral Storage Limit)。
很多团队都会配置 CPU 和 Memory 的 Limit,但往往忽略 Storage。
resources:
requests:
ephemeral-storage: "2Gi"
limits:
ephemeral-storage: "4Gi"
加上这个限制后,Kubelet 会定期扫描每个 Pod 的磁盘使用量(包括它的 Logs 和它容器内的 Overlay2 写层)。
一旦超过 4Gi,Kubelet 会直接驱逐(Evict)这个 Pod。
注意,这里是驱逐 Pod ,而不是让整个Node 崩溃。
这简直是本质的区别!
-
没有 Limit :Pod 疯写 -> 磁盘满 -> Node NotReady -> Node Local DNS 挂 -> 全节点几十个业务全部中断。
-
有 Limit :Pod 疯写 -> Kubelet 发现超标 -> 杀掉该 Pod -> 其他业务不受影响 -> 研发收到报警"Pod Evicted due to disk pressure"。
这时候,研发会跑来找你:"我的 Pod 怎么挂了?" 你可以理直气壮地把 Grafana 截图甩给他:"你的应用在 10 分钟内写了 4G 日志,被系统自动制裁了。请优化你的日志级别。"
这招叫"故障隔离"。通过配置 Storage Limit,我们将"基础设施的风险"转化为了"单个应用的风险"。
对于 Node Local DNS,我们也要给它设限,但要给得宽裕些(比如 10Gi),因为它不仅是给自己用,还得缓存全集群的 DNS 记录。
13. 留存现场:死前的"黑匣子"
虽然我们的目标是 99.5% 的健康度,但剩下的 0.5% 总会发生。当一个节点因为磁盘问题彻底失联(比如 Kernel Panic 或者 SSH 都连不上),我们往往只能重启服务器。
重启后,现场没了。日志被 Logrotate 了,临时文件被 tmpwatch 删了。我们也就永远不知道当初是谁把磁盘打满的。
我们需要一个 "Last Breath"(最后一口气)机制。
在 disk-sentinel 进入红色警戒状态(比如磁盘 > 95%)且清理无效时,它应该执行最后一套动作:快照取证。
-
执行
du -sh /* | sort -hr | head -n 20:记录当时最大的文件夹。 -
执行
docker ps -s:记录当时容器的大小。 -
执行
iotop -b -n 1:记录谁在狂写磁盘。 -
打包上传:将这几KB的文本信息,通过 HTTP POST 发送到一个外部的死信队列(Dead Letter Queue)或者对象存储(S3)。
// 简单的 Go 代码片段
func sendMaydaySignal(report string) {
// 设置极短的超时,发不出去就算了,别卡死
client := http.Client{Timeout: 2 * time.Second}
client.Post("http://monitor-center/api/blackbox", "text/plain", strings.NewReader(report))
}
有了这个"黑匣子",第二天复盘会的时候,你就不再是猜测:"可能是那个 服务吧...",而是直接拿出证据:"10月24日凌晨3点14分,Node-07 节点磁盘耗尽,罪魁祸首是 payment-service-v3,瞬间产生了 40GB 的 core-dump 文件。"
这能极大地提升团队的信任度。数据不会说谎。
14. 展望:Serverless 时代的磁盘策略
写到这里,传统的 Kubernetes 节点磁盘策略基本讲透了。
但技术在演进。在 Serverless K8s (如 AWS Fargate, 阿里云 ASI) 环境下,你甚至无法登录节点,也无法部署 DaemonSet。那时候怎么办?
那时候,逻辑将完全反转。你不再管理磁盘,你管理的是流(Stream)。所有的日志必须通过 Logtail/Fluent-bit 的 Sidecar 模式直接推送到远端(Elasticsearch/Loki),本地不落盘,或者只落在一个极小的 tmpfs 内存盘里。
那将是另一个维度的挑战:网络带宽的竞争 。当日志量大到把网卡打满,导致业务请求进不来时,我们又该如何设计 Node Local 的网络 QoS 策略?
不过,只要你还在维护自建的 Kubernetes 集群,还在面对物理机或虚拟机,这篇专栏里的每一条策略------从双核监控 到IO节流 ,从文件句柄治理 到配额隔离------都是你保住年终奖的护身符。
别让磁盘,成为你职业生涯的短板。