银行风控项目踩坑实录:指标跑了6小时,风险评分全挂了

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 这件事教会了我什么

这次事故之后,我有三个很深的体会:

  1. 测试环境和生产环境,不是放大版的关系------是两种完全不同的生物。

测试环境跑得通的东西,生产环境不一定跑得通。数据量差两个数量级,很多问题才会暴露出来。

  1. 写代码的时候,脑子里要有一根弦:这行代码会处理多少数据?

不是所有问题都能靠"优化查询""加缓存"解决。有些场景必须换思路------比如从"一次性加载"换成"分页处理"。

  1. 银行的风控系统,容错率是零。

电商系统挂了,用户刷不出来商品,最多抱怨几句。银行的风控系统挂了,贷款批不了,那是真金白银的业务损失,也是实实在在的监管压力。

作为外包开发,我们也许不直接承担那个压力,但午夜被叫起来修Bug的时候,那种"不能让业务停"的紧迫感是一样的。

07 写在最后

这就是我在银行做风控指标平台时,印象最深的一次线上事故。

如果你也在做类似的风控系统、数据平台或者指标计算相关的项目,希望这篇文章能帮你避开我踩过的坑。

相关推荐
随读手机1 小时前
多式联运信息交互平台完整方案(2026版)
java·ai·eclipse·云计算·区块链
许彰午2 小时前
03-二叉树——从递归遍历到非递归实现
java·算法
nj01282 小时前
Spring 循环依赖详解:三级缓存、早期引用、AOP 代理与懒加载
java·spring·缓存
野生技术架构师2 小时前
2026年最全Java面试题及答案汇总(建议收藏,面试前看这篇就够了)
java·开发语言·面试
一只叫煤球的猫3 小时前
ThreadForge 源码解读一:ThreadScope 如何把并发任务放进清晰边界?
java·面试·开源
洛_尘4 小时前
Python 5:使用库
java·前端·python
程序员小假4 小时前
HTTP3 性能更好,为什么内网微服务依然多用 HTTP2?HTTP2 内网优势是什么?
java·后端
Mr数据杨4 小时前
【Codex】用教案主体模块沉淀标准化教学设计内容
java·开发语言·django·codex·项目开发
苍煜4 小时前
RocketMQ系列第三篇:Java原生基础使用实操,手把手写生产者消费者Demo
java·rocketmq·java-rocketmq