跳转到内容

工地日记

彻底清理并重新部署 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 并完成初始管理员密码的设置。

架构师的外科手术:破解 kube-vip 启动死循环

💡 前置联动说明 本文是对 K3s 高可用集群浇筑 (HA Setup) 章节中“补丁 2”配置的深度原理解析。

  • 🕵️ 发现异常:如果不加该补丁,仅照抄基础部署清单,kube-vip 容器会无限 CrashLoopBackOff,日志疯狂报错 Timeout(连接 API Server 超时)。
  • 🧠 根本原因:底层网络插件(Cilium)未完全就绪,或者 Kube-proxy 被卸载时,组件向集群内部虚拟 IP(10.61.0.1)寻址产生的“先有鸡还是先有蛋”的死循环。
  • 🛠️ 解决方案:通过注入宿主机物理回环地址 (127.0.0.1) 强行搭桥,实现控制面与虚拟网络的彻底解耦。

在打造这套赛博堡垒的高可用地基时,如果直接照搬基础的部署清单,我们大概率会在 kube-vip 的启动阶段摔得头破血流。今天,包工头就带你从底层扒开这个“补丁”的硬核逻辑,复盘我们是如何斩断这个死循环的。


💣 灾难复盘:为什么会疯狂 Timeout?

Section titled “💣 灾难复盘:为什么会疯狂 Timeout?”

在默认情况下,Kubernetes 集群里的任何 Pod(包括 kube-vip)想要呼叫“总部”(API Server),都会去请求一个内部的虚拟 IP(比如我们配置的 10.61.0.1 这个 ClusterIP)。

但这就产生了一个致命的死循环

  1. kube-vip 启动了,它需要和另外两台机器商量谁当“带头大哥”(Leader Election),这必须读写 API Server 的数据。
  2. 于是 kube-vip 向上级汇报,把请求发给了虚拟网关 10.61.0.1
  3. 但是! 此时底层的 Cilium 网络插件可能正在重启,或者还没完全就绪;又或者我们在浇筑阶段强行卸载了 kube-proxy,导致这个虚拟 IP 10.61.0.1 根本没人给它做底层转发。
  4. 结果kube-vip 找不到总部,一直苦苦等待响应,最后只能抛出绝望的 Timeout 并崩溃重启。大门失守,高可用形同虚设。

🗝️ 破局之法:精准搭桥 (外科手术)

Section titled “🗝️ 破局之法:精准搭桥 (外科手术)”

为了打破这个死循环,我们在 kube-vip.yaml 中通过 sed 命令强行注入了这两行环境变量:

- name: KUBERNETES_SERVICE_HOST
value: "127.0.0.1"
- name: KUBERNETES_SERVICE_PORT
value: "6443"

它的作用,就是kube-vip 塞了一张“本地特权 VIP 通行证”!

由于我们在部署时声明了 --inCluster 并开启了 hostNetwork: true(共享宿主机网络),当我们强制注入 127.0.0.16443 后,kube-vip 就不再去那个虚无缥缈的 10.61.0.1 绕弯子了。

它会直接“敲隔壁的门”——通过主机的本地回环网卡(Localhost),呼叫本台物理机上正在运行的 K3s API Server! 管你底层的 Cilium 瘫没瘫痪,管你 K8s 的虚拟网络通没通,我们走的是物理机的地下通道,绝对畅通无阻!这相当于给我们的赛博堡垒建立了一条最高安全级别的消防通道

我们的 KUBERNETES_SERVICE_HOST 注入是在做精密的外科手术。通过注入本地回环地址,实现控制面组件与虚拟网络的硬解耦,彻底斩断网络启动期的死循环锁。

赛博工地的“五分钟定律”:揭秘节点宕机后的 Pod 驱逐真相

在我们的 K3s 高可用堡垒中,我进行了一次“暴力断电”演习:直接关掉了 k3s-master-03 节点。

当时的情况如下:

  • 业务中断:原本运行在 03 节点上的 Headlamp(单副本)瞬间无法访问。
  • 状态迷惑:控制台输入 kubectl get pods -o wide,发现该 Pod 居然还显示为 Running,且依然赖在已经失联的 03 节点上。
  • 没有漂移:想象中的“毫秒级瞬移到其他节点”并没有发生。

这种“僵尸 Pod”现象并非 Bug,而是触发了 Kubernetes 调度架构中极其核心的机制:基于污点的驱逐(Taint-based Eviction)与五分钟定律

📖 官方原典参考:本文底层逻辑均来源于 Kubernetes 官方文档:污点和容忍度


要理解这五分钟,必须先看懂 K8s 官方定义的一对博弈属性,我们直接用命令在案发现场求证:

1. 污点 (Taint) —— 节点的“逐客令”

Section titled “1. 污点 (Taint) —— 节点的“逐客令””

03 节点断电失联时,K8s 控制面会迅速给该节点打上严厉的污点。 🕵️ 求证指令:我们怎么确定节点真的被打上污点了?

Terminal window
# 查看 03 节点的污点状态
kubectl describe node k3s-master-03 | grep Taints

输出结果:你会看到 Taints: node.kubernetes.io/not-ready:NoExecute, node.kubernetes.io/unreachable:NoExecute。 这就是节点的逐客令。NoExecute 效果意味着:不仅新 Pod 不能来,已经在上面的 Pod 必须立刻被驱逐!

2. 容忍度 (Toleration) —— Pod 的“特权通行证”

Section titled “2. 容忍度 (Toleration) —— Pod 的“特权通行证””

面对驱逐令,Pod 能否抗旨?这就看它自身有没有配置对应的容忍度。


🕵️ 破案:这 300 秒是从哪来的?

Section titled “🕵️ 破案:这 300 秒是从哪来的?”

既然污点是 NoExecute (立刻驱逐),为什么 Headlamp 还能在上面赖 5 分钟?这是因为 API Server 在创建普通 Pod 时,默认、悄悄地给它们注入了限时的容忍度。

🕵️ 求证指令:抓出 Pod 身上被暗中注入的“宽限期”

Terminal window
# 查看 Headlamp 这个 Pod 的详细配置
kubectl describe pod <你的-headlamp-pod-名字> -n kube-system | grep -A 5 Tolerations

输出结果:你会赫然发现这样两行配置:

node.kubernetes.io/not-ready:NoExecute op=Exists for 300s
node.kubernetes.io/unreachable:NoExecute op=Exists for 300s

官方文档解读:“这些自动添加的容忍度意味着 Pod 可以在检测到对应的问题之一时,在 5 分钟内保持绑定在该节点上。” 即:长官让我滚,但我有 300 秒的宽限期。


🏗️ 架构思考:DaemonSet 为什么永远不漂移?

Section titled “🏗️ 架构思考:DaemonSet 为什么永远不漂移?”

Ciliumkube-vip 这类组件,即便过了 5 分钟也不会去别的节点重新拉起。

🕵️ 求证指令:看看 DaemonSet 的容忍度有何不同?

Terminal window
# 使用 -A 10 确保能把 DaemonSet 身上长长的一串特权全打印出来
kubectl describe pod -l app.kubernetes.io/name=kube-vip-ds -n kube-system | grep -A 10 Tolerations

输出结果:你会发现它们也有 not-readyunreachable 的容忍度,但是没有 for 300s 这个时间限制!

官方文档明确指出:“DaemonSet 中的 Pod 被创建时,针对不可达污点添加的 NoExecute 容忍度,将不会指定 tolerationSeconds。这保证了出现上述问题时 DaemonSet 永远不会被驱逐。”


⚡ 极客进阶:掌控驱逐倒计时的“快与慢”

Section titled “⚡ 极客进阶:掌控驱逐倒计时的“快与慢””

作为集群的架构师,你可以通过在 YAML 的 spec.tolerations 中显式声明,来覆盖这默认的 300 秒:

  • 场景 A(无状态前端):想让面板瞬间漂移?配置 tolerationSeconds: 30(警告:容易引发网络抖动时的重建风暴)
  • 场景 B(有状态存储):数据库绑了本地硬盘?必须延长保命!配置 tolerationSeconds: 6000 死等网络恢复。

  1. 架构侧:核心业务增加副本数(Replicas >= 2)配合反亲和性,是应对这 5 分钟断档期的最优雅解法。
  2. 应急侧:演练时不想等待?直接执行强制抹除:kubectl delete pod <POD_NAME> -n <NAMESPACE> --force --grace-period=0
  3. 排查侧:想要系统性掌握这套机制的排查手法?👉 点击查看《赛博工地巡检手册:污点与容忍度排查指令集》

赛博赶羊战术:不改 YAML 强制定向调度 Pod

在赛博堡垒的日常巡检中,我们偶尔会萌生一些“非分之想”:我想把正在运行的 Headlamp 面板,强行从 01 节点挪到 03 节点上去。

按照正规军的做法,我们应该去修改 Deployment 的 YAML 文件,加上 nodeSelector 或节点亲和性。但如果这仅仅是一次临时测试,为了这么点小事去改核心图纸,未免太兴师动众。如果给 0102 节点打 NoSchedule 污点,又会像“核弹打蚊子”一样误伤集群里的其他无辜业务。

今天,包工头教你一招极客圈里经典的“障眼法”——利用节点封锁 (Cordon) 机制的“赛博赶羊战术”


🐑 战术核心:关上多余的门,只留一条路

Section titled “🐑 战术核心:关上多余的门,只留一条路”

Kubernetes 提供了一个专门用于节点临时维护的命令:kubectl cordon(封锁)。 当节点被 Cordon 后,它会被打上 SchedulingDisabled 的标记。这就像给节点挂上了“暂停营业”的牌子:已经在里面吃饭的顾客(运行中的 Pod)不受影响,但绝对不接待新客(新创建的 Pod 无法调度进来)。

我们的战术逻辑极其简单:把不想去的节点全封锁,然后把 Pod 杀掉让它重生。K8s 大脑环顾四周发现只有一台节点“开着门”,就只能乖乖把 Pod 丢进去。


假设我们要把 kube-system 命名空间下的 Headlamp 强行赶到 k3s-master-03 节点。

第一步:封锁非目标节点 (关门)

Section titled “第一步:封锁非目标节点 (关门)”

我们要逼迫目标去 03,所以先把 0102 的大门焊死:

Terminal window
kubectl cordon k3s-master-01 k3s-master-02

验证:此时敲击 kubectl get nodes,你会看到 0102 的 STATUS 变成了 Ready,SchedulingDisabled

第二步:击杀当前 Pod (放狗咬羊)

Section titled “第二步:击杀当前 Pod (放狗咬羊)”

直接删掉当前正在运行的 Headlamp Pod。由于它是由 Deployment 管理的,K8s 会瞬间拉起一个新的替代品。

Terminal window
kubectl delete pod -l app.kubernetes.io/name=headlamp -n kube-system

去看看新重生的 Headlamp 落在哪了:

Terminal window
kubectl get pods -n kube-system -o wide | grep headlamp

因为大脑别无选择,你会发现新的 Pod 已经精准无误地降落在了 k3s-master-03 上!微操成功!

第四步:光速解除封锁 (极其重要!)

Section titled “第四步:光速解除封锁 (极其重要!)”

羊已经进圈了,赶紧把 0102 的大门重新打开,否则你后续部署的任何新业务都会因为找不到节点而卡在 Pending 状态。

Terminal window
kubectl uncordon k3s-master-01 k3s-master-02

验证:再次 kubectl get nodesSchedulingDisabled 标记消失,集群恢复常态。


虽然这套连招玩起来行云流水,极其酷炫,但我必须提醒你:它终究是一张“体验卡”。

Kubernetes 的核心信仰是声明式架构(Declarative)。Cordon 大法只是一次性欺骗了调度器。如果明天 03 节点重启了,或者 Headlamp 意外崩溃了,K8s 大脑在重新调度时,面对三扇全开的大门,依然会随机把 Headlamp 扔回 0102。系统并没有记住你的真实意图。

总结:

  • 临时测试、排查干扰:用 Cordon 大法,干净利落。
  • 永久固化、生产要求:请务必遵守纪律,老老实实去写 nodeSelectornodeAffinity。这才是让 Tech Fortress 坚不可摧的终极规范。

blog1

“所谓的高可用,就是当你拔掉电源的时候,它确实不可用了。” > 欢迎来到赛博工地的最底层。这里没有优雅的代码,只有嗡嗡作响的散热风扇、错综复杂的网线,以及用金钱堆砌起来的虚假安全感。

starlightBlog.post.tags