01 背景:这个平台是干什么的?
先简单介绍一下项目。
某银行要做一套风控指标平台,核心目标是为信贷审批场景提供实时/离线的风险评分指标。
通俗点说:
银行在审批一笔贷款之前,需要判断这个人"会不会违约"。判断的依据不是拍脑袋,而是几百个指标------比如"过去1年逾期次数""近3个月申请贷款的次数""负债收入比"等等。
这些指标不是直接能从数据库里拿到的,需要通过原始数据(交易记录、申请记录、征信报告等)计算出来。这个过程就叫指标加工。
我做外包的那个组,负责的就是这个平台的后端开发和运维。
技术栈:
Spring Boot 2.3 + MyBatis Plus
MySQL 8.0(分库分表,单表数据量过亿)
Redis(缓存指标元数据)
XXL-Job(调度指标计算任务)
数据源:离线数仓(Hive)+ 实时Kafka
我负责的模块:
指标配置管理(增删改查、版本控制)
指标计算任务的调度编排(配置任务依赖、触发条件)
项目上线前两个月,整体还算顺利。
直到那天。
02 事故发生:凌晨两点,收到告警
那天是周三,凌晨两点多。
我被电话吵醒。
值班同事在群里发消息:"指标计算任务卡住6小时了,上午9点的风险评分全都没出。"
我迷迷糊糊打开电脑,登录XXL-Job后台。
一看,愣住了。
今天凌晨应该跑的80多个指标计算任务,只有不到20个完成了。剩下的全卡在"运行中"状态,有的已经跑了将近5个小时。
按照设计,单个指标的平均计算时间不应该超过10分钟。
这意味着:上午所有信贷审批都会因为没有风险评分而暂停。
我瞬间清醒了。
03 排查过程:先从最可疑的地方下手
第一步:看调度日志
XXL-Job的日志显示,卡住的任务集中在同一个任务组------这批任务都是依赖某张用户行为流水表计算的。
日志最后一条记录停在凌晨1点23分,之后没有任何输出。
没有报错,没有异常堆栈,进程就这样静默卡死了。
第二步:看服务器资源
登录服务器,执行
top
命令。
CPU使用率只有15%,不高。但内存占用已经到了92%。
再执行
free -h
,发现交换分区(Swap)被用了将近8个G------物理内存明显不够用了。
第三步:定位到具体进程
用
jstat -gcutil [pid] 1000
看JVM内存使用情况。
老年代(Old Generation)占用率98%,Full GC一直在执行,但内存始终释放不下来。
典型的内存泄漏或者内存溢出前兆。
第四步:看具体的计算逻辑
我把卡住的任务对应的代码翻出来。
那段代码的逻辑是:从用户行为流水表中,按用户ID分组,计算过去90天内每个用户的"贷款申请次数"指标。
看起来不复杂,但有个严重的问题:
数据量估算错了。
上线前,测试环境里的用户行为流水表只有100万行数据。但生产环境里,这张表有2.7亿行。
代码里用的方式是:一次性把整个表的数据加载到内存里,再按用户ID分组聚合。
2.7亿行 × 每行几百字节 ≈ 几十个G的数据。
物理内存只有16G,不OOM才怪。
更糟糕的是,OOM触发了频繁的Full GC,但数据还没处理完,GC根本没法回收。最后任务卡死在"快要OOM但还没彻底崩溃"的边缘,既不报错也不继续。
04 不止一个坑:还有一条慢SQL
排查过程中,我还发现了另一个问题。
有一个依赖征信报告表的指标,计算SQL在没有索引的字段上做了
GROUP BY
。
用
EXPLAIN
看了一下:
扫描行数:1800万
Extra字段:
Using temporary; Using filesort
没有命中任何索引
这条SQL单独跑就要40多分钟。
而且这个任务和前面OOM的任务有依赖关系------后者必须等前者跑完才能开始。
一个慢40多分钟,另一个直接OOM卡死,整个任务链就瘫痪了。
05 解决方案:先止血,再根治
临时止血(凌晨3点-5点):
Kill掉卡住的任务:手动终止XXL-Job上所有运行超过2小时的任务。
拆分数据批次:把那个一次性加载全表的任务,改成按日期分批次处理(每次处理一个月的数据)。
调大JVM内存:临时从16G调到24G(服务器还有余量)。
手动触发失败任务:优先跑那些不依赖大表的简单指标。
凌晨5点半,第一批风险评分终于算出来了。虽然比正常时间晚了将近7个小时,但至少上午的业务没有停摆。
长期根治(后续两周):
改代码:把一次性加载全表的逻辑,改成分页查询 + 流式处理。每次只处理10万行,处理完就GC回收。
加索引:在征信报告表的
apply_date
和
user_id
字段上加了复合索引,慢SQL从40多分钟降到4分钟。
增加监控:在XXL-Job里加了任务执行时间阈值告警(超过30分钟自动报警)。
写规范:规定所有指标计算任务,上线前必须评估数据量,预估内存占用。
06 这件事教会了我什么
这次事故之后,我有三个很深的体会:
- 测试环境和生产环境,不是放大版的关系------是两种完全不同的生物。
测试环境跑得通的东西,生产环境不一定跑得通。数据量差两个数量级,很多问题才会暴露出来。
- 写代码的时候,脑子里要有一根弦:这行代码会处理多少数据?
不是所有问题都能靠"优化查询""加缓存"解决。有些场景必须换思路------比如从"一次性加载"换成"分页处理"。
- 银行的风控系统,容错率是零。
电商系统挂了,用户刷不出来商品,最多抱怨几句。银行的风控系统挂了,贷款批不了,那是真金白银的业务损失,也是实实在在的监管压力。
作为外包开发,我们也许不直接承担那个压力,但午夜被叫起来修Bug的时候,那种"不能让业务停"的紧迫感是一样的。
07 写在最后
这就是我在银行做风控指标平台时,印象最深的一次线上事故。
如果你也在做类似的风控系统、数据平台或者指标计算相关的项目,希望这篇文章能帮你避开我踩过的坑。