Kubernetes 容器存储控制:用 XFS 项目配额替代驱逐

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-limitephemeral-storage 分离设计:

  • xfs-quota-limit:真正的限制,由 XFS 在内核层强制
  • ephemeral-storage:设一个高到不会触发的值,仅用于让 kubelet 的调度/驱逐系统正常工作

七、前置条件

条件说明
XFS 文件系统containerd 数据目录(/var/lib/containerd)必须挂载为 XFS
reflink=0mkfs.xfs -m reflink=0,XFS reflink 与 project quota 互斥
pquota 挂载选项mount -o pquota,启用项目配额
containerd 2.x使用 overlayfs snapshotter
kubectl + admin.confwatcher 需要调用 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_specCRI 插件崩溃
替换 /usr/bin/runc wrappercontainerd-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 私有镜像仓库

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值