带本地盘的部署:升级能滚、迁移会丢数

Kubernetes本地存储hostPath有状态部署平台工程化

带本地盘的部署:升级能滚、迁移会丢数

TL;DR 私有云上给有状态应用挂本地盘,看起来只是多一个 Volume 配置;真正难的是把「运维动作」拆成两套语义——同机升级/重启保留数据,跨机迁移/删除清理数据。盘在宿主机上,Pod 可以走,数据不会跟着走。产品必须把这句话写进每一个按钮的后果里,而不是写在事后工单里。


📌 本文要点

  • 本地盘 ≠ 分布式存储:路径绑在物理机上,所有「搬家」语义都要单独设计
  • 升级能滚的关键:业务 IP 与宿主机绑定不换,Pod 重建后仍挂回同一路径
  • 迁移会丢数不是 bug:跨机 = 新盘;旧路径要主动清,否则脏数据会变成定时炸弹
  • 宕机分两种:机器还能起来 → 原地恢复;机器彻底没了 → 只能迁 + 接受丢数或走业务侧恢复
  • 产品折中:同机同集群实例上限、删除保留倒计时、操作互斥——都是在约束「本地盘的物理现实」

一、为什么还要本地盘?

无状态服务挂 emptyDir 或干脆不挂盘,Pod 随便调度,滚动升级、节点腾空都简单。

但私有云上总有一批应用就是要本地盘

典型诉求为什么网络盘/共享盘不够
日志/缓存落盘,吞吐要高本地 SSD 延迟和 IOPS 更稳
中间件有状态目录单机语义清晰,少一层网络故障域
成本与运维习惯存量物理机本地盘「已经在那儿」,不必先上共享存储集群

于是平台会提供一类 本地存储集群:配置容器内挂载路径,对应到宿主机上的普通盘或 SSD 目录。路径形态可以抽象成:

宿主机根路径/
  └── {集群名}/
        └── {分组名}/
              └── …(业务实际写入的子路径)

容器里看到的是业务配置的 mount path;宿主机上是平台按集群/分组拼出来的 hostPath
这一层看起来只是挂载,后面所有运维语义的分叉,都从这里开始


二、一个必须先钉死的物理事实

┌──────────── 物理机 A ────────────┐     ┌──────────── 物理机 B ────────────┐
│  本地盘路径                        │     │  本地盘路径                        │
│  /.../clusterX/groupY/  ← 数据在这 │     │  (空,或别的实例的数据)           │
│           ▲                        │     │           ▲                        │
│           │ hostPath               │     │           │                        │
│        Pod P1                      │     │        Pod P1'(迁过来之后)       │
│        业务 IP: 10.x.x.x           │     │        业务 IP: 10.x.x.x(可保留)  │
└────────────────────────────────────┘     └────────────────────────────────────┘
         数据不会自动复制 ─────────────────→
  • 业务 IP 可以跟着实例逻辑身份走(平台做 IP 绑定/释放)
  • 本地目录 只属于物理机 A,不会 magically 出现在 B

所以:

动作Pod业务 IP本地数据
同机重启重建可不变
同机升级(指定/滚动)重建可不变
迁到别的物理机重建可不变默认不在
删节点 / 删部署消失释放清理

「升级能滚、迁移会丢数」不是两句营销话术,而是上表的产品承诺


三、升级为什么「能滚」:把实例钉在机器上

无状态滚动升级常见套路:先起新 Pod → 就绪 → 再删旧 Pod。新 Pod 可能落在任意节点,无所谓。

本地盘集群不能这样玩。路径在宿主机上,新 Pod 必须回到原来那台机器,才能挂上原来的目录

平台侧通常会组合几件事(实现细节因系统而异,语义一致即可):

  1. 节点选择记录
    每个实例维护 业务 IP ↔ 宿主机 IP ↔ Pod 名 一类映射,部署与升级都查这张表。

  2. 调度约束
    升级重建时用 node affinity / 指定候选宿主机 IP,把 Pod 钉回原机;不是「集群里随便找一台」。

  3. 版本标签与滚动
    指定升级:只动某一个节点到目标版本,IP 与物理机绑定不变。
    滚动升级:在部署或指定升级之后,整组推进版本,每个实例仍各自钉在原物理机上

  4. 同机实例密度限制
    默认「一台物理机上,同一集群不同分组的实例」有上限(例如 100)。
    这不是拍脑袋:本地盘路径、磁盘 IO、故障域都按「同机多实例」放大,必须有天花板。

一句话概括升级路径:

换镜像、换配置、重建 Pod——可以;换宿主机——不行。
所以滚动升级在本地盘语义下是「原地滚动」,不是「自由调度滚动」。

用户感知上:点升级,实例版本变了,数据还在。这正是本地盘产品最该守住的承诺。


四、迁移为什么「会丢数」:搬家不等于搬盘

业务上「迁移」往往是:这台物理机要下线、磁盘满了、机房调整——实例必须挪到另一台机器

对本地盘实例,迁移的真实步骤更接近:

操作者 → 部署平台:指定实例迁移

    ├─→ 物理机 A:摘除旧 Pod / 解除调度绑定
    ├─→ 物理机 A:清理旧 hostPath(异步,可失败重试)
    ├─→ 平台:选新宿主机 B(避开已占用列表)
    ├─→ 平台:业务 IP 在新节点侧重新绑定
    └─→ 物理机 B:创建 Pod + 新路径挂载

结果:旧数据默认不搬;新路径是空目录

几个容易误解的点:

1. 「IP 还在」≠「数据还在」

迁移链路里常会做 IP 重置/再绑定,让业务入口尽量不变。
但这只保证网络身份连续,不保证磁盘内容连续。两者是解耦的。

2. 旧路径必须主动清理

旧机上的目录若留下:

  • 占磁盘;
  • 下次若调度巧合挂回同路径,可能读到过期/脏数据
  • 合规与租户隔离也不允许「删了实例磁盘还在」。

所以迁移/删除后会触发 按宿主机 + 路径的远程删除(节点 agent 删目录),失败则重试、节点 NotReady 则告警——清理是一等公民操作,不是可选项。

3. 这是产品边界,不是实现偷懒

本地盘产品若承诺「跨机迁移数据不丢」,就要自带:

  • 应用层复制 / 主从同步;
  • 或共享存储 / 网络盘;
  • 或显式的「备份 → 迁 → 恢复」流水线。

私有云常见折中是:平台不假装能搬本地盘,把「跨机 = 数据重新初始化」写清楚,把「同机升级保数据」做扎实。
应用若不能接受丢数,应走有状态中间件自己的副本机制,而不是指望调度器搬目录。


五、一张操作后果表(比文档正文更重要)

内部说明文档里,本地存储操作几乎可以压成下面这张表。对外产品文案也应该长这样,而不是只写「支持本地盘」。

操作业务 IP / 物理机本地数据
初次部署新绑定新路径(空)
指定升级绑定不变保留
滚动升级绑定不变保留
同机重启绑定不变保留
迁移到其他物理机换物理机(IP 可重置保留)清理(丢)
指定节点删除规模 -1清理
批量删除规模缩小清理
删除整次部署全部释放清理

把这张表钉在控制台每个危险按钮旁,比写十页 ARCHITECTURE 都管用。


六、宕机:能重启 vs 不能重启,是两种事故

本地盘故障域的边界就是那台物理机

物理机可重启

机器还能起来(内核 panic 后 reboot、短暂掉电恢复等):

  • 宿主机上的本地目录还在;
  • 节点 Ready 后,实例按原绑定拉起;
  • 数据不丢——这是本地盘相对「一迁移就清空」的最大红利。

平台侧还可给强依赖本地盘/DB 类工作负载加长 not-ready / unreachable 容忍时间,减少「机器只是抖一下就被误迁」的概率(误迁在本地盘语义下等于主动丢数)。

物理机不可重启

硬盘坏了、机器报废、整机被拔线:

  • 旧路径物理上没了或再也访问不到;
  • 只能 手动迁移到备用机
  • 平台语义下:数据丢失,或依赖业务事先备份/多副本自行恢复

这里没有魔法。诚实的产品文案应是:

单机本地盘不提供跨机高可用。HA 请用应用层副本或网络存储;本地盘只保证「同机生命周期内」的数据连续。


七、围绕「丢数」的产品缓冲:保留与清理倒计时

纯「删了就清盘」太狠;纯「永不清理」又脏。常见折中是 Hold 数据

  1. 删除/缩容时可选「暂保留本地数据(及可选保留 IP)」;
  2. 后台任务按 holdTime 倒计时;
  3. 到期前告警;到期后删路径、解绑 IP、删映射记录;
  4. 保留期内支持查询、续期、恢复或主动删除。

这解决的是操作误触与变更窗口,不是跨机 HA。
它承认:清理必须发生,但给人和业务一点缓冲——这和「迁移默认丢数」并不矛盾。


八、资源与密度:本地盘集群不是无限叠罗汉

异常场景里高频两条:

  1. 物理机资源不足 → 加机器,而不是硬塞;
  2. 同机同集群实例触顶(默认 100 这类上限) → 加机器或调配额。

本地盘集群的容量规划,不能只看 CPU/内存配额,还要看:

  • 单机磁盘容量与 IO;
  • 路径数量与清理失败堆积;
  • 故障时「一台机器挂掉影响多少实例」。

上限就是在提醒:你在用故障域很尖的存储模型。


九、和 K8s 原生概念怎么对齐(便于对外解释)

若用社区语言对照,本地存储集群接近:

平台语义接近的 K8s 概念
宿主机目录挂进容器hostPath / Local PV
升级钉死原机node affinity / 固定 nodeName 一类约束
迁移 = 新机新盘不是 PVC 在线迁移;更像删旧 PV 声明 + 新节点重建
有状态副本应用自己负责;平台 StatefulSet 也解决不了「本地盘自动搬家」

社区 Local PersistentVolume 同样强调:调度要考虑卷在哪台节点,而不是卷跟着 Pod 飞。
私有云本地盘产品,本质是把这套约束做成了运维按钮级的产品语义


十、写给使用方与平台方

使用方(业务/运维)

  1. 选本地盘前先回答:单机挂了能否接受重建?有没有应用层副本或备份?
  2. 日常变更优先同机升级/重启,不要把「迁移」当常规扩缩手段。
  3. 物理机下线流程:先业务侧迁主/备份 → 再平台迁移 → 默认按丢数验收。
  4. 删除类操作看清表:缩容、删节点、删部署都会清盘;需要缓冲就用保留期,不要赌「平台没删干净」。

平台方(做产品的人)

  1. 每个写操作写清数据后果,升级/迁移/删除三分法,禁止模糊文案。
  2. 清理与部署同级:异步删目录、NotReady 告警、失败重试、事件可查。
  3. 迁移与升级互斥:避免半升级半迁移把绑定表写乱。
  4. 不要用「自动迁移」掩盖本地盘限制:自动迁 = 自动丢数,除非上层已确认无状态或已有副本。
  5. 宕机演练分两种:reboot 恢复 vs 整机报废迁走,验收标准完全不同。

十一、收束

本地盘部署的核心矛盾就一句:

计算可以编排,本地磁盘不能瞬移。

所以产品必须诚实:

  • 升级能滚:因为我们把实例钉在原物理机上,路径还在,数据还在;
  • 迁移会丢数:因为换机就是新路径,旧路径要清,平台默认不帮你搬磁盘。

这不是功能缺陷,是存储模型的边界
边界讲清楚了,本地盘才是好用的性能与成本工具;边界糊成「和普通集群一样随便迁」,线上丢数就只是时间问题。


附录:机制速查

同机(升级 / 重启)
  绑定表:IP + host 不变
  调度:钉回原 host
  路径:原 hostPath 继续挂
  数据:保留

跨机(迁移)
  绑定表:host 变;IP 可重置保留
  调度:新 host
  路径:新 hostPath(空)+ 旧路径删除
  数据:默认丢失

删除(节点 / 批量 / 部署)
  路径删除(可 Hold 倒计时)
  映射与 IP 按策略释放
  数据:清理(Hold 只是延期)

若你的环境还在「有状态也当无状态迁」,不妨先把上面三行贴进变更 checklist——比再写一篇事故复盘便宜得多。