部署与运维

从 Docker Compose 到 Kubernetes:AI 数据平台为什么需要统一运行底座

从单机演示继续向多工作负载平台推进,理解 Deployment、Job、Service、ConfigMap、Secret 与 PVC 的分工。

1. Compose 解决了我的第一个部署问题

把 Vue、Spring Boot、MySQL、Redis 和 MinIO 放到同一台机器上时,Docker Compose 很合适。服务、网络、端口和数据卷可以写进一份文件,别人也能看见平台由哪些进程组成。

text7 行
Browser
  → Nginx
      ├─ Vue static files
      └─ /api → Spring Boot
                    ├─ MySQL
                    ├─ Redis
                    └─ MinIO

这套结构足以完成项目演示。继续加入长时间训练、GPU 节点、多个 Worker 和模型推理服务以后,单机编排开始遇到新的问题:任务应该在哪台机器运行,GPU 怎样分配,失败后谁负责重建,配置怎样按环境管理,数据怎样在节点之间访问?

把这些部署动作自动化、按规则去执行,就是编排(orchestration,自动管理容器的部署、网络、伸缩与故障恢复)。Compose 是一台机器的编排,Kubernetes 则是把这一套能力扩展到多节点集群。

2. 迁移的核心是工作负载分类

Compose 中所有服务都写在 services 下。Kubernetes 要先判断每个进程属于哪类工作负载(workload,集群中长期或一次性运行的一类应用集合):

平台进程 Kubernetes 资源 运行特点
Spring Boot API Deployment 长期运行,可滚动更新,可有多个副本
前端入口 Deployment + Service / Gateway 长期运行,对外提供流量入口
训练任务 Job 运行到完成,保留成功或失败结果
周期同步 CronJob 按时间创建一次性任务
MySQL、Redis 托管服务或有状态方案 需要持久化、备份和明确运维责任
数据集与模型 PVC、CSI 或对象存储 生命周期独立于单个 Pod

Kubernetes 的价值来自这些控制器(controller,持续比较期望状态与实际状态的循环)。Deployment 希望 API 保持指定副本数,实例挂了就自动重建,更新时做滚动替换;Job 希望任务最终完成,失败时按策略重试,成功即止。两种工作负载的结束语义完全不同——前者“永远在跑”,后者“跑完为止”——所以不应该共用同一套重启策略。把任务错当长期服务去部署,会在它意外退出时被无限重启;把 API 错当 Job 去部署,则会在完成后被当成失败回收。

这里的关键概念是期望状态与当前状态(desired vs. current state):控制器读期望状态、观察当前状态、执行操作去收敛差异。我们只需告诉集群“想要的最终结果”,具体步骤由控制器完成。

3. 一次 HTTP 请求不再直接启动训练进程

在单机原型里,接口可能直接执行脚本。迁移后可以把流程改成:

text6 行
POST /training-jobs
  → MySQL 写入 TrainingJob
  → 创建 Kubernetes Job
  → 返回 202 + trainingJobId
  → Job 等待资源并运行
  → 状态同步器记录结果

这样分层之后,每层的职责就清晰了:Java API 处理认证、参数、数据版本、重复提交和权限;训练镜像处理计算;Kubernetes 处理 Pod(调度的最小执行单元,一个或多个容器的集合) 生命周期和节点调度。接口不再等待训练结束,而是把请求落到数据库、创建 Job、立即返回 202 与任务编号,训练结果由独立的状态同步器回收。

下面的清单只展示边界:

yaml23 行
apiVersion: batch/v1
kind: Job
metadata:
  name: training-job-1042
spec:
  backoffLimit: 1
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: trainer
          image: registry.example/trainer:2026-08
          args: ["--job-id", "1042"]
          envFrom:
            - configMapRef:
                name: training-runtime
          volumeMounts:
            - name: dataset
              mountPath: /datasets
      volumes:
        - name: dataset
          persistentVolumeClaim:
            claimName: training-datasets

Job 编号、镜像和 PVC 名称都是示例。生产配置还需要资源请求、身份、网络策略和凭据管理。

4. ConfigMap 和 Secret 也有边界

ConfigMap 适合保存非敏感配置,例如日志级别和服务地址;Secret 专门承载敏感配置。但仅仅把值放进 Secret 对象,不会自动解决加密、轮换、访问审计和最小权限——它只是让敏感值不再散落在镜像或普通环境变量里。

配置还要分清两类:

text2 行
平台配置:数据库地址、队列地址、对象存储入口
任务快照:数据版本、模型版本、训练参数、代码版本

平台配置可以随部署更新;任务快照需要跟随一次训练记录固定下来。只在 ConfigMap 中保留当前值,无法回答历史任务当时用了什么参数。训练任务的复现、审计和排错,依赖的是“那一刻的配置”,而不是“现在的配置”。

5. Service 可达不代表业务就绪

Compose 的 depends_on 与 Kubernetes 的控制器都不能替代健康检查。API 进程启动后,可能还没有连接数据库;推理容器启动后,模型可能仍在加载。两者都确认了“进程在跑”,但都回答不了“能不能承受业务流量”。

可以分别使用三类探针:

text3 行
startupProbe    进程是否完成较长的启动阶段
readinessProbe  当前实例能否接收流量
livenessProbe   进程是否陷入需要重启的状态

探针路径必须与真实服务状态一致。把 /health 固定返回 200,只能证明 HTTP 线程仍在响应,数据库连接池是满的也照样返回 200。

训练 Job 的健康判断又不同。长时间没有日志不一定代表卡死,频繁重启也可能让有效检查点丢失。平台需要结合心跳、训练步数、资源状态和超时规则判断——这类任务的“健康”不在容器探针里,而在业务语义里。

6. 数据生命周期不能绑在 Pod 上

容器文件系统随容器重建而变化。数据库、数据集、检查点和模型产物必须独立持久化,否则 Pod 一重建数据就没了。

Kubernetes 用 PV/PVC(PersistentVolume / PersistentVolumeClaim,存储资源与使用申请的分离) 把存储提供方式与使用请求分开;CSI(Container Storage Interface) 允许接入不同厂商的存储系统;对象存储则通常由应用或 SDK 直接访问。选择哪一条路,取决于训练程序需要 POSIX 文件接口,还是能够直接读取对象 URI——这直接改变你的部署方式和代码写法。

需要提前回答:

  • Pod 换节点后能否读取同一数据?
  • 多个训练任务是否会同时读写目录?
  • 删除 Job 是否会误删数据?
  • 数据和模型如何备份、恢复与校验?

7. 迁移以后仍然需要 Java 控制面

Kubernetes 保存的是集群资源状态,业务平台还要保存用户能够理解的关系:

text2 行
项目 → 数据集版本 → 训练任务 → Kubernetes Job
                           → 检查点 → 模型版本

Java 服务不需要复制 kube-scheduler 的功能。它负责把业务请求转换成受约束的工作负载,并把底层状态整理成可追踪记录。集群负责“跑起来”,平台负责“讲得清”跑的是什么、为什么跑、结果在哪。

Compose 仍然适合本地开发和单机演示。Kubernetes 适合需要多节点调度、统一工作负载、资源隔离和控制器扩展的平台,但迁移会引入集群、网络、存储、安全和可观测性的运维成本,这些成本要在选择前权衡清楚。

相关阅读

参考资料

← 返回文章