跳转到内容

Longhorn

starlightBlog.tags.count

K8s 存储踩坑记:强拆 Longhorn '僵尸 Pod' 钉子户

在折腾 K3s + Longhorn 搭建我的赛博影音堡垒时,我遇到了一个极其让人血压升高的玄学 Bug。

整个流水线突然停工,应用 Pod 疯狂报错无法启动,而罪魁祸首,居然是一个在系统里“死活退不出来”的僵尸进程。

一切起因于我修改了媒体应用(Radarr/Sonarr)的配置文件并重新部署。新 Pod 迟迟无法启动,查看事件日志,赫然飘着那句经典的存储死锁报错:

Multi-Attach error for volume ... Volume is already exclusively attached to one node and can't be attached to another

意思是:存储卷正被旧节点死死锁着,新 Pod 拿不到权限。

顺藤摸瓜,我发现底层负责管理挂载的 longhorn-csi-plugin Pod 一直卡在 Terminating(正在终止)状态,时长超过了 20 分钟!查看它的详细日志,看到了这句致命的超时宣告:

error killing pod: failed to "KillContainer" for "longhorn-liveness-probe" ... rpc error: code = DeadlineExceeded desc = context deadline exceeded

🧠 赛博验尸报告:为什么它“杀不死”?

Section titled “🧠 赛博验尸报告:为什么它“杀不死”?”

表面上看是 Kubernetes 出了 Bug,但深入探究,这其实是 Linux 内核底层的自我保护机制导致的。

  1. I/O 阻塞:由于之前尝试跨网段挂载 NFS 导致了网络超时,底层的存储进程卡在了“等待磁盘响应”的状态。
  2. 内核级锁死 (D-state):在 Linux 中,当进程卡在底层 I/O 等待时,内核会将其标记为 不可中断睡眠 (Uninterruptible Sleep, D状态)。在这个状态下,进程会无视任何外部信号(包括 kill -9)。
  3. Kubelet 懵逼:Kubernetes 的包工头 Kubelet 给容器引擎下达了“拔管”指令,但底层容器因为 D 状态根本不鸟它。Kubelet 苦等 2 分钟后无奈宣布超时(DeadlineExceeded),于是这个 Pod 就成了一个永远卡在 Terminating 的“钉子户”,并且牢牢霸占着 Longhorn 的存储锁。

对付这种级别的钉子户,常规的清理已经没用了。必须采取暴力强拆手段。

第一板斧:K8s 逻辑强抹除(本次生效的绝招)

Section titled “第一板斧:K8s 逻辑强抹除(本次生效的绝招)”

既然正常的优雅退出走不通,那就直接在 K8s 的记录本上把它强行划掉,不给任何宽限时间。

执行以下命令强杀卡死的 Pod:

Terminal window
kubectl delete pod <卡住的Pod名称> -n longhorn-system --force --grace-period=0

效果:命令敲下的瞬间,Pod 从列表中消失,Longhorn 控制平面终于意识到锁可以释放,随后新 Pod 瞬间挂载存储卷成功,业务恢复!

第二板斧:重启底层容器引擎(备用方案)

Section titled “第二板斧:重启底层容器引擎(备用方案)”

如果在执行完第一步后,kubectl get pods 里确实没它了,但应用依然报 Multi-Attach error,说明底层 Linux 进程依然在霸占资源。此时需要重启 K3s 的 agent 服务来重置容器状态:

Terminal window
# 登录出问题的 K3s 工作节点执行
sudo systemctl restart k3s-agent
# 如果是主节点则执行 restart k3s

第三板斧:物理重启(终极毁灭)

Section titled “第三板斧:物理重启(终极毁灭)”

如果前两招都打完,存储依然处于死锁状态,说明 Linux 内核的 I/O 队列已经彻底崩溃。别犹豫,直接 sudo reboot 重启该节点所在的物理机或虚拟机,这是清理 D 状态进程最彻底的方案。

这次踩坑不仅让我深入理解了 Kubernetes 和底层操作系统的联动关系,也总结出了一条铁律:

在 HomeLab 环境中,对于独占存储(ReadWriteOnce)的单副本有状态应用(如 qBittorrent、数据库等),在 deployment.yaml 中务必将更新策略设置为重建!

spec:
replicas: 1
strategy:
type: Recreate # 强制先杀旧,再启新,绝不抢锁

遇到 Terminating 不要慌,找准病根,果断拔管!

赛博拆迁办:安全定向爆破 Longhorn 分布式存储大坝

在 Kubernetes 的世界里,无状态应用(如之前部署的 Headlamp)卸载起来就像拔掉 U 盘一样简单。但是,存储组件是集群的“承重墙”

Longhorn 作为底层存储大坝,掌管着所有容器的命脉数据。为了防止新手误操作导致“删库跑路”的悲剧,Longhorn 官方在底层上了一道极其死板的“物理锁”。如果你直接暴力执行卸载,整个 longhorn-system 命名空间会永远卡在 Terminating(挂起)状态,你的 K8s 集群将陷入无尽的死锁僵局。

今天,包工头就结合官方的卸载与排错指南,带你按照正规的“定向爆破协议”,一步步安全、干净地拆除这座大坝,并解决拆除过程中可能遇到的所有疑难杂症。

⚠️ 拆除前置警告:清退所有租客!

Section titled “⚠️ 拆除前置警告:清退所有租客!”
极度危险

在执行任何卸载命令前,为了防止损坏集群,官方强烈建议: 你必须手动删除所有正在使用 Longhorn 卷的 Kubernetes 工作负载(包括 PersistentVolume, PersistentVolumeClaim, StorageClass, Deployment, StatefulSet 等)。 一旦拆除开始,所有基于 Longhorn 创建的虚拟硬盘数据将瞬间灰飞烟灭!请务必提前做好数据备份。


  1. 解除数据自毁保护锁 (The Safety Catch)

    这是卸载 Longhorn 最核心、最不可省略的一步。默认情况下 deleting-confirmation-flag 是关闭的,卸载任务会直接报错拦截。我们需要强制告诉 Longhorn 控制器:“我确定要销毁一切”。

    在终端敲入这行指令,修改核心配置,允许删除操作:

    Terminal window
    kubectl -n longhorn-system patch -p '{"value": "true"}' --type=merge lhs deleting-confirmation-flag

    预期输出:setting.longhorn.io/deleting-confirmation-flag patched。保护锁现已解除。

  2. 呼叫 Helm 拆迁队 (Uninstall Release)

    保护锁解除后,我们就可以正常呼叫 Helm 执行反向拆除了。它会自动释放 Cilium 分配的 LoadBalancer IP,并遣散所有节点的存储引擎容器。

    Terminal window
    helm uninstall longhorn -n longhorn-system

    稍等片刻,直到终端提示 release "longhorn" uninstalled

  3. 清理大坝废墟 (Delete Namespace)

    确认 Helm 卸载完毕后,我们将整个存储专区物理抹除:

    Terminal window
    kubectl delete namespace longhorn-system
  4. 打扫物理宿主机的残渣 (Node Data Cleanup)

    注意:这一步需要到你所有的 3 台 Ubuntu 物理机/虚拟机上分别执行! 虽然 K8s 里的组件删除了,但 Longhorn 之前在你的宿主机磁盘上生成的物理数据块依然保留着。为了彻底归还磁盘空间,直接使用 rm 大法:

    Terminal window
    sudo rm -rf /var/lib/longhorn
  5. 恢复 Linux 内核秩序 (可选)

    如果你确定这几台机器以后再也不碰基于 iSCSI 的存储,可以把之前我们立下的“开机规矩”撤销掉。

    Terminal window
    sudo rm /etc/modules-load.d/longhorn.conf
    sudo systemctl disable --now iscsid

🛠️ 疑难杂症与抢修指南 (Troubleshooting)

Section titled “🛠️ 疑难杂症与抢修指南 (Troubleshooting)”

在赛博工地,意外总是难免的。如果你在卸载过程中遇到了卡死、报错,或者突然“手滑”后悔了,请参考以下官方急救方案:

💊 症状 1:手滑卸载了,但我不想删!(取消卸载)

Section titled “💊 症状 1:手滑卸载了,但我不想删!(取消卸载)”

如果你不小心执行了 helm uninstall(且当时没开保护锁导致它卡在 uninstalling 状态),你可以利用 Helm 的时光机功能紧急回档。

抢修指令:

Terminal window
# 1. 查找 Longhorn 卸载前的最后一个正常版本号 (REVISION)
helm list -n longhorn-system -a
# 2. 假设查到上一个正常版本是 1,执行强制回滚:
helm rollback longhorn 1 -n longhorn-system

提示 Rollback was a success! 代表大坝抢修成功,数据保住了!

💊 症状 2:Namespace 一直卡在 Terminating,CRD 删不掉

Section titled “💊 症状 2:Namespace 一直卡在 Terminating,CRD 删不掉”

这是 Kubernetes 卸载存储组件最常见的恶疾:Finalizer 幽灵锁。因为底层引擎已经被你删了,K8s 还在傻傻等待底层引擎来确认删除这些 CRD(自定义资源),从而形成死锁。

抢修指令(暴力清除所有 Longhorn 状态): 执行以下脚本,它会遍历所有 Longhorn 的 CRD,强行抹除它们的 Finalizer,然后连根拔起。

Terminal window
# 批量强拆:剥夺所有 Longhorn 资源的终结器
for crd in $(kubectl get crd | grep longhorn.io | awk '{print $1}'); do
kubectl get $crd -n longhorn-system -o name 2>/dev/null | xargs -I {} kubectl patch {} -n longhorn-system -p '{"metadata":{"finalizers":null}}' --type merge 2>/dev/null
done

💊 症状 3:执行清理脚本时报错 Webhook 找不到

Section titled “💊 症状 3:执行清理脚本时报错 Webhook 找不到”

如果你在执行上面那个清理脚本时,K8s 抛出了类似这样的错误: Internal error occurred: failed calling webhook "validator.longhorn.io"... service "longhorn-admission-webhook" not found

这是因为残缺的卸载过程把 Webhook 服务删了,但注册表里还留着它的名字,导致 K8s 每次修改资源都想去请求一个不存在的验证服务。

抢修指令: 删除这些拦截请求的幽灵配置,为清理脚本放行:

Terminal window
kubectl delete ValidatingWebhookConfiguration longhorn-webhook-validator
kubectl delete MutatingWebhookConfiguration longhorn-webhook-mutator

删除这两个配置后,重新执行症状 2中的 CRD 清理脚本,即可丝滑通关。


历经波折后,执行最后的扫尾质检:

Terminal window
kubectl get crd | grep longhorn
kubectl get ns | grep longhorn

如果上述命令没有任何输出,恭喜你,这座赛博存储大坝已经被完美定向爆破,所有的顽疾和锁链都已被彻底斩断!

K3s + Cilium 踩坑实录:Longhorn 插件无限重启?揪出 eBPF 路由脑裂的幕后黑手

大家好,我是赛博包工头。欢迎来到赛博工地。

在咱们上一期对 K3s 故障节点进行了“核弹级强拆”并重新加入集群后,本以为可以顺利盖起 Longhorn 存储大坝了。结果,Longhorn 的核心存储特派员 longhorn-csi-plugin 却一直在无限崩溃重启(CrashLoopBackOff)。

表面上看是存储插件坏了,但经过一顿抽丝剥茧,最后发现:这根本不是存储的锅,而是一起典型的网络大动脉断裂案!

今天这篇排错实录,我们就来复盘一下如何解决这种“查号台还活着,但去查号台的路被炸断了”的底层网络脑裂问题。


💥 案发现场:伪装成存储故障的网络崩塌

Section titled “💥 案发现场:伪装成存储故障的网络崩塌”

当时查看 longhorn-csi-plugin 的崩溃遗言(Logs),发现了极其关键的一行报错:

E0419 00:36:26.346354 1 main.go:167] "Error connecting to CSI driver" err="context deadline exceeded"
time="2026-04-19T00:36:30.790662906Z" level=warning msg="Failed to initialize Longhorn API client Get \"http://longhorn-backend:9500/v1\": dial tcp: lookup longhorn-backend on 10.61.0.10:53: read udp 10.60.2.244:53943->10.61.0.10:53: i/o timeout. Retrying"

日志翻译: Longhorn 插件启动时,需要找集群内部的“114查号台”(CoreDNS,IP 为 10.61.0.10:53)去查询总控中心的地址。结果请求发出去后,如同石沉大海,直接报了 i/o timeout(连接超时)。因为拿不到总控地址,插件干脆拒绝启动,最后被 Kubelet 当做死进程给毙了。


🕵️‍♂️ 抽丝剥茧:派出侦察兵探路

Section titled “🕵️‍♂️ 抽丝剥茧:派出侦察兵探路”

为了验证是不是真的连不上 DNS,我跑到后台看了一眼 CoreDNS 的状态,发现它分明是 1/1 Running,活得好好的,安安稳稳地跑在 k3s-master-02 节点上。

接着,我拉起了一个带 nslookup 工具的 busybox 侦察兵进行极限测试:

Terminal window
kubectl run -i --tty --rm debug-net --image=busybox --restart=Never -- nslookup kubernetes.default

返回的结果非常绝望:

;; connection timed out; no servers could be reached

结论:查号台活得好好的,但去找查号台的路,全断了。


🧠 底层逻辑:大动脉为什么会断?

Section titled “🧠 底层逻辑:大动脉为什么会断?”

这要归咎于咱们这套集群的高级架构。

在安装 K3s 时,我们加上了 --disable-kube-proxy 参数。这意味着 Kubernetes 传统的基于 iptables 的内部流量转发机制被废弃了。全集群的 Service IP(包括内部 DNS 地址 10.61.0.10),全靠 Cilium 的 eBPF 机制在 Linux 内核底层进行拦截和高效转发。

但是!咱们刚才对 k3s-master-03 执行了“核弹级强拆”,导致整个集群的 Cilium 路由映射表(BPF Maps)出现了严重的数据断层和脑裂。

Cilium 彻底迷失了方向,不知道该怎么把 UDP 53 端口的请求跨节点发给 k3s-master-02 上的 CoreDNS,于是网络包在内核层直接被无情丢弃。底层网络一断,上层依赖内部域名的 Longhorn 存储插件自然跟着全军覆没。


🛠️ 抢修方案:网络管线“大换血”

Section titled “🛠️ 抢修方案:网络管线“大换血””

既然病根在底层的路由表缓存,咱们不需要重装,只需要强制集群对网络管线进行一次滚动重启(Rollout Restart),让 eBPF 重新绘制整张全网路由表即可。

请依次执行这排雷三把斧

第一斧:强制重建 Cilium 网络映射表

Section titled “第一斧:强制重建 Cilium 网络映射表”

让所有节点的 Cilium 特派员重新扫描集群,重写内核里的路由规则:

Terminal window
kubectl rollout restart ds cilium -n kube-system

(敲完后喝口水等个半分钟,让 3 个节点的 Cilium Pod 挨个重启完成)

第二斧:顺手重启查号台 (CoreDNS)

Section titled “第二斧:顺手重启查号台 (CoreDNS)”

为了防止 CoreDNS 本身残留什么僵尸缓存,给它也来一记还我漂漂拳:

Terminal window
kubectl rollout restart deploy coredns -n kube-system

第三斧:再次派出侦察兵验证验收

Section titled “第三斧:再次派出侦察兵验证验收”

等上面的网络和 DNS Pod 都处于 Running 状态后,重新敲一遍之前的测试命令:

Terminal window
kubectl run -i --tty --rm debug-net --image=busybox --restart=Never -- nslookup kubernetes.default

💡 预期结果: 只要这次没有报 timed out,而是瞬间返回了类似 Server: 10.61.0.10Address: 10.61.0.10:53 的解析结果(即便带了 NXDOMAIN 的小 Bug 也不影响),就说明全集群的大动脉彻底打通了!

大动脉一通,之前卡在 CrashLoopBackOfflonghorn-csi-plugin 就会瞬间找回组织,自动停止报错并挂载成功。

包工头施工笔记: 排查 K8s 故障时,如果上层应用报“连接超时”,不要急着重装应用。拔出网络排错“侦察兵”测一下底层连通性,往往能事半功倍。“遇到 eBPF 脑裂,一键 Rollout 重启网络插件”,这是赛博工地里必须掌握的保命神技!

彻底清理并重新部署 Portainer:解决存储与权限冲突

在通过 Helm 将 Portainer 的底层存储从 local-path 更改为 longhorn 时,直接执行 helm upgrade 通常会导致升级失败。这主要由以下三个原因造成:

  1. PVC Immutable 报错:Kubernetes 规范限制了已绑定 PVC 的 storageClassName 是不可变的,不能直接修改覆盖。
  2. 全局权限冲突:Helm 尝试创建全局的 ClusterRoleBinding 时,会因为系统中存在历史安装遗留的同名资源而报错。
  3. 手动资源残留:通过 kubectl apply 手动创建的 Service(例如手动配置的 LoadBalancer)不在 Helm 的管理范围内,helm uninstall 不会自动将其回收。

为了保证新配置能够顺利应用,必须先彻底清理命名空间下的所有关联资源,然后执行干净的重装。以下是完整的操作步骤。


注意: 执行以下清理命令将删除该节点上 Portainer 的所有历史数据。请在操作前确认数据已备份或不再需要。

首先,通过 Helm 卸载现有的 Portainer 实例:

Terminal window
helm uninstall portainer -n portainer

2. 删除命名空间下的所有资源及 PVC

Section titled “2. 删除命名空间下的所有资源及 PVC”

由于 Helm 卸载操作会保留 PVC 以及未被其管理的手动资源,需要使用以下命令强制清空 portainer 命名空间:

Terminal window
kubectl delete all --all -n portainer
kubectl delete pvc --all -n portainer

ClusterRoleClusterRoleBinding 是集群级别的资源,不包含在特定命名空间内。需要手动删除它们以解决后续的权限冲突报错:

Terminal window
kubectl delete clusterrolebinding portainer
kubectl delete clusterrole portainer

执行以下命令检查命名空间,确认环境已完全清空:

Terminal window
kubectl get all,pvc -n portainer

正常情况下,终端应返回:No resources found in portainer namespace.


环境清理完毕后,使用配置好 Longhorn 存储的 portainer-values.yaml 文件重新执行 Helm 安装命令:

Terminal window
helm install portainer portainer/portainer \
-n portainer \
--create-namespace \
--set persistence.storageClass=longhorn \
--set nodeSelector=null \
-f portainer-values.yaml

第三阶段:验证部署与恢复服务

Section titled “第三阶段:验证部署与恢复服务”

安装完成后,检查 Pod 和 PVC 的状态。确认 PVC 已经绑定到 longhorn,并且 Pod 处于 Running 状态:

Terminal window
kubectl get pods,pvc,svc -n portainer -o wide

2. 恢复手动创建的 Service (视情况执行)

Section titled “2. 恢复手动创建的 Service (视情况执行)”

如果你之前的外网访问依赖于手动创建的 Service(例如名为 portainer-lb-manual 的资源),并且该配置没有整合进 Helm 的 values.yaml 中,你需要重新应用该配置文件来恢复服务的对外暴露:

Terminal window
# 请将文件名替换为你实际使用的 yaml 文件
kubectl apply -f portainer-lb-manual.yaml

执行完毕后,即可通过分配的 IP 地址访问 Portainer 并完成初始管理员密码的设置。