1. Compose 解决了我的第一个部署问题
把 Vue、Spring Boot、MySQL、Redis 和 MinIO 放到同一台机器上时,Docker Compose 很合适。服务、网络、端口和数据卷可以写进一份文件,别人也能看见平台由哪些进程组成。
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 请求不再直接启动训练进程
在单机原型里,接口可能直接执行脚本。迁移后可以把流程改成:
POST /training-jobs
→ MySQL 写入 TrainingJob
→ 创建 Kubernetes Job
→ 返回 202 + trainingJobId
→ Job 等待资源并运行
→ 状态同步器记录结果这样分层之后,每层的职责就清晰了:Java API 处理认证、参数、数据版本、重复提交和权限;训练镜像处理计算;Kubernetes 处理 Pod(调度的最小执行单元,一个或多个容器的集合) 生命周期和节点调度。接口不再等待训练结束,而是把请求落到数据库、创建 Job、立即返回 202 与任务编号,训练结果由独立的状态同步器回收。
下面的清单只展示边界:
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-datasetsJob 编号、镜像和 PVC 名称都是示例。生产配置还需要资源请求、身份、网络策略和凭据管理。
4. ConfigMap 和 Secret 也有边界
ConfigMap 适合保存非敏感配置,例如日志级别和服务地址;Secret 专门承载敏感配置。但仅仅把值放进 Secret 对象,不会自动解决加密、轮换、访问审计和最小权限——它只是让敏感值不再散落在镜像或普通环境变量里。
配置还要分清两类:
平台配置:数据库地址、队列地址、对象存储入口
任务快照:数据版本、模型版本、训练参数、代码版本平台配置可以随部署更新;任务快照需要跟随一次训练记录固定下来。只在 ConfigMap 中保留当前值,无法回答历史任务当时用了什么参数。训练任务的复现、审计和排错,依赖的是“那一刻的配置”,而不是“现在的配置”。
5. Service 可达不代表业务就绪
Compose 的 depends_on 与 Kubernetes 的控制器都不能替代健康检查。API 进程启动后,可能还没有连接数据库;推理容器启动后,模型可能仍在加载。两者都确认了“进程在跑”,但都回答不了“能不能承受业务流量”。
可以分别使用三类探针:
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 保存的是集群资源状态,业务平台还要保存用户能够理解的关系:
项目 → 数据集版本 → 训练任务 → Kubernetes Job
→ 检查点 → 模型版本Java 服务不需要复制 kube-scheduler 的功能。它负责把业务请求转换成受约束的工作负载,并把底层状态整理成可追踪记录。集群负责“跑起来”,平台负责“讲得清”跑的是什么、为什么跑、结果在哪。
Compose 仍然适合本地开发和单机演示。Kubernetes 适合需要多节点调度、统一工作负载、资源隔离和控制器扩展的平台,但迁移会引入集群、网络、存储、安全和可观测性的运维成本,这些成本要在选择前权衡清楚。