应使用 golang-migrate/v4 而非手动 db.Exec():它提供版本管理、锁表、状态持久化和幂等控制;迁移文件须按 YYYYMMDDHHIISS_up.sql 命名;K8s Job 配置 restartPolicy: Never、backoffLimit: 0;DSN 用 service 名而非 localhost,显式设 connect_timeout;必须设置 activeDeadlineSeconds 防卡死。

用 database/sql + github.com/golang-migrate/migrate/v4 做迁移,别自己写 SQL 执行器
直接调 db.Exec() 拼接 SQL 跑迁移,短期能跑,上线后大概率出事:没版本跟踪、回滚不可控、并发执行会冲突。golang-migrate 是事实标准,它管版本号、锁表、状态持久化(支持 postgres、mysql、sqlite 等后端),Job 重启也不会重复跑。
实操建议:
- 迁移文件必须按
YYYYMMDDHHIISS_<name>.up.sql</name>格式命名,比如20240520103000_add_users_table.up.sql,否则migrate无法排序 - 把迁移文件打包进镜像,路径设为
file://migrations;别挂载 ConfigMap——K8s 更新 ConfigMap 不触发 Pod 重启,Job 可能永远用旧迁移 - 初始化
migrate.New()时传入database/sql的*sql.DB,不是 DSN 字符串;否则连接池复用失效,Job 启动就报too many connections
K8s Job 的 restartPolicy 必须设为 Never,且加 backoffLimit: 0
默认 restartPolicy: OnFailure 配合 backoffLimit: 6,看起来安全,实际是陷阱:迁移失败后重试,但 migrate 默认不幂等(同一版本 up 文件只允许执行一次),第二次跑直接报 Error: no change to database 或更糟的锁等待超时。
正确做法:
立即学习“go语言免费学习笔记(深入)”;
-
restartPolicy: Never—— 迁移失败就停,靠 CI/CD 或人工干预,不自动重试 -
backoffLimit: 0—— 彻底禁用重试,避免 K8s 自作聪明拉起新容器 - 在容器启动脚本里加
set -e,任何命令失败立刻退出,防止迁移中途出错却继续往下走
连接数据库时必须用 host 而非 localhost,且显式设置 connect_timeout
Job 容器和数据库不在同一网络命名空间,localhost 指向容器自身,不是数据库服务。常见错误现象:dial tcp 127.0.0.1:5432: connect: connection refused,但日志里还显示 “connecting to localhost:5432” —— 其实根本没发出去。
实操要点:
- DSN 中
host填 Service 名,如host=postgres.default.svc.cluster.local(K8s DNS 可解析) - PostgreSQL 加
connect_timeout=10,MySQL 加timeout=10s,避免 Job 卡在 DNS 解析或网络抖动上,超时后快速失败并退出 - 不要依赖
initContainer检查数据库连通性——它成功不代表主容器运行时仍可达,迁移阶段再连一次才真实
迁移 Job 必须带 activeDeadlineSeconds,且值要大于最长迁移耗时的 2 倍
没有超时控制的 Job,一旦某次迁移卡死(比如大表加索引、锁表未释放),Pod 就永远 Pending 或 Running,后续所有依赖它的部署都被堵住。K8s 不会主动杀掉“看起来还在干活”的 Job。
设置原则:
- 先在测试环境跑一次全量迁移,记录最大耗时(比如 4 分钟),然后设
activeDeadlineSeconds: 600(10 分钟) - 这个值不能拍脑袋定:太小导致正常迁移被误杀;太大等于没设,失去保护意义
- 注意:该字段作用于整个 Job 生命周期,包括容器启动、迁移执行、进程退出,不是单指 SQL 执行时间
迁移逻辑越复杂,越要盯紧锁竞争和事务隔离级别。比如 PostgreSQL 上用 ALTER TABLE ... ADD COLUMN 在大表上可能锁全表几秒,而 migrate 默认不设 statement_timeout,得手动在 DSN 里加上。










