cronjob 不适合业务逻辑型定时任务,因其仅为触发器而非执行器,仅按时创建 job pod,不提供分布式锁、幂等、重试、超时控制等能力,多实例下易导致重复执行与状态冲突。

直接在 Kubernetes 里用 cron/v3 或 time.Ticker 启动定时任务,多实例下必然重复执行——这不是配置问题,是设计缺陷。
为什么 CronJob 不适合业务逻辑型定时任务
Kubernetes CronJob 本质是“触发器”,不是“执行器”。它只负责按时创建一个 Job Pod,而该 Pod 里的 Go 程序如果自己再启动 cron.AddFunc,就又回到单机定时器的老路:每个副本都跑一份,锁没协调、状态没共享。
- 典型症状:日志里同一任务每小时出现 3 次(replicas=3),DB 写冲突、第三方接口限流告警频发
-
CronJob的jobTemplate.spec.template.spec.restartPolicy只能设为OnFailure或Never;设成Always会校验失败 - 它不感知任务是否真正完成、是否被中断、是否需要重试——这些得靠你自己的 Go 代码补全
- 如果你的任务要查 DB、调外部 API、发邮件,
CronJob本身不提供重试、幂等、超时控制等能力
Go 服务内嵌定时器必须加分布式锁
想让多个 Pod 中只有一个执行任务?不能靠“约定”或“运气”,必须用 etcd lease 实现原子抢占。Kubernetes 原生支持,Go 客户端 clientv3 直接可用。
- 抢锁动作必须用事务:
txn.If(txn.Version(), "=", 0).Then(OpPut()),否则并发 Put 会互相覆盖 - lease TTL 要设为任务预期耗时的 2–3 倍(例如任务通常 20s 完成,TTL 设 60s),避免网络抖动导致误释放
- 每次执行前必须
Grant新 lease,不能复用旧 lease ID——旧 lease 过期后 key 自动删除,但其他节点可能还没抢到,造成空窗 - 锁值别写
time.Now().Unix(),各节点时钟偏移会导致 key 计算不一致;改用 lease ID 或 etcd revision
探针和监听地址不配齐,定时任务根本启不来
很多 Go 定时任务服务卡在 CrashLoopBackOff,不是逻辑错了,是连容器都没真正“活”起来。
- HTTP server 必须监听
:8080(即0.0.0.0:8080),写成"127.0.0.1:8080"会导致 readinessProbe 失败——Service 流量压根进不来 -
livenessProbe和readinessProbe必须分离:/healthz只返回 200,/readyz要检查 DB 连通性;共用一个 endpoint 会让 Pod 就绪前就被杀掉 - 务必设
initialDelaySeconds: 5和failureThreshold: 3,避免冷启动或 GC STW 导致探针误判 - Dockerfile 必须禁用 CGO:
CGO_ENABLED=0 GOOS=linux go build,否则 Alpine/musl 环境运行时报no such file or directory
Deployment labels 和 selector 不一致是最隐蔽的坑
哪怕只差一个空格,Kubernetes 就拒绝更新,并报 field is immutable。这不是警告,是硬拦截。
-
spec.selector.matchLabels和template.metadata.labels必须逐字相同,包括大小写、引号、空格 - 例如:selector 是
{app: "report-svc"},那 template labels 也必须是{app: "report-svc"},不能是{APP: "report-svc"}或{app:"report-svc"}(缺空格) - 别在 template 里多加 label(如
version: "v2"),除非 selector 也包含它——否则新 Pod 永远不会被纳入 Service 流量 - 一旦 Deployment 创建成功,
spec.selector就不可变更;改错只能删掉重建
真正的难点不在写定时逻辑,而在让所有 Pod 对“谁来执行”达成瞬时共识——这要求你把 lease 抢占、探针语义、label 校验、镜像构建四个环节全部对齐,漏一环,任务就不可靠。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











