Kubernetes 容器存储控制:用 XFS 项目配额替代驱逐
一、问题的本质
Kubernetes 提供了 ephemeral-storage 限制来控制容器的临时存储用量。但它的工作方式存在一个根本缺陷:
容器写超限 → kubelet 定期 du 扫描 → 发现超限 → Eviction → SIGKILL → 容器死亡
Pod 被直接杀掉。应用没有机会清理、降级或告警。就像你家水表超标,物业直接拆了你家大门。
二、我们的目标
容器写超限 → 内核拦截 write() 系统调用 → 返回 ENOSPC → Pod 继续运行
| K8s 原生驱逐 | XFS 项目配额 | |
|---|---|---|
| 超限后果 | Pod 被杀(SIGKILL) | write() 返回 No space left on device |
| 进程存活 | ❌ | ✅ |
| 能否 exec 进去清理 | ❌ | ✅ |
| 删除文件释放空间 | ❌ | ✅ 删完立即恢复写入 |
| 应用感知 | 连接断开 | 可捕获异常、优雅降级 |
三、XFS 项目配额是什么
XFS 文件系统支持项目配额(Project Quota)——可以给任意目录设置容量上限,内核在每次 write() 系统调用时即时检查,超限直接返回 ENOSPC。
# 给目录 /var/lib/containerd/snapshots/179/fs 设 50MB 配额
xfs_quota -x -c "project -s -p <路径> <项目ID>" /var/lib/containerd
xfs_quota -x -c "limit -p bhard=50M <项目ID>" /var/lib/containerd
关键特性:
- 内核级强制,零延迟
- 只影响写入,不影响读取和删除
- 进程完全无感知,直到调用
write()收到ENOSPC
四、为什么 K8s 原生驱逐做不到
K8s 的 ephemeral-storage 无论底层是 ext4 还是 XFS,都走同一条路径:
kubelet → cAdvisor → du 命令扫描 → 对比 limits → 超限 → EvictionManager → 驱逐
即使底层挂载了 XFS 并开启了项目配额,kubelet 根本不用它。这是两套完全独立的机制。
社区在 2021 年就有相关讨论(kubernetes#102023),但目前 kubelet 的 LocalStorageCapacityIsolationFSQuotaMonitoring feature gate 也只是用 FS 配额做监控(替代 du 扫描),不改变驱逐的行为。
五、架构设计
┌─────────────────────────────────────────────────────────┐
│ Kubernetes Cluster │
│ │ │
│ Pod YAML │ │
│ ┌─────────────────────┐ │
│ │ annotations: │ │
│ │ xfs-quota: "50M" │──── kubectl apply │
│ │ resources: │ │
│ │ ephemeral-storage │ │
│ │ : "10Gi" ← 高,确保 kubelet 不驱逐 │
│ └─────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ xfs-quota- │ 每 2 秒轮询 │
│ │ watcher │ kubectl get pods │
│ │ (Daemon) │ --field-selector │
│ └──────┬───────────┘ spec.nodeName= │
│ │ 本节点 │
│ 检测到新 Pod │ │
│ 读 annotation│ │
│ ▼ │
│ ┌───────────────────────┐ │
│ │ 设 XFS 项目配额 │ │
│ │ xfs_quota limit │ │
│ │ -p bhard=50M │ │
│ │ <project-ID> │ │
│ └───────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ Linux 内核 │ │
│ │ XFS 模块 │ │
│ │ ┌─────────┐ │ │
│ │ │write() │ │ 超限 → ENOSPC │
│ │ │ ↓ │ │ 未超 → 正常 │
│ │ │检查配额 │ │ │
│ │ └─────────┘ │ │
│ └──────────────┘ │
└─────────────────────────────────────────────────────────┘
六、核心组件
6.1 Watcher(守护进程)
一个 Bash 脚本,运行在需要启用配额的节点上:
#!/bin/bash
# 每 2 秒检查本节点 Running Pod
# 读取 annotation xfs-quota-limit
# 设 XFS 项目配额
while true; do
pods=$(kubectl get pods -A --field-selector "spec.nodeName=$NODE" \
--no-headers | grep Running | awk '{print $1, $2}')
while read -r ns pod; do
# 检查 annotation
xfs_ann=$(kubectl get pod "$pod" -n "$ns" \
-o jsonpath='{.metadata.annotations.xfs-quota-limit}')
if [ -n "$xfs_ann" ]; then
# 找容器 overlay upperdir
# 设 XFS 配额
xfs_quota -x -c "project -s -p $UPPERDIR $PROJID" $MOUNT
xfs_quota -x -c "limit -p bhard=$xfs_ann $PROJID" $MOUNT
fi
done <<< "$pods"
sleep 2
done
6.2 Pod 声明方式
apiVersion: v1
kind: Pod
metadata:
name: my-app
annotations:
xfs-quota-limit: "200M" # ← watcher 读这个
spec:
containers:
- name: app
image: my-app:latest
resources:
limits:
ephemeral-storage: "10Gi" # ← 设高值,避免 kubelet 驱逐
xfs-quota-limit 和 ephemeral-storage 分离设计:
xfs-quota-limit:真正的限制,由 XFS 在内核层强制ephemeral-storage:设一个高到不会触发的值,仅用于让 kubelet 的调度/驱逐系统正常工作
七、前置条件
| 条件 | 说明 |
|---|---|
| XFS 文件系统 | containerd 数据目录(/var/lib/containerd)必须挂载为 XFS |
reflink=0 | mkfs.xfs -m reflink=0,XFS reflink 与 project quota 互斥 |
pquota 挂载选项 | mount -o pquota,启用项目配额 |
| containerd 2.x | 使用 overlayfs snapshotter |
| kubectl + admin.conf | watcher 需要调用 Kubernetes API |
八、部署步骤
Step 1:准备 XFS 文件系统
# 如果已有空闲 LVM 空间
lvcreate -l +100%FREE -n data ubuntu-vg
mkfs.xfs -m reflink=0 /dev/mapper/ubuntu--vg-data
# 停止服务,迁移数据
systemctl stop kubelet containerd
mount -o pquota /dev/mapper/ubuntu--vg-data /mnt/tmp
rsync -a /var/lib/containerd/ /mnt/tmp/
umount /mnt/tmp
mv /var/lib/containerd /var/lib/containerd.old
mkdir /var/lib/containerd
mount -o pquota /dev/mapper/ubuntu--vg-data /var/lib/containerd
# 持久化
echo '/dev/mapper/ubuntu--vg-data /var/lib/containerd xfs defaults,pquota 0 0' >> /etc/fstab
# 恢复服务
systemctl start containerd kubelet
Step 2:安装 watcher
# 复制脚本到节点
cp xfs-quota-watcher.sh /usr/local/bin/
chmod +x /usr/local/bin/xfs-quota-watcher.sh
# 确保 kubectl 可用
mkdir -p /root/.kube
cp admin.conf /root/.kube/config
# 启动(可通过 systemd 管理为服务)
nohup /usr/local/bin/xfs-quota-watcher.sh > /var/log/xfs-quota-watcher.log 2>&1 &
Step 3:创建带配额的 Pod
metadata:
annotations:
xfs-quota-limit: "200M"
spec:
containers:
- resources:
limits:
ephemeral-storage: "10Gi"
九、实测效果
测试 Pod
annotations:
xfs-quota-limit: "50M" # XFS 配额 50MB
spec:
containers:
- command: ["sh", "-c", "while true; do dd if=/dev/zero of=/tmp/f.$i bs=1M count=10; i=$((i+1)); sleep 1; done"]
resources:
limits:
ephemeral-storage: "10Gi" # K8s 限制 10GB(不会触发)
测试结果
# 正常写入阶段
10+0 records in
10+0 records out
10485760 bytes (10.0MB) copied
# 配额用尽后
dd: can't open '/tmp/f.12': No space left on device
dd: can't open '/tmp/f.13': No space left on device
dd: can't open '/tmp/f.14': No space left on device
...
# Pod 状态
storage-test 1/1 Running 0 46s ← 持续运行!
清理演示
# 进入 Pod
$ kubectl exec -it storage-test -- sh
# 删除旧文件
$ rm -f /tmp/f.*
# dd 立即恢复写入
# 日志输出:
10+0 records in
10+0 records out ← 恢复!
十、与 OCI Hook 方案的对比
我们也探索了通过 containerd OCI Hook 实现自动配额(容器创建时自动设配额),但遇到兼容性问题:
| 方式 | 结果 |
|---|---|
containerd base_runtime_spec | CRI 插件崩溃 |
替换 /usr/bin/runc wrapper | containerd-shim-runc-v2 不走外部 runc |
| Kubernetes Watch API + 轮询 | ✅ 稳定可靠 |
OCI Hook 方案虽更优雅,但在 containerd 2.x 上需要 NRI(Node Resource Interface)支持,目前 NRI 生态尚未成熟。轮询方案虽然有多余 2 秒延迟,但实现简单、不侵入 containerd,生产可用。
十一、后续优化方向
| 优化项 | 方案 |
|---|---|
| 降低延迟 | 使用 kubectl get pods --watch 替代轮询,支持 --output-watch-events |
| 生产化部署 | watcher 封装为 systemd service |
| 多节点支持 | 通过 DaemonSet 在每个节点运行 watcher |
| 配额清理 | Poststop 回调清除已删除 Pod 的 project ID |
| 监控告警 | Prometheus exporter 暴露配额使用率 |
| 多租户 | 结合 ResourceQuota + LimitRange,不同 namespace 不同配额策略 |
十二、结论
XFS 项目配额为 Kubernetes 容器存储控制提供了一种非驱逐式的替代方案。它不是要完全替代 ephemeral-storage,而是在以下场景中提供更好的体验:
- 有状态应用:不能随意重启,需要存活以清理数据
- 日志密集型:写满后停止写日志但继续服务
- 多租户平台:给租户一个清晰的"磁盘已满"而不是 Pod 突然消失
核心代码不到 100 行 Bash,即可在现有集群上运行,无需修改 containerd 或 kubelet 源码。
仓库地址:所有脚本和配置已整理在
offline/和项目根目录下。环境:Kubernetes v1.35.6 · containerd v2.2.5 · XFS on LVM · Ubuntu 22.04
集群:3 Master + 1 Worker 高可用架构,VIP + HAProxy + keepalived,Harbor 私有镜像仓库

1224

被折叠的 条评论
为什么被折叠?



