从零开始的云原生之旅(八):CronJob 实战定时清理任务
从零开始的云原生之旅(八):CronJob 实战定时清理任务
定时任务不用 crontab 了,交给 K8s 管理!
📖 文章目录
- 前言
- 一、为什么需要 CronJob?
- 二、Job vs CronJob
- 三、清理任务需求分析
- 四、编写清理任务代码
- 五、配置 CronJob
- 六、构建和部署
- 七、测试和验证
- 八、失败处理和重试
- 九、调度策略实战
- 十、并发策略深度解析
- 十一、生产环境优化
- 十二、常见问题排查
- 结语
前言
在前面的文章中,我学会了部署各种工作负载:
- 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: importer
image: data-importer:v1.0
command: ["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: cleanup
image: cleanup-job:v1.0
restartPolicy: 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 main
import (
"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 := 0
noTTLCount := 0
for _, key := range keys {
// 获取 TTL
ttl := 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"] = deletedCount
stats["no_ttl"] = noTTLCount
stats["duration"] = time.Since(startTime)
return stats, nil
}
TTL 状态码:
- -2:键不存在
- -1:键存在,但没有过期时间
- > 0:剩余过期时间(秒)
五、配置 CronJob
5.1 基础配置
apiVersion: batch/v1
kind: CronJob
metadata:
name: cleanup-job
labels:
app: cleanup-job
version: v0.2
spec:
# 调度表达式
schedule: "0 * * * *" # 每小时的第0分钟
# Job 模板
jobTemplate:
spec:
template:
spec:
restartPolicy: OnFailure # 失败自动重试
containers:
- name: cleanup
image: cleanup-job:v0.2
env:
- name: REDIS_HOST
value: "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 个成功的 Job
failedJobsHistoryLimit: 1 # 保留 1 个失败的 Job
为什么要限制?
- Job 对象会占用 etcd 存储
- 太多历史记录影响性能
- 保留最近几次就够了
生产建议:
- 成功的:保留 3-5 个(看趋势)
- 失败的:保留 1-3 个(排查问题)
5.5 超时和重试
spec:
jobTemplate:
spec:
# 完成后 1 小时删除 Pod
ttlSecondsAfterFinished: 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 builder
WORKDIR /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:latest
WORKDIR /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_HOST
value: "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: CronJobFailed
expr: kube_job_status_failed{job="cleanup-job"} > 0
annotations:
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" # 保证 64Mi
cpu: "50m" # 保证 0.05 核
limits:
memory: "128Mi" # 最多 128Mi
cpu: "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: CleanupJobFailedMultipleTimes
expr: |
sum(increase(kube_job_status_failed{job="cleanup-job"}[3h])) > 3
annotations:
summary: "Cleanup job failed 3 times in 3 hours"
# 任务执行时间过长
- alert: CleanupJobTooSlow
expr: |
kube_job_status_completion_time - kube_job_status_start_time > 600
annotations:
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 []string
var cursor uint64
for {
// SCAN 每次返回一批键
var scanKeys []string
var err error
scanKeys, 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(水平自动扩缩容)
敬请期待!
如果这篇文章对你有帮助,欢迎点赞、收藏、分享!
有问题欢迎在评论区讨论! 👇
更多推荐
所有评论(0)