4. Savepoint生命周期管理:创建、触发、清理、过期策略
说实话,很多团队把Savepoint当成“一次性快照”来用——建完就扔在那不管了。结果呢?等真正需要恢复的时候,发现磁盘塞满了旧快照,或者根本找不到哪个版本是能用的。
我经历过一个线上事故:某业务线的Savepoint目录里堆了300多个文件,总大小超过2TB。恢复时Flink JobManager光列举文件就花了5分钟,最后还因为磁盘IO瓶颈直接OOM了。嗯,从那以后,我对生命周期管理这件事就特别上心。
这一章,我们就来聊聊Savepoint从生到死的完整流程。说白了,就是回答三个问题:什么时候建?怎么触发?什么时候删?
4.1 Savepoint的创建策略
创建Savepoint不是拍脑袋决定的。我个人习惯按场景分三类:
| 创建类型 | 触发方式 | 典型场景 | 我踩过的坑 |
|---|---|---|---|
| 手动创建 | CLI命令 / REST API | 版本升级前、配置变更前 | 忘记等Savepoint完成就重启了作业 |
| 定时创建 | Cron表达式 + 脚本 | 每日凌晨备份、大促前快照 | 定时任务和Checkpoint撞车导致背压 |
| 事件驱动创建 | 自定义钩子 / 监控告警 | 检测到数据倾斜、延迟飙升时自动保存 | 事件风暴时连续创建了20个Savepoint |
4.2 触发机制与最佳实践
触发Savepoint的方式,Flink官方给了三种。但实际用起来,各有各的脾气。
4.2.1 CLI命令行触发
这是最常用的方式。我一般这么写:
# 触发Savepoint,并指定目标路径
flink savepoint <jobId> /data/flink/savepoints
# 触发后立即取消作业(常用于停机迁移)
flink savepoint <jobId> /data/flink/savepoints -y
# 从指定Savepoint恢复
flink run -s /data/flink/savepoints/savepoint-xxxxx \
-c com.example.MyJob myjob.jar
这里有个细节:-y参数的意思是“触发Savepoint成功后自动取消作业”。我刚开始用的时候不知道这个参数,每次都要手动再发一个cancel命令,中间那几秒如果作业挂了,Savepoint可能是不完整的。
4.2.2 REST API触发
如果你在搞自动化运维,REST API是更好的选择。我团队的自愈系统就是这么干的:
# 触发Savepoint
POST /jobs/<jobId>/savepoints
{
"target-directory": "/data/flink/savepoints"
}
# 查询Savepoint状态
GET /jobs/<jobId>/savepoints/<triggerId>
# 响应示例(成功时)
{
"status": {
"id": "COMPLETED"
},
"operation": {
"location": "/data/flink/savepoints/savepoint-xxxxx"
}
}
为什么要用REST API?因为可以写轮询逻辑。CLI命令返回后你不知道进度,但API可以一直查状态,直到COMPLETED或者FAILED。我建议在自动化脚本里加个超时重试,默认5分钟查不到完成就报警。
4.2.3 定时自动触发
定时触发我一般用两种方案:
- Cron + curl脚本:简单粗暴,适合中小集群。我写过的一个例子是每天凌晨3点触发,保留最近7天的。
- Flink Operator(K8s环境):原生支持SavepointSchedule CRD,可以声明式配置。我最近在推这个方案,因为不用额外维护脚本。
定时触发有个坑:别和Checkpoint周期重叠。我遇到过定时Savepoint和Checkpoint同时触发,导致TaskManager的IO被打满,作业延迟飙升。解决办法是把定时任务设在Checkpoint周期的“空窗期”,比如Checkpoint是1分钟一次,那Savepoint就设在整点过10秒。
4.3 清理策略:别让磁盘成为“垃圾场”
清理这件事,很多团队是“等到磁盘报警了才动手”。但那时候往往已经晚了。我总结了一套清理策略,分为三个层次:
4.3.1 基于时间的过期策略
最简单的策略:保留最近N天的Savepoint。我一般用脚本配合find命令实现:
#!/bin/bash
# 保留最近7天的Savepoint,其余删除
SAVEPOINT_DIR="/data/flink/savepoints"
RETENTION_DAYS=7
find $SAVEPOINT_DIR -type d -name "savepoint-*" \
-mtime +$RETENTION_DAYS \
-exec rm -rf {} \;
但要注意:mtime是文件的修改时间,不是Savepoint的创建时间。如果你手动修改过目录属性,这个策略会不准。我建议用Savepoint文件名里的时间戳来判断,更可靠。
4.3.2 基于版本的保留策略
有些场景下,时间策略不够用。比如大促前你手动创建了一个Savepoint,虽然已经过了7天,但你不想删——因为那是大促的“黄金快照”。
我的做法是:给重要的Savepoint打标签。在文件名里加个_golden后缀,清理脚本自动跳过:
# 清理时跳过带_golden标记的Savepoint
find $SAVEPOINT_DIR -type d -name "savepoint-*" \
! -name "*_golden*" \
-mtime +$RETENTION_DAYS \
-exec rm -rf {} \;
4.3.3 基于磁盘容量的动态清理
这是最“智能”的策略。当磁盘使用率超过阈值时,自动删除最旧的Savepoint,直到使用率降到安全水位。我实现过一个版本:
#!/bin/bash
THRESHOLD=80 # 磁盘使用率超过80%触发清理
TARGET=60 # 清理到60%为止
while [ $(df -h /data | awk 'NR==2 {print $5}' | sed 's/%//') -gt $THRESHOLD ]; do
# 找到最旧的Savepoint并删除
OLDEST=$(ls -1t /data/flink/savepoints/ | tail -1)
rm -rf "/data/flink/savepoints/$OLDEST"
echo "Deleted $OLDEST due to disk pressure"
done
这个脚本我放在crontab里,每5分钟跑一次。效果不错,但有个副作用:如果业务量暴增,Savepoint生成速度比删除速度快,磁盘还是会爆。所以最好配合告警一起用。
4.4 生命周期全景图
说了这么多,我们来画一张图,把Savepoint从创建到销毁的完整流程串起来。
4.5 生产级清理脚本实战
最后,分享一个我目前在用的生产级清理脚本。它整合了上面说的三种策略:
#!/bin/bash
# savepoint_cleaner.sh - 生产级Savepoint清理脚本
# 策略:保留最近7天 + 保留所有_golden标记 + 磁盘超80%时强制清理
SAVEPOINT_DIR="/data/flink/savepoints"
RETENTION_DAYS=7
DISK_THRESHOLD=80
LOG_FILE="/var/log/savepoint_cleaner.log"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> $LOG_FILE
}
# 第一步:基于时间的过期清理
log "开始基于时间的过期清理(保留${RETENTION_DAYS}天)"
find $SAVEPOINT_DIR -type d -name "savepoint-*" \
! -name "*_golden*" \
-mtime +$RETENTION_DAYS \
-exec rm -rf {} \; \
-exec log "已删除过期Savepoint: {}" \;
# 第二步:检查磁盘使用率
DISK_USAGE=$(df -h $SAVEPOINT_DIR | awk 'NR==2 {print $5}' | sed 's/%//')
log "当前磁盘使用率: ${DISK_USAGE}%"
if [ $DISK_USAGE -gt $DISK_THRESHOLD ]; then
log "磁盘使用率超过阈值${DISK_THRESHOLD}%,启动强制清理"
while [ $DISK_USAGE -gt 60 ]; do
OLDEST=$(ls -1t $SAVEPOINT_DIR | grep "savepoint-" | tail -1)
if [ -z "$OLDEST" ]; then
log "没有可清理的Savepoint,退出"
break
fi
rm -rf "$SAVEPOINT_DIR/$OLDEST"
log "强制清理: $OLDEST"
DISK_USAGE=$(df -h $SAVEPOINT_DIR | awk 'NR==2 {print $5}' | sed 's/%//')
done
fi
log "清理完成"
嗯,关于生命周期管理,核心就这些。记住一句话:Savepoint不是创建出来的,是管理出来的。没有清理策略的Savepoint,迟早会变成运维的噩梦。