CubeStudio 多机部署这件事说实话我犹豫了很久才动手。之前一直用单机模式跑三台GPU服务器各管各的模型训练任务手动分发数据靠U盘和网盘倒腾项目组之间抢卡全靠吼。后来赶上团队扩张、多个项目并行单机模式彻底撑不住了才下定决心做多机、多集群、多资源组改造。这篇文章把整个过程的思路、步骤和踩过的坑都记下来给同样在摸索的人一个参考。1. 为什么单机模式必然走到头算力孤岛和运维黑洞先说说我当时遇到的实际问题。CubeStudio 单机部署确实快一条命令装完就能拉起界面几个工程师自己用完全够。但当规模涨到四五台机器、二三十号人同时使用时单机模式的问题就暴露得特别明显模型训练需要跨节点的大显存单机搞不定数据放在某一台机器上换台机器跑任务就要重新拷项目组之间没有隔离A 组的任务把 GPU 占满B 组连个测试任务都排不进去。有人可能会想那我把 CubeStudio 在每台机器上都装一遍不就成了多机这条路我试过短期看确实能分散压力但实际是制造了更多的算力孤岛和运维黑洞每台机器一套独立的实例用户要记住不同的访问地址任务和数据完全割裂监控也看不到全局视角。这就像每家每户自己挖一口井而不是统一的自来水管道系统水量再大也是分散的。所以我当时给自己定了一个改造方向以 Kubernetes 为底座把多台物理机汇聚成一个统一的资源池CubeStudio 作为上层的 AI 算力管理平台负责接收用户的训练任务、自动调度到合适的节点、按项目组分配资源额度。这样一来用户接触的还是同一个 CubeStudio 界面但背后的算力已经在多台机器上自由流转。这个思路的核心是把机器管理和算力管理两个层面分开机器层面由 K8s 统一纳管算力层面由 CubeStudio 统一调度。后面所有实操步骤都是围绕这两层展开的。2. 扩容前的硬门槛网络、存储、GPU 这三件事必须先想清楚多机部署不是把机器拉到一起就能干的前置条件不满足后面全是坑。我踩得最狠的三块是网络、存储和 GPU 环境。2.1 网络规划主机名、IP 段和端口一个都不能乱多机模式下节点之间要通过网络通信所以必须提前规划好 IP 和主机名。我当时的规划方法是所有 GPU 服务器放到同一个二层网络里保证相互之间低延迟高带宽至少万兆网卡每台机器设置固定的主机名如gpu-node-01、gpu-node-02并在/etc/hosts里写全所有节点的映射避免 K8s 集群内部通过 DNS 解析出现混乱GPU 服务器之间可能需要大的模型文件传输光靠管理网络跑会卡死业务有条件的话单独拉一条数据网络或者做网卡 bond。另外要确认几个关键端口能互通Kubernetes API Server 的 6443、节点上 kubelet 的 10250、etcd 的 2379-2380以及 CubeStudio 自身的 Web 端口和各类组件之间的内部端口。我见过不少团队前面步骤都装好了最后卡在防火墙规则上节点状态反反复复地NotReady排查了一个晚上才发现是 10250 端口被安全策略挡了。2.2 存储规划模型权重和数据集必须共享单机模式下数据放在本机磁盘就行。多机部署后同一个训练任务可能被调度到任意一台节点上运行如果数据只在某一台机器上那任务跑到别的机器上就找不到数据了。最直接的解法是上共享存储。我没有一上来就搞复杂的分布式存储而是先上了 NFS一台存储服务器作为 NFS Server各 GPU 节点作为 Client 挂载同一个目录模型权重、数据集、输出目录全部放在共享存储上。CubeStudio 在提交任务时容器里挂载的路径是统一的/data底层映射到 NFS 共享目录这样无论任务被调度到哪台节点访问的数据都是一致的。这里有个细节值得提醒NFS 的挂载参数要注意权限和锁。多节点同时读写同一个目录文件名冲突、缓存不一致的情况时有发生建议挂载时加上nolock,noatime,actimeo60这类参数并统一使用同一个 UID/GID 启动容器避免权限错乱。等规模再大一点可以升级到 CephFS 或 JuiceFS 这类分布式文件系统但起步阶段 NFS 完全够用。2.3 GPU 环境驱动、containerd 和运行时一条链GPU 是多机部署里最容易出问题的地方。我们当时每台机器显卡型号还不完全一样有 A 卡有 N 卡折腾了很久才把环境对齐。确认了以下几点才敢往下走每台节点安装与显卡匹配的驱动版本并保证驱动版本在同一大版本内容器运行时我们用的是 containerd要配置好 nvidia-container-runtime让容器能真正访问 GPU 设备提前把 GPU 型号对应的调度标签打上比如gpu-typea100、gpu-type3090方便后面按项目组分算力时做精细调度。这里最容易犯的错是只在部分节点配好了 GPU 环境结果任务被调度到没配好的节点上容器起来了却拿不到nvidia-smi的输出报一堆奇怪的驱动错误。排查半天最后发现是nvidia-device-plugin这个 DaemonSet 在部分节点上没正常启动。所以强烈建议多机部署前先把所有节点都打成一个统一基线驱动版本、容器运行时版本、共享存储挂载、时间同步全部一致。3. 多机部署实操从主节点初始化到 worker 节点加入这一部分是整个改造的核心动作。以下步骤基于常规的 Kubernetes 部署方式实际环境可以根据版本和网络情况微调。3.1 初始化主节点Master我选择其中一台高性能机器作为 K8s 主节点跑控制面组件和 CubeStudio 的调度服务。初始化前需要先做几件事关闭 swap 分区K8s 的 kubelet 默认要求加载内核模块br_netfilter并设置net.bridge.bridge-nf-call-iptables1配置时间同步我这里用的是chrony所有节点指向同一台时间服务器避免证书校验因时间偏差而失败。初始化命令大致是kubeadm init \ --apiserver-advertise-address192.168.1.10 \ --pod-network-cidr10.244.0.0/16 \ --service-cidr10.96.0.0/16初始化成功后kubeadm 会输出两段关键信息一段是kubeadm join命令和 token另一段是用于生成管理员配置的kubeconfig。token 默认 24 小时过期但后面还要加 worker 节点建议把输出内容存好或者提前在需要时再生成新 token。3.2 安装容器网络插件CNI多机部署和单机最大的区别之一就是容器网络必须跨节点互通。我们用的是 Calico安装后每个节点上的 Pod 会获得一个集群内的独立 IP跨机器访问容器地址不再依赖宿主机端口映射。这一步容易踩的坑是CNI 的网段必须和初始化时的--pod-network-cidr保持一致否则节点之间路由完全不通Pod 只能在自己节点内部通信。当时我就在这个坑里困了好久反复看日志才发现 Calico 的CALICO_IPV4POOL_CIDR配的和 kubeadm 的 pod-network-cidr 不一致。3.3 加入 worker 节点其余 GPU 服务器作为 worker 节点加入集群命令就是初始化输出里的那段kubeadm join 192.168.1.10:6443 --token xxx --discovery-token-ca-cert-hash sha256:xxx每台节点执行完回到主节点上用命令确认状态kubectl get nodes看到所有节点都处于Ready状态后再给每个节点打上 GPU 类型标签比如kubectl label node gpu-node-01 gpu-typea100至此底层 Kubernetes 集群已经完成多机化所有节点的 CPU、内存、GPU 资源都被汇总到同一个资源池里。但这只是第一步CubeStudio 还需要做对应的适配。3.4 部署 CubeStudio 到集群CubeStudio 自身要以容器方式运行在这个 K8s 集群里相当于把平台服务托管到资源池上。为了便于升级和监控我选择了 Helm 方式部署把 Web 服务、调度服务、数据库等组件都装进集群。这里有一个重要的取舍CubeStudio 的服务组件包含数据库和文件存储我特意把数据库挂到共享存储上这样即使组件 Pod 在节点间漂移数据也不会丢。如果数据库跑在本地磁盘上一旦 Pod 被调度到别的节点数据就完全找不回来了。部署完成后需要确认平台能看到集群的全部资源。正常情况下节点列表会显示所有已加入的 GPU 机器而不是单机模式下的那一台。如果只显示一台多半是 kubeconfig 权限或者节点标签问题优先检查这两个方向。3.5 从单机切换到多机的最小心智如果之前已经用了单机版 CubeStudio里面还有历史任务和数据建议先做好备份再切换。我们当时的做法是把单机版的数据目录整体打包存到共享存储再在新环境中指定该目录重新启动后发现历史任务记录、数据集列表都还在。这一步虽然在文档里不怎么提但实际升级时非常关键直接决定你能不能无痛迁移。4. 把外部 K8s 集群收编进来纳管流程与证书权限多机做完之后多集群的需求很快就来了。我们的第二套 K8s 集群跑在另一朵云上平时归另一个团队管两边的资源各自独立但都想统一在 CubeStudio 看板里使用。于是 CubeStudio 的多集群纳管功能就派上了用场。4.1 在目标集群里创建一个专用的服务账号纳管的核心不是让 CubeStudio 直接拿到目标集群的admin权限而是为目标集群创建一个受限的服务账号ServiceAccount授予它足够调度资源的权限但又不至于影响集群本身的稳定性。我们按最小权限原则分配apiVersion: v1 kind: ServiceAccount metadata: name: cubestudio-sa namespace: kube-system --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: cubestudio-bind subjects: - kind: ServiceAccount name: cubestudio-sa namespace: kube-system roleRef: kind: ClusterRole name: cluster-admin这里要注意虽然例子用了cluster-admin但如果你对集群安全有更高要求建议细化为只授予namespaces、pods、persistentvolumeclaims等资源的读写权限不要轻易给全量管理员权限。创建好之后通过命令拿到该 ServiceAccount 的 Tokenkubectl -n kube-system describe secret $(kubectl -n kube-system get secret | grep cubestudio-sa | awk {print $1})还需要拿到目标集群的 API Server 地址一般是在~/.kube/config里可以看到server字段。有了 API Server 地址和 Token就能在 CubeStudio 平台的集群管理页面里填入这两项完成纳管。4.2 证书有效期和网络可达性是两个大坑纳管成功后最常踩的坑来自两个方向。第一个是证书过期。K8s 集群里的 ServiceAccount Token 有的有明确有效期过期后 CubeStudio 就无法正常往该集群下发任务。解决办法有两个方向一个是定期刷新 Token另一个是部署支持自动轮转的 Token 机制。第二个是网络可达性。CubeStudio 所在集群的节点必须能够访问目标集群的 API Server 地址。如果两个集群在不同的内网段或者一个在线下机房一个在云上就要提前打通专线或公网安全组策略。否则平台上会一直显示该集群连接失败。4.3 多集群视图下的资源归属收编多个集群后同一个资源池可能包含不同地域、不同计费方式的机器。我们通过给集群打标签来区分归属比如regionbeijing、regionshanghai、cloudprivate。这样调度任务时既可以指定某几个项目组用某个集群也可以设置跨集群的全局调度策略让任务自动选择最空闲的机器。多集群最怕的是看着资源很多但任务全堆在同一个集群里所以要定期查看各集群的实时占用率。CubeStudio 的集群看板可以直观看到每个集群的 GPU 使用率、节点状态、任务排队数这是多集群管理里最值得盯的一个面板。5. 按项目组分算力资源组、配额与所有权多机和多集群解决了算力在哪里的问题但如果人人都能不看配额地提交任务很快资源池又会变成新的混乱。所以下一步就是按项目组分算力。5.1 资源组的逻辑先划物理池再划逻辑配额我们的资源分组有两层逻辑物理资源池把节点按用途划分。比如gpu-a100这个池子只包含 A100 节点gpu-3090池子包含 3090 节点逻辑配额在 CubeStudio 里创建项目组给每个项目组绑定一个或多个物理资源池并设置 CPU、内存、GPU 卡数的上限。举个例子算法一队做大型模型预训练独占gpu-a100池子里的 8 张卡算法二队做模型微调和推理测试绑定gpu-3090池子里的 6 张卡。这样两队之间互不干扰谁也不会因为对方的任务而排队。5.2 配额设置与超卖控制给项目组设置配额时要考虑的不只是 GPU 卡数还有显存和 CPU。我当时做过一个不太科学的设置只限制了 GPU 卡数结果一个项目组把整个池子的系统内存和节点磁盘跑爆了其他项目组跟着遭殃。合理的配额应该是一个组合GPU 卡数、显存总量、CPU 核数、内存上限、存储配额最好都设置上。团队规模不大的情况下可以用简单的静态配额先不给超卖等资源利用率确实上来了再动态调整。具体操作上在 CubeStudio 的资源组管理页面新建项目组后需要配置选择绑定的集群单集群或多集群选择允许使用的资源池标签比如gpu-type3090设置 GPU、CPU、内存的限额关联项目组用户或用户组。这些配置保存后用户在提交任务时就只能看到自己项目组绑定的那些资源其他池子的资源对个人用户完全不可见。这个隔离效果非常重要它让用户从感受上觉得这个池子就是我的而不是所有人共享一个巨大的公共池从而减少抢资源引发的矛盾。5.3 命名空间隔离与权限粒度在 Kubernetes 层面每个项目组对应的就是一个独立命名空间Namespace。CubeStudio 创建项目组时会自动在该命名空间下创建对应的配额对象ResourceQuota和限制范围LimitRange这样即使绕开 CubeStudio 直接操作 K8s也能保证同样的配额限制。我建议把命名空间名称和项目组名称强绑定并形成命名规范比如proj-nlp、proj-cv。这样后续排查问题、看日志、做成本核算都方便得多。否则项目组起了一堆随意命名的名字运维排查时根本分不清哪个是哪个。5.4 按项目组分算力的一个隐藏收益除了控制资源按项目组分算力还有一个隐藏收益数据权限隔离。每个项目组在共享存储上有独立目录挂载到各自容器的不同路径下。项目组 A 的容器访问不到项目组 B 的数据目录这在多人协作和合规审计的场景下非常重要。我们当时因为一个项目涉及不能互相对外公开的数据特意加强了这块配置效果很直接。6. 部署过程中的坑和我的处理方式前面那些章节基本是地主家的流程下面这些坑才是我真正花时间的地方一个个说清楚。6.1 节点状态反复 NotReady 的元凶多机集群刚开始运行时经常出现某个节点NotReady过几分钟又恢复。查了半天发现根因是内存空间不足——kubelet 定期报告节点状态但系统压力大时kubelet 会被 cgroup 限制甚至被 OOM Kill。解决办法是给 kubelet 所在的系统进程留足够的系统资源并检查节点 docker/containerd 目录是否有堆积的日志和镜像缓存定期清理。另一个容易忽略的因素是节点时钟漂移。K8s 的证书校验非常依赖时间一台节点的时钟和主节点差了几分钟连接就会断断续续。所以我们后来在每台节点上强制配置了chrony并加了定时任务检查时间同步状态。6.2 GPU 显存与调度器的盲区平台调度任务时对 GPU 的感知依赖于nvidia-device-plugin上报的显存和型号。有一次我看到某个节点上报了 8 张 A100就放心地把 8 个任务调度过去结果第三个任务直接失败。查了下是因为显存已经被之前残留的僵尸进程占满但设备插件上报的数据还是空闲。这个问题的本质是K8s 和调度器默认只认 GPU 卡数不认显存余量。如果多个小任务共享一张卡就会发生资源冲突。我们的处理方案是在调度策略上默认每个任务独占整张 GPU 卡不做显存切分如果需要显存切分开启 MIG 或者 GPU 共享插件但这要求任务本身能适配定期检查节点上的残留 GPU 进程发现异常及时清理。6.3 共享存储的权限混乱多集群接入后不同集群的节点通过不同的 UID 运行容器结果访问同一个 NFS 目录时权限乱了套一个集群创建的文件另一个集群没权限读。这个问题我处理得比较暴力在所有集群上统一了nfs-utils的挂载 UID/GID并规定所有容器使用同一个runAsUser用户名。虽然不够精细但对于一个内部平台来说稳定性和谁都能读比严格的权限划分更重要。真正要做到精细化的多租户存储建议引入带配额和权限体系的分布式文件存储而不是继续用 NFS 死磕。6.4 日志和监控跨集群不统一多集群环境下最怕的就是排错时在每个集群里东翻西找用户报任务失败但根本不知道任务被调度到了哪台机器上。我们后来把所有集群的日志统一汇聚到一套日志平台里并且强制在日志里记录Node Name、Pod UID、Project Group三个维度。这样排错时可以根据项目组 时间范围直接过滤出完整链路不用再挨个节点翻日志。监控方面同理。各个集群独立部署 Prometheus 的话管理成本很高最好在 CubeStudio 侧统一接入监控数据源按集群维度做聚合面板。这样一张大屏可以看清整个平台的状态而不是十几个 Grafana 页面来回切。6.5 高可用与升级策略多机、多集群架构下平台的调度服务是核心中的核心。如果 CubeStudio 调度服务挂掉所有项目组都没法提交新任务。所以生产环境千万不能让平台服务单点运行。我的建议是CubeStudio 自身的核心组件至少 2 个副本分布在不同节点数据库和文件存储走共享存储或托管数据库平台升级前先在测试集群验证一次再滚动升级生产集群每次纳管新集群时做一个最小任务验证确认该集群可以正常拉起 Pod、访问共享存储、上报资源再正式开放给项目组。这套策略帮我们躲过了两次升级翻车的局面虽然每次升级都花费不少时间但总比线上任务大面积失败再回滚好得多。尾声实操中的三个体会这套架构跑了几个月之后我最大的感受是多机、多集群并不是越复杂越好而是要让用户和运维都能舒服地使用才算成功。第一个体会是先跑通再优化。不要一开始就追求所有集群全部接入、所有资源组都严格配额先让一个小团队的一两个项目在上面跑起来把流程验证通了再逐步扩容。第二个体会是标签的一致性就是生命线。无论是集群标签、GPU 标签还是项目组命名规范都要统一维护否则随着节点和集群越加越多混乱度会指数级上升。第三个体会是排错时先看时间再看权限最后看资源。通常这三个维度能覆盖八成的部署问题。如果你也在做类似的改造不妨从一台机器扩展到两台的场景开始把平台在 K8s 上的部署流程完全跑通再考虑多集群和资源组。直接一步到位虽然看起来快但遇到问题时跨度太大很难定位是哪一层出的错。希望这篇实操记录能帮你少走几步弯路。