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可能是不完整的。

注意:CLI触发是异步的。命令返回后,Savepoint可能还在写。一定要等日志里出现“Savepoint completed”再操作。我曾经因为心急,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 {} \;
核心原则:清理策略要“先保后删”。先确认哪些Savepoint不能删(比如版本基线、大促快照),再对剩下的执行过期清理。我见过有人写反了顺序,结果把最重要的快照给删了。

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从创建到销毁的完整流程串起来。

Savepoint生命周期管理全景图 创建 手动 / 定时 / 事件驱动 存储 分布式文件系统 / OSS 使用 恢复 / 迁移 / 克隆 基于时间过期 基于版本保留 基于磁盘容量 清理执行:删除 / 归档 / 迁移冷存储 审计日志:记录每次创建 / 删除操作 说明:创建后的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 "清理完成"
使用建议:把这个脚本放到crontab里,每天凌晨执行一次。如果磁盘压力大,可以缩短到每小时一次。记得加日志,方便排查“谁删了我的Savepoint”。

嗯,关于生命周期管理,核心就这些。记住一句话:Savepoint不是创建出来的,是管理出来的。没有清理策略的Savepoint,迟早会变成运维的噩梦。