大面积故障规避案例

简介: 本文记录了一次由Kotlin语法误用引发的FastJson反序列化全局异常问题。因混编环境下将`{}`误赋值给Java对象字段,导致FastJson解析时触发静态标记位`kotlin_error`,进而使整个应用反序列化失效。排查耗时两天,揭示了多语言混编、框架兼容性及静态状态风险等深层问题,值得开发者警惕。

在短短不到两年的开发生涯里,加上这次,印象中已经碰到过至少3次FastJson的问题了。而且FastJson不同版本之间的差异很大,各位同学在使用时一定注意不要踩坑。
下面讲一下我碰到的这个细思极恐的问题。
一、首先讲讲工程背景
我们的工程是Kotlin与Java混编的,再附加偶尔写写Groovy,在团队中不断熟悉后发现各语言在编程中各有利弊。
● Java就不说了,阿里在国内Java应用上堪称鼻祖,集团各种工具对Java的支持都比较完备;
● Kotlin有很多语法上的优势,同样的代码Java 10行,Kotlin可能只需要5行,此外支持协程(coroutines),编写非阻塞异步代码看起来像是同步的,能很好地处理IO密集型任务。但是Kotlin毕竟非正统,集团很多工具对Kotlin的支持性比较一般;
● groovy的语法规则更加特殊,工程里用的不是特别多,所以基本都是需要开发时用大模型去解决,没有特意去研究;
二、问题现象
突然有一天组里同学告诉我预发环境(开发-测试-预发/灰度/UAT-生产)有大量报错,是不是我改了架构层面的代码引起的。我心想肯定不是啊,我这是增量编程,并没有改动太多原来的代码。
但是处于谨慎起见,还是把分支踢掉吧,重新部署了一下,发现同样的问题在工程重启一小段时间后又发生了。
下面是问题报错,这会导致工程运行时大部分涉及反序列化的链路中断,而工程到处用了FastJson,影响面可想而知。

三、排查过程

  1. 怀疑FastJson版本
    既然是FastJson的抛错,那是不是有人改动了工程依赖,于是开始查pom文件的改动,一时也想不到别的原因可能导致该报错。
    查了一圈并没有发现可疑的地方,于是上AppInsights中看了一下FastJson相关类的加载情况,发现这里有个1.2.68_noneautotype版本的包,而工程其实统一用的是1.2.83_noneautotype,会不会是这里引起的呢?

后来登到线上机器对比了一下发现没有这个版本,那有可能就是别的包引入的,于是看了一下出问题module的pom文件,发现果然有这个版本的FastJson,再定位了下发现是rass-sdk-core引入了这个包,scope为compile,打包时会将这个包编译进工程中。

很果断的将该包中的FastJson排掉,那问题铁定解决了,观察了日志发现没有再抛错,我就没再管。到了下午同学又来反馈有问题,我一看还真是,难道是搞错了。
再次对比了一下预发和线上,发现还真是我搞错了,线上也有1.2.68_noneautotype。
我有点郁闷,为了定位这个bug,翻遍了预发每个人的分支,为了排包前后部署过很多次。此时这个问题已经困扰了我1天多了,想撂挑子放弃,到时候谁发线上时注意下好了。
但又一想这是我们辛苦维护的庞大工程,不能因为这种莫名其妙的问题引发大面积故障,到时候辛苦半年多白干(上价值,给自己增加点动力)。理智告诉我哪怕手头的需求先放一放,还得继续查,虽然只是预发。

  1. 怀疑kotlin相关依赖
    跟同学确认了JDK近期也没有变动后,接下来开始到处翻资料。
    大概能确认是反序列化Kotlin的data class实体时出现的问题。类似下面这种结构

GitHub上关于该问题的讨论也比较多,总结一下解法有:
1.FastJson和kotlin版本不兼容
2.工程中需要引入kotlin-reflect
3.需要对data class的非空字段默认初始化
4.……

第三个字段初始化问题不太可能,因为受影响的class从来没改动过,不可能好端端出问题。估计问题应该还是出在工程环境上。
于是去工程里看了一下kotlin-reflect依赖,仍然是正常引入的。
又去看了几遍近期所有分支的所有改动,也没有任何相关变动,慢慢地开始头麻,环境问题一向是最难的。。。。

  1. 翻阅到相似的例子
    接下来就还是一直在网上翻阅其他资料,后来在掘金找到一篇: https://juejin.cn/post/6929740384734019597

文章提到的问题现象跟我们的还挺像的,都是一开始正常,过了一会儿就又出现问题。
文章对FastJson剖析挺深,感觉我们的工程不至于这么奇葩吧。还提到了什么getKoltinConstructorParameters、Unit变量、kotlin_error标记位问题。核心原因是有个静态标记位被置为异常状态。

虽然抽象,但死马当活马医,试试看吧。
先看工程里有没有打印这个NPE吧,SLS日志搜了半天都没搜到,去源码看了一下,原来这里只打印了异常堆栈,那不会被采集到SLS中,不过服务器日志里可能会有。

妈耶还真给我搜到了,再仔细读了一下打堆栈的地方,会将kotlin_error置为true,而且关键的地方在于这玩意儿是个static volatile变量!!具有静态共享特征(static)和多线程可见性(volatile),意味着整个工程使用的都是被修改后的值。

  1. ☆ 锁定关键报错位置
    继续进QuestionCardProxyApi 228行 找到报错源,发现确实有个toJsonString。对于invokeResumeReq这个对象,我一眼看到有个古怪的赋值,this.resumeBody被赋值为{}。但这个赋值有何神奇之处,哪能这么个小玩意儿导致全局异常啊??

这段日志代码是12-17添加的,时间看了下还挺符合。再看resumeBody其实是个Java类的一个object类型字段,kotlin中这样对其赋值,编译器倒也没报错。

其实到这个地方已经感觉要水落石出了,再一看master,妈耶这个代码上线了,但线上却没有任何报错,运行一切正常。
但基本能确定是这里无疑了,就赶紧拉写这段代码的同学看(悄悄说,仁兄一开始提给我的bug),确认线上还没开始灰度,无任何流量后放心了。他在家里加班修,我继续看原理。
首先肯定本意是想将resumeBody置空,误用了{},{}在kotlin中被编译器解释为一个lambda表达式。

再看debug信息,这里resumeBody实际上被解释为 ()->kotlin.Unit 这样一个lambda函数,arity表示函数的参数数量为0.

也就是说:{}是一个没有任何入参,并且返回值默认为Unit类型的一个函数(没有明确返回值时默认Unit类型)。
为了更清晰地了解这个语法,可以看下面这个例子:这里定义了一个函数式变量x,入参为s,出参为s+“000”。在使用时可以直接以函数的方式调用x,最终得到y=111000

FastJson自然无法正确解析这样的一个Object字段,而最令人细思极恐的问题是 解析这个对象报了错,却把kotlin_error置为true,而后有没有地方将其复原为false,导致所有的反序列化都进到这里,返回不正确的结果,错误结果被外层拦截抛出default constructor not found异常。这个影响面是巨大的,会导致整个工程崩溃。
具体代码解释:
1.一开始 kotlin_error !=true ,会进到 kotlin_kclass_getConstructors.invoke获取类构造器,这里抛错了,把kotlin_error改为true;
2.往后所有相关的FastJson序列化、反序列化重新进到这里都会return null,导致类构造器获取失败;
3.外层没拿到paramNames,直接抛了异常。

  1. 有问题的代码(自测)
    有kotlin运行环境的同学可以尝试运行下面的代码自测。
    注意:经测试这段代码在FastJson 2.0.53版本可以正常运行,其他版本同学们可以再自测下。

四、总结&反思
至此,问题定位清楚并彻底解决了。这次bug是工作以来碰到的最抽象的一个,耗时两天。虽然有点低效,但找到原因后俺非常激动,逮住组里同学细细讲了一番,估计这么抽象的问题工作多年的大佬也不一定能遇到。
再次膜拜一下掘金大佬:https://juejin.cn/post/6929740384734019597
另外也反思了以下几个问题:
1.工程中Java、kotlin、groovy等多语言混编,对开发同学的语法掌握程度有较高要求,有时候各语言间会混淆,特别是判空、变量定义规则等。这些语言的线上空指针我都尝过,有点酸爽。这次同学出现语法问题,其实也是有点语言混淆了,如果纯Java,铁定不会甩个{}上去;
2.这次线上无异常,得益于灰度开关(哥们可得感谢我,还没放量,否则一旦流量进来,涉及kotlin的链路全部中断就寄了);
3.FastJson是有很多漏洞的,使用时仍然要高度注意。任何框架都是不能完全信任的,毕竟代码都是人写出来的,Bug要大家一起合力发现hhh;
4.写Bug是每个开发同学都会经历的,朋友经常嘲笑我写Bug。但实际上你写过的Bug越多,说明越有经验,吃一堑长一智,下次不要再犯就好了。

相关文章
|
12天前
|
JSON C# 数据格式
C# JSON 序列化与反序列化:Newtonsoft.Json 用法
JSON是前后端交互常用格式,Newtonsoft.Json(Json.NET)是C#中最流行的JSON处理库。本文介绍如何使用它实现对象与JSON字符串的序列化和反序列化,并提供封装工具类及调用示例,便于在项目中快速集成与使用。
|
20天前
|
Java
常见加载顺序
本示例展示了Java中各类代码块的执行顺序:静态代码块随类加载仅执行一次,优先于主函数;局部代码块在方法内直接运行;构造代码块每次创建对象前自动执行,早于构造器。输出结果体现三者优先级:静态 > 局部 > 构造。
|
21天前
|
存储 监控 Java
整合切面,参数拦截+过滤
该类基于Spring AOP实现请求参数日志记录,通过`@Before`、`@Around`和`@After`切面拦截Controller层方法,自动记录请求来源、URL、方式、参数及执行耗时,便于调试与监控,日志通过LogProxy输出,提升系统可观测性。(238字)
|
20天前
|
SQL Java 关系型数据库
分页
本文介绍了五种分页实现方式:MyBatis自带RowBounds内存分页、PageHelper插件分页、SQL原生分页、数组分页及拦截器分页。对比了逻辑分页(内存处理)与物理分页(数据库层处理)的优劣,指出大数据量下应优先选用物理分页以避免内存溢出,提升性能。
|
20天前
|
XML SQL 监控
整合Logback,滚动记录+多文件
`logback-spring.xml` 配置了多模块日志分离输出,按类别将支付、任务、SQL、错误等日志写入不同文件,支持滚动策略与UTF-8编码。通过 `LogProxy.getLogger("LOG_NAME")` 获取指定日志器,实现精准日志记录,便于问题追踪与系统监控。(236字符)
|
20天前
|
XML JSON Java
映射关系(1-1 1-n n-n)
MyBatis中通过resultMap实现关联映射:一对一使用resultMap解决字段与属性名不一致;一对多在“一”方配置<collection>,如用户包含多个角色;多对一通过<association>关联,如博客关联作者;多对多借助中间类,双方均用<collection>维护集合关系。
|
20天前
|
存储 NoSQL Linux
MongoDB单机部署
提供Win32/64位MongoDB安装包,支持命令行或配置文件启动,Linux与Windows系统均可部署。建议选择y为偶数的稳定版本,通过官网下载并解压,配置data目录及mongod.conf,使用mongod启动服务,mongo命令连接。可选Compass图形化工具管理数据库。注意端口、路径格式与防火墙设置。
|
21天前
|
存储 安全 Java
Java泛型类型擦除以及类型擦除带来的问题
Java泛型在编译时会进行类型擦除,所有泛型信息被移除,替换为原始类型(如Object或限定类型)。例如,List<String>和List<Integer>在运行时均为List,导致无法通过instanceof判断泛型类型。类型检查在编译期完成,基于引用而非实际对象。擦除后,编译器自动插入强制转换保证类型安全。但这也引发多态冲突、静态成员限制等问题,需通过桥方法等机制解决。基本类型不能作为泛型参数,静态上下文中也不能使用类级别泛型参数。
|
20天前
|
JSON Java Maven
SpringBoot使用汇总
Spring Boot是Spring框架的延伸,旨在简化Spring应用的初始搭建与开发过程。它通过自动配置、内嵌服务器、开箱即用的依赖等方式,极大减少了项目配置和编码量,提升开发效率。支持快速构建微服务,是Java EE开发的主流趋势。
|
20天前
|
Arthas 存储 运维
记Arthas实现一次CPU排查与代码热更新
本文介绍如何使用Arthas排查线上Java应用CPU占用过高问题,结合thread、watch、jad等指令定位阻塞线程与异常代码,实现无需重启服务的热更新修复,并通过profile生成火焰图进行性能分析,提升线上问题排查效率。

热门文章

最新文章