Longhorn 实战:让 Pod 真正跨节点漂移(含 SeaweedFS 备份 + 离线安装)

环境前置:k0s 单节点(controller + worker 合一),WSL2,私有镜像仓库 172.29.102.221:5000,SeaweedFS 已用 local-path 跑起来充当内网 OSS。

一、local-path 的问题:Pod 被钉死在节点上

做 K8s 实验时,几乎所有人都从 local-path-provisioner 起步。它极简——一个 Deployment 加一个 ConfigMap,PVC 一声明,节点目录就建好,PV 就绑定。开发体验一流。

但它的本质就一句:把节点本地目录动态包装成 PV。这带来一个绕不开的硬伤——

1
PV 的 nodeAffinity = kubernetes.io/hostname: <某个节点>

PV 一旦在某节点创建,就永久绑定到这个节点。Pod 哪怕被删掉重建,调度器也会因为 PV 节点亲和性,被迫调度回原节点。Pod 没有真正的迁移自由,只是”假装能调度”。

单节点集群上这个问题被彻底放大:

故障 数据 Pod 能否飘走
节点临时重启/网络抖动 不丢 ❌ 本来就一个节点,没地方飘
节点磁盘坏/重装系统
想扩一个 worker 节点分担负载 不丢 ❌ 老数据全锁在原节点上

更要命的是 local-path 的容量写法会被直接忽略——PVC 声明 100Gi,实际能写到撑爆根盘为止,没有真正的限额。

我之前部署 TiDB 时也踩过这个坑:local-path 下的 TiKV Store 强绑定节点,下线一个节点要走 Region 迁移 + Store Tombstone + 删 PVC 的完整流程,半天都收不回来。单节点 + local-path,本质上和 Docker 的 -v /host:/container 没区别,K8s 调度的弹性被掏空了。

二、Longhorn 是什么:把存储也变成可调度的微服务

Longhorn 是 CNCF 孵化项目(Rancher 出品),定位是云原生分布式块存储。一句话概括它的设计哲学:

把传统存储里那个”大而全的控制器”,拆成”每个卷一个轻量 Engine”。每个 PV 都是一个独立的微服务,故障域被隔离到单个卷。

两层架构

1
2
3
4
5
6
7
8
9
10
11
12
┌──────────────────────────────────────────────────┐
│ 控制平面 Longhorn Manager (DaemonSet, 每节点) │
│ 创建卷 / 调度副本 / 健康检查 / 触发重建 │
└──────────────────────┬───────────────────────────┘
│ CSI 接口
┌──────────────────────▼───────────────────────────┐
│ 数据平面 │
│ Engine (每卷一个控制器) │
│ ├─ Replica-1 (Node A) │
│ ├─ Replica-2 (Node B) ← 跨节点同步复制 │
│ └─ Replica-3 (Node C) │
└──────────────────────────────────────────────────┘

Engine 把同一份数据同步写到 N 个节点上的 Replica。Pod 挂载的块设备挂的是 Engine,Engine 知道哪个 Pod 在用、哪些 Replica 是健康的。这正是它能实现”卷跟随 Pod 跨节点迁移”的根本原因。

它怎么让 Pod 飘起来

这是和 local-path 最大的本质区别。Longhorn 的卷和节点没有强绑定

  • Pod 调度到 Node B → Engine 在 Node B 启动 → 挂载 Node B 上的副本(或通过已有的副本恢复)
  • Node B 故障 → Pod 被重新调度到 Node C → Engine 在 Node C 重新启动,从健康的副本拉起数据
  • 节点扩容后,把副本数从 1 调到 2/3,数据自动在新节点重建,老 PV 不会被锁死

单节点场景下副本数设为 1,持久性确实和 local-path 一个级别(数据只有一份),但 Longhorn 给的是一套未来可平滑演进的体系——哪天你加了第二个 worker 节点,把 numberOfReplicas 改成 2,立刻就有跨节点冗余,老卷迁移过去即可。local-path 没有这个升级路径。

local-path vs Longhorn(副本=1)对比

维度 local-path Longhorn (副本=1)
底层 节点本地目录 块设备 + 独立 Engine
PV 节点亲和 强绑定 hostname 跟随 Pod 调度
跨节点迁移 ❌ 不支持 ✅ Pod 飘哪,卷去哪
容量限额 ❌ 忽略 ✅ 真实限额
快照 (COW)
备份到 S3/NFS
在线扩容
多节点冗余演进 ❌ 无路径 ✅ 改副本数即可
复杂度 极简 重(Manager/Engine/CSI 全套)

一句话:local-path 是”动态 hostPath”,Longhorn 是”自带调度能力的分布式块存储”。

三、离线安装:所有镜像走内网仓库

内网环境无法直接拉 docker.io。核心思路:用 skopeo copy 把所有镜像从公网同步到私有仓库 172.29.102.221:5000,Helm 安装时通过 global.imageRegistry 指过去。

1. 节点前置依赖

V1 引擎依赖 iSCSI,RWX/备份依赖 NFS 客户端:

1
2
3
4
5
6
7
# Ubuntu/Debian
apt-get install -y open-iscsi nfs-common
modprobe iscsi_tcp
systemctl enable --now iscsid

# RHEL/CentOS/Rocky
yum install -y iscsi-initiator-utils nfs-utils

必备 shell 工具(longhornctl 预检会查):bash curl findmnt grep awk blkid lsblk

2. 安装预检工具

1
2
3
4
5
6
curl -sSfL -o longhornctl \
https://github.com/longhorn/cli/releases/download/v1.12.0/longhornctl-linux-amd64
sudo mv longhornctl /usr/local/bin/
sudo chmod +x /usr/local/bin/longhornctl

longhornctl check preflight --kubeconfig ~/.kube/config

3. WSL2 特殊处理(关键)

WSL2 下挂载传播默认不全,不加这一步 DaemonSet 会起不来

1
sudo mount --make-rshared /

建议写进 /etc/rc.local 或 systemd unit,重启后自动执行。

4. 同步镜像到私有仓库

官方镜像清单在 longhorn/longhorn deploy/longhorn-images.txt,共 14 个。同步脚本(以核心组件为例,CSI 侧同理):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# Longhorn 核心组件
for img in \
longhornio/longhorn-manager:v1.12.0 \
longhornio/longhorn-engine:v1.12.0 \
longhornio/longhorn-instance-manager:v1.12.0 \
longhornio/longhorn-share-manager:v1.12.0 \
longhornio/longhorn-ui:v1.12.0 \
longhornio/backing-image-manager:v1.12.0 \
longhornio/longhorn-cli:v1.12.0 \
longhornio/support-bundle-kit:v0.0.86
do
skopeo copy docker://docker.io/$img \
docker://172.29.102.221:5000/$img \
--dest-tls-verify=false \
--dest-creds="boer:Admin@123" \
--override-arch=amd64 --override-os=linux
done

# CSI 侧车组件
for img in \
longhornio/csi-attacher:v4.12.0 \
longhornio/csi-provisioner:v5.3.0-20260514 \
longhornio/csi-resizer:v2.1.0-20260514 \
longhornio/csi-snapshotter:v8.5.0-20260514 \
longhornio/csi-node-driver-registrar:v2.17.0 \
longhornio/livenessprobe:v2.19.0
do
skopeo copy docker://docker.io/$img \
docker://172.29.102.221:5000/$img \
--dest-tls-verify=false \
--dest-creds="boer:Admin@123" \
--override-arch=amd64 --override-os=linux
done

生产建议用 skopeo sync --src yaml --dest registry 批量同步整个清单文件,比 for 循环更稳。

5. Helm 安装(指向内网仓库)

1
2
3
4
5
6
7
helm install longhorn longhorn/longhorn \
--namespace longhorn-system \
--create-namespace \
--version 1.12.0 \
--set defaultSettings.defaultReplicaCount=1 \
--set defaultSettings.defaultDataPath=/opt/longhorn \
--set global.imageRegistry=172.29.102.221:5000

关键参数解释:

  • defaultReplicaCount=1:单节点必须改成 1,默认 3 会导致 PVC 卡在 Pending(找不到足够节点调度副本)。
  • defaultDataPath=/opt/longhorn:数据落到本机目录,必须是 ext4/xfs,不要用软链接(ln -s),需要别名用 mount --bind
  • global.imageRegistry:所有组件镜像统一走私有仓库,绕开 docker.io 拉取。

6. 验证

1
2
3
4
5
kubectl -n longhorn-system get pod
# 期望:所有 Pod Running,无 ImagePullBackOff

longhornctl check preflight --kubeconfig ~/.kube/config
# 期望:All checks passed

四、StorageClass:单节点必须改副本数

Helm 改的是默认值,但StorageClass 里的 numberOfReplicas 会覆盖默认值。如果 StorageClass 还写 "3",单节点上 PVC 依然会卡 Pending。这是个非常常见的坑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# storageclass-longhorn.yaml
allowVolumeExpansion: true
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
annotations:
storageclass.kubernetes.io/is-default-class: "true"
name: longhorn
parameters:
backupTargetName: default
dataEngine: v1 # V1 = iSCSI + 稀疏文件,成熟稳定(V2 是 SPDK 实验性)
dataLocality: disabled
disableRevisionCounter: "true"
fromBackup: ""
fsType: ext4
numberOfReplicas: "1" # 单节点必须为 1,默认 3 会调度失败
staleReplicaTimeout: "30"
unmapMarkSnapChainRemoved: ignored
provisioner: driver.longhorn.io
reclaimPolicy: Delete
volumeBindingMode: Immediate

创建一个测试 PVC + Pod 验证卷能动态供应:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
kubectl apply -f storageclass-longhorn.yaml

# 测试 PVC
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: longhorn-test
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: longhorn
resources:
requests:
storage: 5Gi
EOF

kubectl get pvc longhorn-test
# 期望:STATUS = Bound

五、接 SeaweedFS 做 S3 备份(闭环)

单副本意味着数据只有一份,节点磁盘坏了就没了。Longhorn 自带增量备份到 S3 的能力,正好把已有的 SeaweedFS 当成内网对象存储来用——既不依赖公网云厂商,又给了数据一份异地副本。

我的环境里 SeaweedFS 已经用 local-path 跑起来,对外暴露 S3 兼容端点 http://172.29.102.221:30083,预创建了 bucket longhorn-backup。SeaweedFS 提供 S3 协议、bucket 语义、版本控制和公开读,对 Longhorn 来说就是一个”私有 OSS”。

1. 创建备份凭据 Secret

Longhorn 读的是标准 AWS 环境变量,自定义 S3 兼容存储(MinIO/SeaweedFS)必须额外提供 AWS_ENDPOINTS

1
2
3
4
5
6
7
8
9
10
11
12
# s3-backup-secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: seaweedfs-s3-secret
namespace: longhorn-system
type: Opaque
stringData:
AWS_ACCESS_KEY_ID: r6FqVNudx9AfFByqsJnZ
AWS_SECRET_ACCESS_KEY: WzY5xHoWPVYsEIOshR2YWJhzVm6Rr7a3x9xmpBwP
AWS_ENDPOINTS: http://172.29.102.221:30083
# AWS_CERT: <base64 编码的 CA,http 不需要>

2. 配置默认 BackupTarget

⚠️ 版本变更(容易踩坑):从 Longhorn 1.8 起的”多备份目标”改造后,默认备份目标不再是老的 settings.longhorn.io,而是一个名为 defaultBackupTarget CRD。网上很多教程(包括 1.7 时代的)还在让你 kubectl edit setting backup-target这条命令在 1.12 里已经被废弃,改了不生效helm --set defaultSettings.backupTarget 同理——只对全新安装生效,已运行的集群改它不会回流到 CR。

先看一眼默认 BackupTarget 的现状(通常 URL 是空的、状态 Unavailable):

1
2
3
4
5
6
7
8
kubectl -n longhorn-system get backuptargets.longhorn.io default -o yaml
# spec:
# backupTargetURL: "" ← 要填这里
# credentialSecret: "" ← 要填这里
# pollInterval: 5m0s
# status:
# conditions:
# message: backup target URL is empty

填进去有两种等价方式,推荐第一种

方式 A:直接 patch BackupTarget CR(最直接)

1
2
3
4
5
6
7
kubectl -n longhorn-system patch backuptarget.longhorn.io default --type=merge -p \
'{
"spec": {
"backupTargetURL": "s3://longhorn-backup@us/",
"credentialSecret": "seaweedfs-s3-secret"
}
}'

URL 格式拆解:s3://<bucket>@<region>/<path>/

  • longhorn-backup = SeaweedFS 里预创建的 bucket 名
  • @us = region 占位符,自定义 S3 兼容存储(MinIO/SeaweedFS)写 us 即可
  • 结尾的 / 必须有,否则 Longhorn 报错

方式 B:通过 longhorn-default-resource ConfigMap(声明式,会被 controller reconcile 到 CR)

这个 ConfigMap 是 1.8+ 引入的”默认资源”统一入口,字段名和老 setting 类似:

1
2
3
4
5
6
7
8
9
10
11
# longhorn-default-resource.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: longhorn-default-resource
namespace: longhorn-system
data:
default-resource.yaml: |
"backup-target": "s3://longhorn-backup@us/"
"backup-target-credential-secret": "seaweedfs-s3-secret"
"backupstore-poll-interval": "180"
1
kubectl apply -f longhorn-default-resource.yaml

Longhorn Manager 里的 configmap controller 会监听它,几秒内自动 reconcile 到 default 这个 BackupTarget CR 上。如果你直接 patch CR 又同时维护这个 ConfigMap,记得两者保持一致——否则 controller 下次 reconcile 可能用 ConfigMap 覆盖你对 CR 的手动改动。

验证 BackupTarget 已生效

1
2
3
kubectl -n longhorn-system get backuptargets.longhorn.io default
# NAME ENDPOINT CREDENTIALSECRET LASTSYNCEDAT AVAILABLE
# default s3://longhorn-backup@us/ seaweedfs-s3-secret ...s ago true

AVAILABLE=true 就说明 Longhorn 已经成功连上 SeaweedFS 的 S3 端点了。如果一直是 false,看 conditions.message——最常见的是凭据错、AWS_ENDPOINTS 没写、或 bucket 不存在。

3. 创建定时备份

在 Longhorn UI → Backup → 创建 Recurring Job,或直接用 CRD:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# recurring-backup.yaml
apiVersion: longhorn.io/v1beta2
kind: RecurringJob
metadata:
name: daily-backup
namespace: longhorn-system
spec:
name: daily-backup
task: backup # backup | snapshot
retain: 7 # 保留 7 份
concurrency: 2
cron: "0 2 * * *" # 每天凌晨 2 点
labels:
schedule: daily

然后给目标卷打上 label 让它纳入计划:

1
2
kubectl -n longhorn-system annotate volume <vol-name> \
recurring-job.longhorn.io/daily-backup="enabled"

4. 验证备份落地

1
2
3
4
# 用 aws-cli 列出 bucket 里的备份
aws configure set default.s3.endpoint_url http://172.29.102.221:30083
aws s3 ls s3://longhorn-backup/
# 期望:能看到 longhorn 的 backup 树形结构(按卷/快照组织)

或者直接看 Longhorn UI → Backup 页面,应该能看到 SeaweedFS 里的备份条目。

六、收尾:什么场景该用什么

回到选型本身,给一张决策表:

场景 推荐方案 理由
纯开发/临时缓存/数据丢了无所谓 local-path 最轻,资源占用几乎为零
单节点测试 + 想要快照/备份/扩容 Longhorn (副本=1) 持久性和 local-path 同级,但功能完整
多节点 + 要 HA Longhorn (副本=3) 跨节点冗余,节点挂了 Pod 能真飘走
临时挂载配置/单机调试 hostPath 不需要 CSI 的最简形态

单节点 ≠ 没有数据保护。 把 SeaweedFS 接到 Longhorn 的 BackupTarget,就完成了”本地存储 + 异地备份”的闭环——磁盘坏了,从 SeaweedFS 拉备份恢复;节点扩了,把副本数调高,数据自动在新节点重建。

这套组合在 homelab、私有云、离线内网里都跑得起来,唯一的代价是 Longhorn 比起 local-path 要重不少。值不值,看你的数据值不值。


参考链接


Longhorn 实战:让 Pod 真正跨节点漂移(含 SeaweedFS 备份 + 离线安装)
https://www.boer.xyz/posts/longhorn-k8s-storage/
作者
boer
发布于
2026年8月5日
许可协议