一、XXL-JOB日志表的“膨胀危机”现场

平时用XXL-JOB的同学,可能都碰过这样的情况:线上某个任务报错了,想查执行日志,结果点进去卡半天,甚至要等几十秒,最后还可能超时。这不是你的网络问题,是XXL-JOB的执行日志表“疯长”惹的祸。

1.1 为什么日志会“疯长”?

XXL-JOB的每个任务执行时,都会往执行日志表里写一条记录——任务ID、触发时间、执行时长、返回结果这些信息都得存下来,方便后续排查问题。按正常情况,一天的日志量可能只有几万条,但遇到大促或者业务高峰期,比如电商平台的订单超时取消、商品库存同步这些任务,每分钟可能都要跑几百次,加上重试的失败日志,一天涨几百万条都是常事。

1.2 并发写时的查询性能坑点

当日志表数据量到几千万条时,查询就会出问题。就像你找课本里的某一页,本来有目录(索引)能一秒找到,但课本厚到根本翻不动,目录也变得乱糟糟的,找起来就慢了。并发写的时候,数据库的硬盘读写压力会瞬间拉满,主库的IO直接爆满,不仅新日志写不进去,连旧日志的查询也会跟着卡,严重时整个调度中心都快动不了。

二、给日志做“冷热分家”的思路

既然日志分常用和不常用的,那干脆分开放就行。把最近经常查的日志叫“热数据”,留在线上的主库,给它配最快的硬件;把很久没人查的日志叫“冷数据”,放到单独的历史库,不用占用主库的资源,这样就能解决查询卡的问题。

2.1 先搞懂冷热数据怎么分?

不用太复杂,按时间分就好,比如保留最近7天的日志,超过7天的就算冷数据。为什么选7天?因为大部分公司的业务问题,7天内的日志都是常用的,超过7天的,除非查旧订单,不然没人会动,刚好平衡查询需求和资源占用。

2.2 用分页归档替代硬删的好处

很多人会想,直接把超过7天的日志删了不就行?但直接删几十万条数据,会锁表很久,线上任务写日志都卡,风险太大。分页归档就不一样了,每次只删几百条,删完再去下一批,锁表时间短到几乎不影响线上,而且中途停了,下次再跑的时候从上次的位置继续,不会丢数据。

三、落地时的详细示例

这里用大家最熟的Shell+MySQL来做示例,也是XXL-JOB最常用的技术栈,不用换复杂的东西,能直接抄作业。

3.1 提前准备数据库结构

先在MySQL里建两个库:一个叫xxl_job(存热数据,线上用),另一个叫xxl_job_log_his(存冷数据,历史用),两个库的表结构完全和XXL-JOB默认的xxl_job_log表一样,直接复制建表语句就行,不用改。

3.2 写归档定时任务的脚本

把下面的Shell脚本保存成archive_job_log.sh,放到服务器上,设置成凌晨2点执行(这个时间业务最少,不会影响线上)。脚本里的注释都标了,替换成自己的数据库信息就能用:

# 技术栈:Shell + MySQL
# 功能:每天凌晨自动将7天前的XXL-JOB执行日志分页归档到历史库
# 配置说明:需要提前创建xxl_job_log_his库,表结构与xxl_job.xxl_job_log一致
# -------------------------- 配置项替换成自己的 --------------------------
DB_USER="你的MySQL用户名"
DB_PASS="你的MySQL密码"
MAIN_DB="xxl_job"       # 线上热数据库名
HIS_DB="xxl_job_log_his" # 历史冷数据库名
KEEP_DAYS=7             # 保留热数据的天数,可根据业务调整
BATCH_SIZE=1000         # 每次处理的日志数量,别太大,避免锁表
# -------------------------- 配置结束 --------------------------

# 计算要归档的时间点:当前时间减去7天
ARCHIVE_THRESHOLD=$(date -d "$KEEP_DAYS days ago" +"%Y-%m-%d %H:%M:%S")

# 循环分页归档,直到没有待归档的日志
while true; do
    # 取本次要归档的1000条日志ID,用grep过滤表头,tr把换行转成逗号
    TARGET_IDS=$(mysql -u$DB_USER -p$DB_PASS -D$MAIN_DB -e "SELECT id FROM xxl_job_log WHERE log_time < '$ARCHIVE_THRESHOLD' LIMIT $BATCH_SIZE;" | grep -v "id" | tr '\n' ',')
    # 如果没有待归档的ID,退出循环
    if [ -z "$TARGET_IDS" ]; then
        break
    fi
    # 去掉最后多余的逗号
    TARGET_IDS=${TARGET_IDS%,}
    # 把日志插入历史库
    mysql -u$DB_USER -p$DB_PASS -D$HIS_DB -e "INSERT INTO xxl_job_log SELECT * FROM $MAIN_DB.xxl_job_log WHERE id IN ($TARGET_IDS);"
    # 从线上库删除已归档的日志
    mysql -u$DB_USER -p$DB_PASS -D$MAIN_DB -e "DELETE FROM xxl_job_log WHERE id IN ($TARGET_IDS);"
done

echo "日志归档完成,保留最近$KEEP_DAYS天的热数据"

3.3 设置定时调度

用Linux的crontab设置定时任务,执行crontab -e,加上下面这行,就能每天凌晨2点自动运行归档脚本:

0 2 * * * /bin/bash /路径/archive_job_log.sh >> /路径/archive_log.log 2>&1

四、这套方案的优缺点和注意事项

4.1 优点

第一,线上查询速度快,因为热数据只有7天的,数据量小,索引找得快,不会卡;第二,归档时不影响线上,分页处理的锁表时间极短,业务不受影响;第三,历史数据还在,要查旧记录直接去历史库,不会丢,满足可追溯的要求;第四,不用改XXL-JOB的源码,只是加个定时脚本,风险低,容易上线。

4.2 缺点

多了一个历史库,要多维护一个库的备份和监控;如果忘了归档,热数据还是会疯长,所以要定期检查归档是否成功;另外,超过7天的日志才归档,如果业务需要查更早的,得去历史库查,稍微多一步,但大部分业务也不会这么做。

4.3 必须注意的细节

分页的数量不能太大,最多1000条就行,不然还是会锁表;归档时间要选在业务低峰,比如凌晨,别影响白天的任务;要定期核对历史库和线上库的日志数量,防止归档时丢数据;如果XXL-JOB还有其他关联日志表(比如xxl_job_log_report),也要一起归档,不然数据不一致,查的时候会找不到对应记录。

五、总结

XXL-JOB日志表疯长是很多用这个工具的团队都会遇到的问题,本质就是“常用数据和不常用数据混在一起,导致常用的那部分用起来卡”。用冷热分离+分页归档的策略,相当于给日志做了“垃圾分类”,把常用的热数据放在干净的小盒子里,不用翻大箱子,自然快;把不常用的冷数据放在专门的箱子里,既不占地方,又能留着备查。这套方案不用改源码,上手简单,能快速解决性能问题,同时满足历史记录可追溯的要求,适合大多数使用XXL-JOB的团队落地。