从零开始的云原生之旅(八):CronJob 实战定时清理任务
从零开始的云原生之旅(八):CronJob 实战定时清理任务
定时任务不用 crontab 了,交给 K8s 管理!
📖 文章目录
- 前言
- 一、为什么需要 CronJob? - 1.1 传统 crontab 的问题
- 1.2 K8s CronJob 的优势
 
- 二、Job vs CronJob - 2.1 Job:一次性任务
- 2.2 CronJob:定时任务
- 2.3 如何选择?
 
- 三、清理任务需求分析 - 3.1 业务场景
- 3.2 清理策略
- 3.3 技术方案
 
- 四、编写清理任务代码 - 4.1 代码结构
- 4.2 核心逻辑实现
- 4.3 清理策略实现
 
- 五、配置 CronJob - 5.1 基础配置
- 5.2 调度表达式
- 5.3 并发策略
- 5.4 历史记录管理
- 5.5 超时和重试
 
- 六、构建和部署 - 6.1 编写 Dockerfile
- 6.2 构建镜像
- 6.3 部署 CronJob
 
- 七、测试和验证 - 7.1 手动触发 Job
- 7.2 查看执行日志
- 7.3 验证清理效果
- 7.4 查看执行历史
 
- 八、失败处理和重试 - 8.1 模拟失败场景
- 8.2 观察重试机制
- 8.3 失败告警
 
- 九、调度策略实战 - 9.1 常用调度表达式
- 9.2 时区问题
- 9.3 错过调度时间
 
- 十、并发策略深度解析 - 10.1 Allow:允许并发
- 10.2 Forbid:禁止并发
- 10.3 Replace:替换旧任务
 
- 十一、生产环境优化 - 11.1 资源限制
- 11.2 日志收集
- 11.3 监控告警
- 11.4 清理策略优化
 
- 十二、常见问题排查
- 结语
前言
在前面的文章中,我学会了部署各种工作负载:
- Deployment:长期运行的无状态服务
- StatefulSet:长期运行的有状态服务
- DaemonSet:节点级守护进程
但这次遇到了新需求:
产品:“Redis 里的临时数据越来越多,要定时清理!”
我:“好,写个脚本,crontab 定时执行?”
产品:“不行!要在 K8s 里管理,统一监控告警!”
这篇文章,我会从零实现一个定时清理任务,完整掌握 K8s 的 CronJob!
一、为什么需要 CronJob?
1.1 传统 crontab 的问题
我以前的做法:
# 在服务器上配置 crontab
crontab -e# 每小时执行清理脚本
0 * * * * /usr/local/bin/cleanup-redis.sh
看起来没问题,但…:
❌ 问题 1:无法访问 K8s 内部服务
#!/bin/bash
# cleanup-redis.sh# 尝试连接 Redis
redis-cli -h redis-service -p 6379 KEYS "temp:*"
# Error: Could not resolve hostname redis-service
原因:
- 脚本运行在宿主机上
- redis-service是 K8s 内部 DNS
- 宿主机无法解析
解决方案:
- 配置 hosts 文件?太麻烦
- 用 NodePort 暴露 Redis?不安全
- 用 K8s CronJob! ✅
❌ 问题 2:服务器重启,cron 丢失
# 服务器重启
sudo reboot# crontab 消失了?
crontab -l
# no crontab for root
原因:某些系统配置不持久化
❌ 问题 3:没有执行日志
# 任务执行了吗?
# 没有日志,不知道!# 任务失败了?
# 不知道,没有告警!
❌ 问题 4:多台服务器,配置麻烦
服务器 A: crontab -e
服务器 B: crontab -e  ← 要在每台配置
服务器 C: crontab -e
1.2 K8s CronJob 的优势
✅ 解决方案:K8s CronJob
| 特性 | crontab | K8s CronJob | 
|---|---|---|
| 访问 K8s 服务 | ❌ 需要配置 | ✅ 原生支持 | 
| 高可用 | ❌ 单点故障 | ✅ K8s 自动调度 | 
| 日志 | ❌ 需要手动配置 | ✅ 自动收集 | 
| 监控 | ❌ 需要自己实现 | ✅ K8s 原生支持 | 
| 重试 | ❌ 失败就失败了 | ✅ 自动重试 | 
| 历史记录 | ❌ 没有 | ✅ 保留最近 N 次 | 
| 配置管理 | ❌ 分散在多台机器 | ✅ 统一 YAML 管理 | 
二、Job vs CronJob
2.1 Job:一次性任务
Job = 运行一次就结束
apiVersion: batch/v1
kind: Job
metadata:name: data-import
spec:template:spec:containers:- name: importerimage: data-importer:v1.0command: ["python", "import.py"]restartPolicy: OnFailure
特点:
- 立即执行
- 运行完退出
- 失败自动重试
适用场景:
- 数据库迁移
- 一次性数据导入
- 手动触发的任务
2.2 CronJob:定时任务
CronJob = 定时触发 Job
apiVersion: batch/v1
kind: CronJob
metadata:name: cleanup-job
spec:schedule: "0 * * * *"  # 每小时jobTemplate:spec:template:spec:containers:- name: cleanupimage: cleanup-job:v1.0restartPolicy: OnFailure
特点:
- 按时间表执行
- 自动创建 Job
- 支持并发控制
适用场景:
- 定时清理数据
- 定时备份数据库
- 定时生成报表
- 定时健康检查
2.3 如何选择?
需要定时执行吗?├─ 否 → 【Job】│       - 数据迁移│       - 一次性导入│└─ 是 → 【CronJob】- 定时清理- 定时备份- 定时报表
三、清理任务需求分析
3.1 业务场景
我的 API 服务会产生两类数据:
-  缓存数据( cache:*)- 用户请求的缓存结果
- 设置了 TTL(过期时间)
- 但有些键可能忘记设置 TTL
 
-  临时数据( temp:*)- 测试时创建的临时键
- 不需要长期保存
 
问题:
- Redis 内存占用越来越高
- 过期键没有及时清理
- 临时键堆积
3.2 清理策略
策略 1:清理 cache:* 键
- 检查 TTL
- 如果 TTL = -1(永不过期),设置为 1 小时
- 如果 TTL < 1 分钟,提前删除
策略 2:清理 temp:* 键
- 无条件删除所有 temp:*键
3.3 技术方案
┌─────────────────────────────────────────────────┐
│          CronJob: cleanup-job                   │
│                                                 │
│  每小时触发一次(0 * * * *)                     │
│                                                 │
│  ┌───────────────────────────────────────┐    │
│  │        Job: cleanup-job-28345670      │    │
│  │                                        │    │
│  │  ┌──────────────────────────────┐     │    │
│  │  │   Pod: cleanup-job-xxx       │     │    │
│  │  │                               │     │    │
│  │  │  1. 连接 Redis                │     │    │
│  │  │  2. 扫描 cache:* 键           │     │    │
│  │  │  3. 处理无 TTL 的键           │     │    │
│  │  │  4. 删除 temp:* 键            │     │    │
│  │  │  5. 输出统计信息              │     │    │
│  │  │  6. 退出(状态:Completed)   │     │    │
│  │  └──────────────────────────────┘     │    │
│  └───────────────────────────────────────┘    │
│                                                 │
└─────────────────────────────────────────────────┘
四、编写清理任务代码
4.1 代码结构
src/cleanup-job/
└── main.go
依赖:
import ("github.com/redis/go-redis/v9"  // Redis 客户端
)
4.2 核心逻辑实现
package mainimport ("context""fmt""log""os""time""github.com/redis/go-redis/v9"
)func main() {log.Println("🧹 Redis 清理任务开始执行...")log.Printf("⏰ 执行时间: %s", time.Now().Format("2006-01-02 15:04:05"))// 获取 Redis 连接信息redisHost := os.Getenv("REDIS_HOST")if redisHost == "" {redisHost = "redis-service:6379"}log.Printf("🔗 连接到 Redis: %s", redisHost)// 创建 Redis 客户端rdb := redis.NewClient(&redis.Options{Addr:         redisHost,Password:     "",DB:           0,DialTimeout:  5 * time.Second,ReadTimeout:  3 * time.Second,WriteTimeout: 3 * time.Second,})defer rdb.Close()ctx := context.Background()// 测试连接if err := rdb.Ping(ctx).Err(); err != nil {log.Fatalf("❌ 无法连接到 Redis: %v", err)}log.Println("✅ Redis 连接成功")// 执行清理任务cleaned, err := cleanupExpiredKeys(rdb, ctx)if err != nil {log.Fatalf("❌ 清理任务失败: %v", err)}// 输出统计log.Printf("✅ 清理完成")log.Printf("📊 统计信息:")log.Printf("   - 检查的键数: %d", cleaned["checked"])log.Printf("   - 删除的键数: %d", cleaned["deleted"])log.Printf("   - 无过期时间的键数: %d", cleaned["no_ttl"])log.Printf("   - 执行耗时: %v", cleaned["duration"])log.Println("🎉 任务执行成功,退出")
}
关键点:
- ✅ 从环境变量读取 Redis 地址
- ✅ 测试连接(快速失败)
- ✅ 输出详细日志(K8s 会收集)
- ✅ 任务完成后退出(状态码 0 = 成功)
4.3 清理策略实现
func cleanupExpiredKeys(rdb *redis.Client, ctx context.Context) (map[string]interface{}, error) {startTime := time.Now()stats := map[string]interface{}{"checked": 0,"deleted": 0,"no_ttl":  0,}// 策略 1: 处理 cache:* 键log.Println("🔍 扫描 cache:* 键...")keys, err := rdb.Keys(ctx, "cache:*").Result()if err != nil {return stats, fmt.Errorf("获取键列表失败: %w", err)}log.Printf("📝 找到 %d 个 cache:* 键", len(keys))stats["checked"] = len(keys)deletedCount := 0noTTLCount := 0for _, key := range keys {// 获取 TTLttl := rdb.TTL(ctx, key).Val()if ttl == -2 {// -2: 键不存在(已过期)deletedCount++log.Printf("   [已过期] %s", key)} else if ttl == -1 {// -1: 键存在但没有过期时间// 设置默认过期时间(1小时)rdb.Expire(ctx, key, 1*time.Hour)noTTLCount++log.Printf("   [设置TTL] %s (设为1小时)", key)} else if ttl < 60*time.Second {// TTL < 1分钟,提前删除rdb.Del(ctx, key)deletedCount++log.Printf("   [删除] %s (TTL: %v)", key, ttl)}}// 策略 2: 删除所有 temp:* 键log.Println("🔍 扫描 temp:* 键...")tempKeys, err := rdb.Keys(ctx, "temp:*").Result()if err != nil {log.Printf("⚠️  警告: 获取 temp:* 键失败: %v", err)} else {log.Printf("📝 找到 %d 个 temp:* 键", len(tempKeys))if len(tempKeys) > 0 {deleted, err := rdb.Del(ctx, tempKeys...).Result()if err != nil {log.Printf("⚠️  警告: 删除 temp:* 键失败: %v", err)} else {deletedCount += int(deleted)log.Printf("   删除了 %d 个临时键", deleted)}}}stats["deleted"] = deletedCountstats["no_ttl"] = noTTLCountstats["duration"] = time.Since(startTime)return stats, nil
}
TTL 状态码:
- -2:键不存在
- -1:键存在,但没有过期时间
- > 0:剩余过期时间(秒)
五、配置 CronJob
5.1 基础配置
apiVersion: batch/v1
kind: CronJob
metadata:name: cleanup-joblabels:app: cleanup-jobversion: v0.2
spec:# 调度表达式schedule: "0 * * * *"  # 每小时的第0分钟# Job 模板jobTemplate:spec:template:spec:restartPolicy: OnFailure  # 失败自动重试containers:- name: cleanupimage: cleanup-job:v0.2env:- name: REDIS_HOSTvalue: "redis-service:6379"
5.2 调度表达式
Cron 格式:
 ┌───────────── 分钟 (0 - 59)│ ┌───────────── 小时 (0 - 23)│ │ ┌───────────── 日 (1 - 31)│ │ │ ┌───────────── 月 (1 - 12)│ │ │ │ ┌───────────── 星期 (0 - 6) (0 = 周日)│ │ │ │ │* * * * *
常用示例:
# 每 5 分钟
schedule: "*/5 * * * *"# 每小时
schedule: "0 * * * *"# 每天凌晨 2 点
schedule: "0 2 * * *"# 每周日凌晨
schedule: "0 0 * * 0"# 每月 1 号凌晨
schedule: "0 0 1 * *"# 工作日 9-17 点每小时
schedule: "0 9-17 * * 1-5"# 每 15 分钟(工作时间)
schedule: "*/15 9-18 * * 1-5"
5.3 并发策略
spec:concurrencyPolicy: Forbid  # 禁止并发
三种策略:
| 策略 | 行为 | 适用场景 | 
|---|---|---|
| Allow | 允许并发执行 | 独立任务(日志归档) | 
| Forbid | 禁止并发,跳过新任务 | 数据库备份 | 
| Replace | 取消旧任务,启动新任务 | 实时报表 | 
我的选择:Forbid
为什么?
- 清理任务操作同一个 Redis
- 并发清理可能导致冲突
- 如果上次任务还没完成,说明数据量太大,应该跳过
5.4 历史记录管理
spec:successfulJobsHistoryLimit: 3  # 保留 3 个成功的 JobfailedJobsHistoryLimit: 1      # 保留 1 个失败的 Job
为什么要限制?
- Job 对象会占用 etcd 存储
- 太多历史记录影响性能
- 保留最近几次就够了
生产建议:
- 成功的:保留 3-5 个(看趋势)
- 失败的:保留 1-3 个(排查问题)
5.5 超时和重试
spec:jobTemplate:spec:# 完成后 1 小时删除 PodttlSecondsAfterFinished: 3600# 失败重试 3 次backoffLimit: 3# 任务超时 5 分钟activeDeadlineSeconds: 300
参数说明:
| 参数 | 说明 | 推荐值 | 
|---|---|---|
| ttlSecondsAfterFinished | 完成后多久删除 Pod | 3600(1小时) | 
| backoffLimit | 失败重试次数 | 3 | 
| activeDeadlineSeconds | 任务超时时间 | 300(5分钟) | 
六、构建和部署
6.1 编写 Dockerfile
# 构建阶段
FROM golang:1.23-alpine AS builderWORKDIR /build# 复制依赖文件
COPY go.mod go.sum ./
RUN go mod download# 复制源码
COPY src/cleanup-job/ ./# 构建
RUN CGO_ENABLED=0 GOOS=linux go build -o cleanup-job main.go# 运行阶段
FROM alpine:latestWORKDIR /app# 复制二进制文件
COPY --from=builder /build/cleanup-job .# 运行
CMD ["./cleanup-job"]
6.2 构建镜像
# 切换到 Minikube 的 Docker 环境
minikube docker-env | Invoke-Expression# 构建镜像
docker build -f Dockerfile.cleanup-job -t cleanup-job:v0.2 .# 验证镜像
docker images | Select-String "cleanup-job"
# REPOSITORY     TAG    IMAGE ID      CREATED        SIZE
# cleanup-job    v0.2   abc123def     5 seconds ago  15MB
6.3 部署 CronJob
# 部署
kubectl apply -f k8s/v0.2/cleanup-job/cronjob.yaml
# cronjob.batch/cleanup-job created# 查看 CronJob
kubectl get cronjobs
# NAME          SCHEDULE      SUSPEND   ACTIVE   LAST SCHEDULE   AGE
# cleanup-job   0 * * * *     False     0        <none>          10s
字段说明:
- SCHEDULE:调度表达式
- SUSPEND:是否暂停
- ACTIVE:当前活跃的 Job 数
- LAST SCHEDULE:上次调度时间
七、测试和验证
7.1 手动触发 Job
不想等 1 小时,手动触发:
# 方法:从 CronJob 创建一个 Job
kubectl create job cleanup-manual-001 --from=cronjob/cleanup-job# 查看 Job
kubectl get jobs
# NAME                  COMPLETIONS   DURATION   AGE
# cleanup-manual-001    0/1           5s         5s
7.2 查看执行日志
# 查看 Pod
kubectl get pods -l job-name=cleanup-manual-001
# NAME                        READY   STATUS    RESTARTS   AGE
# cleanup-manual-001-xxxxx    1/1     Running   0          10s# 查看日志
kubectl logs cleanup-manual-001-xxxxx# 输出:
# 🧹 Redis 清理任务开始执行...
# ⏰ 执行时间: 2025-10-30 15:30:00
# 🔗 连接到 Redis: redis-service:6379
# ✅ Redis 连接成功
# 🔍 扫描 cache:* 键...
# 📝 找到 3 个 cache:* 键
#    [设置TTL] cache:user:1001 (设为1小时)
#    [删除] cache:user:1002 (TTL: 30s)
# 🔍 扫描 temp:* 键...
# 📝 找到 2 个 temp:* 键
#    删除了 2 个临时键
# ✅ 清理完成
# 📊 统计信息:
#    - 检查的键数: 3
#    - 删除的键数: 3
#    - 无过期时间的键数: 1
#    - 执行耗时: 125ms
# 🎉 任务执行成功,退出
7.3 验证清理效果
# 进入 Redis
kubectl exec -it redis-0 -- redis-cli# 查看所有键
KEYS *
# 1) "cache:user:1001"  ← temp:* 键被删除了# 查看 TTL
TTL cache:user:1001
# (integer) 3598  ← 设置了 1 小时的 TTL# 退出
exit
7.4 查看执行历史
# 查看所有 Job
kubectl get jobs -l app=cleanup-job
# NAME                       COMPLETIONS   DURATION   AGE
# cleanup-job-28345670       1/1           15s        2h
# cleanup-job-28345680       1/1           12s        1h
# cleanup-job-28345690       1/1           18s        10m# 查看详细信息
kubectl describe cronjob cleanup-job# 输出:
# Name:         cleanup-job
# Schedule:     0 * * * *
# Successful Job History Limit:  3
# Failed Job History Limit:      1
# Last Schedule Time:  2025-10-30 15:00:00
# Active Jobs:         0
# Events:
#   Normal  SuccessfulCreate  10m  cronjob-controller  Created job cleanup-job-28345690
#   Normal  SuccessfulCreate  1h   cronjob-controller  Created job cleanup-job-28345680
八、失败处理和重试
8.1 模拟失败场景
修改 Redis 地址(故意写错):
kubectl edit cronjob cleanup-job# 修改:
env:
- name: REDIS_HOSTvalue: "redis-service-wrong:6379"  # ← 故意写错# 保存退出# 手动触发
kubectl create job cleanup-failed-001 --from=cronjob/cleanup-job
8.2 观察重试机制
# 查看 Pod 状态
kubectl get pods -l job-name=cleanup-failed-001 -w# 输出:
# NAME                        READY   STATUS              RESTARTS   AGE
# cleanup-failed-001-xxxxx    0/1     ContainerCreating   0          5s
# cleanup-failed-001-xxxxx    0/1     Error               0          10s  ← 第1次失败
# cleanup-failed-001-xxxxx    0/1     Error               1          20s  ← 第2次失败(重启)
# cleanup-failed-001-xxxxx    0/1     Error               2          40s  ← 第3次失败
# cleanup-failed-001-xxxxx    0/1     Error               3          80s  ← 第4次失败
# cleanup-failed-001-xxxxx    0/1     BackoffLimitExceeded 3         100s ← 达到重试上限
重试间隔:
- 第 1 次失败:立即重试
- 第 2 次失败:等 10 秒
- 第 3 次失败:等 20 秒
- 第 4 次失败:等 40 秒
- 指数退避(Exponential Backoff)
查看失败日志:
kubectl logs cleanup-failed-001-xxxxx# 输出:
# 🧹 Redis 清理任务开始执行...
# ⏰ 执行时间: 2025-10-30 15:40:00
# 🔗 连接到 Redis: redis-service-wrong:6379
# ❌ 无法连接到 Redis: dial tcp: lookup redis-service-wrong: no such host
8.3 失败告警
生产环境建议:
- 监控 Job 状态
# Prometheus 指标
kube_job_status_failed{job="cleanup-job"} > 0
- 配置告警
# Prometheus AlertManager
- alert: CronJobFailedexpr: kube_job_status_failed{job="cleanup-job"} > 0annotations:summary: "Cleanup job failed"description: "Job {{ $labels.job }} failed"
- 邮件/短信/Slack 通知
九、调度策略实战
9.1 常用调度表达式
测试调度表达式:
# 查看下次执行时间
kubectl get cronjob cleanup-job -o yaml | grep schedule
# schedule: 0 * * * *# 计算下次执行时间(手动)
# 当前时间:15:45
# 调度表达式:0 * * * *(每小时的第0分钟)
# 下次执行:16:00
实用调度示例:
# 每 5 分钟(高频清理)
schedule: "*/5 * * * *"# 每小时(常规清理)
schedule: "0 * * * *"# 每天凌晨 2 点(数据库备份)
schedule: "0 2 * * *"# 每周日凌晨 3 点(周报)
schedule: "0 3 * * 0"# 每月 1 号凌晨 4 点(月度统计)
schedule: "0 4 1 * *"# 工作日 9-18 点每小时(工作时间清理)
schedule: "0 9-18 * * 1-5"# 每 30 分钟(工作日)
schedule: "*/30 * * * 1-5"
9.2 时区问题
默认使用 UTC 时区:
# 查看 CronJob 的时区
kubectl get cronjob cleanup-job -o yaml | grep timeZone
# (空) ← 默认 UTC
如果想用本地时区(K8s 1.25+):
spec:schedule: "0 2 * * *"  # 本地时间凌晨 2 点timeZone: "Asia/Shanghai"  # 设置时区
注意:
- K8s 1.25 以下不支持 timeZone
- 需要自己计算时差(北京时间 = UTC + 8)
示例(没有 timeZone 支持):
# 北京时间凌晨 2 点 = UTC 时间 18:00(前一天)
schedule: "0 18 * * *"  # UTC 18:00
9.3 错过调度时间
场景:K8s 集群重启,错过了调度时间
spec:startingDeadlineSeconds: 100  # 截止时间 100 秒
行为:
- 调度时间:15:00:00
- 实际启动:15:01:50(晚了 110 秒)
- 超过 100 秒 → 跳过这次执行
设置建议:
- 高频任务(每 5 分钟):startingDeadlineSeconds: 60
- 低频任务(每天一次):startingDeadlineSeconds: 3600
- 不设置:永远不跳过(会补执行)
十、并发策略深度解析
10.1 Allow:允许并发
spec:concurrencyPolicy: Allow
行为:
时间线:
15:00 → Job-001 开始执行(耗时 70 分钟)
16:00 → Job-002 开始执行(Job-001 还在运行)← 并发!
17:00 → Job-003 开始执行(Job-001 还在运行)← 并发!
查看:
kubectl get jobs
# NAME                       COMPLETIONS   DURATION   AGE
# cleanup-job-28345670       0/1           70m        70m  ← 还在运行
# cleanup-job-28345680       0/1           10m        10m  ← 并发运行
# cleanup-job-28345690       0/1           1s         1s   ← 又启动了一个
适用场景:
- 独立任务(日志归档)
- 任务之间不冲突
- 可以并发执行
10.2 Forbid:禁止并发
spec:concurrencyPolicy: Forbid  # 我们用的这个
行为:
时间线:
15:00 → Job-001 开始执行(耗时 70 分钟)
16:00 → Job-002 被跳过(Job-001 还在运行)← 跳过!
17:00 → Job-003 被跳过(Job-001 还在运行)← 跳过!
17:10 → Job-001 完成
18:00 → Job-004 正常执行
查看:
kubectl get events --sort-by='.lastTimestamp' | grep cleanup# 输出:
# 15:00:00  Normal  SuccessfulCreate  Created job cleanup-job-001
# 16:00:00  Warning FailedCreate      Cannot create job (previous job still running)
# 17:00:00  Warning FailedCreate      Cannot create job (previous job still running)
# 18:00:00  Normal  SuccessfulCreate  Created job cleanup-job-004
适用场景:
- 数据库备份(避免冲突)
- 数据清理(操作同一数据源)
- 任务耗时不稳定
10.3 Replace:替换旧任务
spec:concurrencyPolicy: Replace
行为:
时间线:
15:00 → Job-001 开始执行(耗时 70 分钟)
16:00 → Job-001 被取消,Job-002 开始执行 ← 替换!
17:00 → Job-002 被取消,Job-003 开始执行 ← 又替换!
查看:
kubectl get jobs
# NAME                       COMPLETIONS   DURATION   AGE
# cleanup-job-001            0/1           Failed     70m  ← 被取消
# cleanup-job-002            0/1           Failed     10m  ← 被取消
# cleanup-job-003            0/1           Running    1s   ← 当前运行
适用场景:
- 实时报表(只要最新的)
- 任务可以中断
- 旧任务结果无意义
十一、生产环境优化
11.1 资源限制
resources:requests:memory: "64Mi"   # 保证 64Micpu: "50m"       # 保证 0.05 核limits:memory: "128Mi"  # 最多 128Micpu: "100m"      # 最多 0.1 核
为什么要限制?
- 防止任务占用过多资源
- 保证其他服务的资源
- 清理任务不应该消耗太多资源
11.2 日志收集
K8s 会自动收集 Pod 日志:
# 查看最近的日志
kubectl logs -l app=cleanup-job --tail=100# 查看指定 Job 的日志
kubectl logs -l job-name=cleanup-job-28345670
生产建议:
- 使用 Fluentd/Filebeat 收集日志
- 发送到 ElasticSearch/Loki
- 配置 Kibana/Grafana 查看
11.3 监控告警
关键指标:
# Job 成功次数
kube_job_status_succeeded{job="cleanup-job"}# Job 失败次数
kube_job_status_failed{job="cleanup-job"}# Job 执行时长
kube_job_status_completion_time - kube_job_status_start_time
告警规则:
# 连续失败 3 次
- alert: CleanupJobFailedMultipleTimesexpr: |sum(increase(kube_job_status_failed{job="cleanup-job"}[3h])) > 3annotations:summary: "Cleanup job failed 3 times in 3 hours"# 任务执行时间过长
- alert: CleanupJobTooSlowexpr: |kube_job_status_completion_time - kube_job_status_start_time > 600annotations:summary: "Cleanup job took more than 10 minutes"
11.4 清理策略优化
当前问题:KEYS 命令会阻塞 Redis
// ❌ 生产环境不推荐
keys, err := rdb.Keys(ctx, "cache:*").Result()
优化:使用 SCAN 命令
// ✅ 生产环境推荐
func scanKeys(rdb *redis.Client, ctx context.Context, pattern string) ([]string, error) {var keys []stringvar cursor uint64for {// SCAN 每次返回一批键var scanKeys []stringvar err errorscanKeys, cursor, err = rdb.Scan(ctx, cursor, pattern, 100).Result()if err != nil {return nil, err}keys = append(keys, scanKeys...)// cursor = 0 表示扫描完成if cursor == 0 {break}}return keys, nil
}
好处:
- ✅ 不阻塞 Redis
- ✅ 渐进式扫描
- ✅ 对生产环境友好
十二、常见问题排查
问题 1:CronJob 没有执行
症状:
kubectl get cronjobs
# NAME          SCHEDULE      SUSPEND   ACTIVE   LAST SCHEDULE   AGE
# cleanup-job   0 * * * *     False     0        <none>          1h
LAST SCHEDULE 一直是 <none>
排查:
# 1. 检查 CronJob 状态
kubectl describe cronjob cleanup-job# 2. 查看事件
kubectl get events --sort-by='.lastTimestamp' | grep cleanup# 3. 检查调度表达式
kubectl get cronjob cleanup-job -o yaml | grep schedule
常见原因:
- ❌ 调度表达式错误
- ❌ CronJob 被暂停(suspend: true)
- ❌ K8s 控制器异常
问题 2:Job 一直失败
症状:
kubectl get jobs
# NAME                       COMPLETIONS   DURATION   AGE
# cleanup-job-28345670       0/1           5m         5m
COMPLETIONS 一直是 0/1
排查:
# 查看 Pod 状态
kubectl get pods -l job-name=cleanup-job-28345670
# NAME                        READY   STATUS    RESTARTS   AGE
# cleanup-job-28345670-xxx    0/1     Error     3          5m# 查看日志
kubectl logs cleanup-job-28345670-xxx# 查看详细信息
kubectl describe pod cleanup-job-28345670-xxx
常见原因:
- ❌ Redis 连接失败(地址错误)
- ❌ 镜像拉取失败
- ❌ 资源限制过小(OOM)
- ❌ 代码逻辑错误
问题 3:任务超时
症状:
kubectl get pods -l job-name=cleanup-job-28345670
# NAME                        READY   STATUS    RESTARTS   AGE
# cleanup-job-28345670-xxx    0/1     DeadlineExceeded   0  5m
原因:超过 activeDeadlineSeconds
解决:
spec:jobTemplate:spec:activeDeadlineSeconds: 600  # 增加到 10 分钟
结语
这篇文章,我学会了:
✅ CronJob 的核心概念
- 定时触发 Job
- 调度表达式(Cron 格式)
- 并发策略(Allow/Forbid/Replace)
✅ 完整的实战流程
- 编写清理任务代码
- 配置 CronJob
- 构建镜像和部署
- 测试和验证
✅ 失败处理和重试
- 自动重试机制(指数退避)
- 失败日志查看
- 监控和告警
✅ 生产环境优化
- 资源限制
- 日志收集
- 监控告警
- 清理策略优化(SCAN)
最大的收获:
不要再用 crontab 了!
K8s CronJob 提供了更强大的功能:
自动重试、日志收集、监控告警、统一管理!
v0.2 全部完成!
在 v0.2 中,我完整掌握了 K8s 的 4 种工作负载:
- ✅ Deployment:无状态应用
- ✅ StatefulSet:有状态应用(Redis)
- ✅ DaemonSet:节点级服务(日志采集)
- ✅ CronJob:定时任务(数据清理)
下一步(v0.3):
v0.3 将学习高级网络和监控:
- Ingress(统一入口)
- NetworkPolicy(网络隔离)
- Prometheus + Grafana(完整监控)
- HPA(水平自动扩缩容)
敬请期待!
如果这篇文章对你有帮助,欢迎点赞、收藏、分享!
有问题欢迎在评论区讨论! 👇
