一起变更引发的惨案(上)

简介: 一起变更引发的惨案(上)

公司某业务出现严重的线上故障,复盘发现原因竟是某接口的Thrift IDL变更,未及时同步所有上游,导致上游某服务OOM引发Crash。看似操作不规范是本次故障的根因,但进一步思考:该接口非主流程接口,即使IDL版本不统一,带来“最坏”的后果不应该是“仅仅报错”吗,为什么会产生OOM导致整个服务Crash?


实际上这样的例子并不少见,很多公司内部RPC使用Thrift协议,IDL变更及版本不一致在所难免,极端情况下,安全团队扫描thrift端口也会出现类似故障。为了避免更多团队采坑,有必要研究Thrift何种情况下会触发,以及为什么会触发OOM。


一. 从IDL的字段变更说起

化繁为简,该故障的发生可以这样描述:某Thrift服务的返回值是一个数组,数组中每个元素本来包含5个字段,某次调整后,在中间位置新增1个字段,其余保持不变。服务上线后,某上游调用方未收到变更通知,仍使用旧版本的SDK。参照下图所示:


image.png


看到这里,有经验的同学已经瑟瑟发抖。在Thrift协议跨语言、高性能的背后,做了很多取舍,比如接收方在反序列化时,仅做了方法名等少量的校验,通过序号、字段类型认为该返回值属于哪个字段,而并未校验字段名,因此一旦上下游IDL版本不一致,极易产生字段错位的情况。以上述IDL为例,id、image字段正确解析,而bg_image被错误解析为link、link被错误解析为title:


image.png


字段增加时,考虑到字段位置及新旧版本不匹配的各种场景,我们枚举可能的后果:


1.   新增字段放到中位置

A.      新Server、旧Client:容易字段位,引发业务错误,不推荐

B.      旧Server、新Client:容易字段位,引发业务错误,不推荐


2.   新增字段放到最后,并且是required

A.      新Server、旧Client:旧Client感知不到新增字段,会忽略

B.      旧Server、新Client:新Client获取不到新增字段,会报错


3.   新增字段放到最后,并且是optional

A.      新Server、旧Client:旧Client感知不到新增字段,会忽略

B.      旧Server、新Client:新Client获取不到新增Optional字段,会忽略


再进一步,如果是删除字段呢?从删除字段的位置、是否required,大家可自行思考,当然结果类似,轻则被忽略,重则字段错位,当然更严重的会导致服务OOM。

小结:Thrift IDL不要变更已有字段的序列号,上下游版本不一致极易发生错位现象。如需新增字段,应放到最后并设置为optional。

二. OOM问题复现

回到IDL变更上来,为什么会引发上游服务的OOM呢?我们用一段很短的IDL就可以重现。


namespace java  com.didiglobal.thrift.sample
struct Items{
      1:required i64 id;
      2:required list<Item> items;
}
struct Item {
     1:required string name;
     2:required string image; // 新增字段
     3:required list<string> contents;
}
service Sample {
     Items getItems(1:i64 id);
}


如果你也想尝试,可以下载项目代码,在本地搭建环境。

1.   Github下载项目代码:https://github.com/aqingsao/thrift-oom

2.   使用你喜欢的IDE导入工程,该项目基于thrift 0.11.0版本,依赖JDK1.8+以及Maven

3.   运行包com.didiglobal.thrift.sample1.sampleold中OldClientNewServerTest.java类的这个测试用例:oldclient_should_oom_at_concurrency_10

该测试用例会启动一个简单的Thrift服务,客户端使用10个并发,很快触发OOM(如果遇到问题,可联系作者)。


使用不同的Thrift版本,0.9.3到最新的0.13.0-snapshot均可以重现。使用jmap命令,可以看到应用创建了大量的字节数组。


小结:只需要10个并发就可以重现OOM,该问题广泛存在于Thrift 版本0.9.3到最新的0.13.0-snapshot。

三. 为什么会OOM?

本次故障的根因是新增String字段,并且客户端进程创建了大量的字节数组,首先怀疑字段错位后Thrift未正确处理,但查看了下源码,thrift对字段类型不匹配、多余字段均作了skip处理:



image.png


思考了几个其他方向,并做了多次尝试,均发现思路不对,而且始终无法解释的是,单个请求数据量只有上百字节,为什么只需要10个并发、几十个请求就会OOM?这些请求的数据量累加起来也不过几十K,一度陷入僵局。


下班的路上反复思考,是不是和Thrift抛出的异常有关,恰遇某个路口超长时间的红灯,每每感叹该路口浪费多少青春年华,此时却从容拿出电脑,对Thrift抛出的异常做了临时处理,运行测试,果然不再OOM了!


思路一下子清晰起来:虽然Thrift做了多余字段的skip处理,但由于抛出的异常,这些skip操作并未执行到,甚至,List中第一个元素校验抛出异常后,后面所有字段都未继续消费!


按该思路重新阅读相关代码,果然找到了可疑点:Thrift接受服务端响应时,会首先解析TMessage对象,前4个字节(I32)代表了某个字符串的长度,后面readStringBody()方法会分配该长度的字节数组(byte[] buf = new byte[size]),该字符串实际上是thrift的方法名,而debug发现,长度值是184549632,大约176M,这合理解释了为什么10个并发就会触发OOM。


image.png


小结:Thrift接收到请求后首先读取TMessage结构,IDL版本不一致的极端情况下,会分配176M的内存空间,导致10个并发就占用上G内存,触发OOM。


相关文章
CocosCreator 面试题(十六)Cocos Creator 节点池的基本原理是什么?如何使用?
CocosCreator 面试题(十六)Cocos Creator 节点池的基本原理是什么?如何使用?
1174 0
|
Java 数据库连接 Maven
分布式——Maven多模块管理
Maven的多模块管理。所谓Maven的多模块管理其实:子模块继承父模块的Maven依赖,这样在多模块开发之下,多个模块的依赖版本就是一样的,这样就不会造成因模块依赖的版本不同而造成的冲突。其实Mavne管理的就是依赖的版本号。
分布式——Maven多模块管理
|
Java Maven
MapStruct - Lombok & Maven 版本关系
MapStruct - Lombok & Maven 版本关系
2272 0
|
3月前
|
人工智能 缓存 调度
深度解析:AI Agent 指挥官如何通过调度策略降低算力成本
2026年,算力成本成AI落地最大瓶颈。本文提出“AI Agent指挥官”范式,聚焦成本感知调度:通过异构模型路由、语义缓存增强、异步并行批处理与动态提示精简四大策略,实现智能与经济的最优平衡,实测降本达83%。(239字)
372 0
|
8月前
|
Java 数据库 C++
Java异常处理机制:try-catch、throws与自定义异常
本文深入解析Java异常处理机制,涵盖异常分类、try-catch-finally使用、throw与throws区别、自定义异常及最佳实践,助你写出更健壮、清晰的代码,提升Java编程能力。
|
9月前
|
并行计算 Java API
Java List 集合结合 Java 17 新特性与现代开发实践的深度解析及实战指南 Java List 集合
本文深入解析Java 17中List集合的现代用法,结合函数式编程、Stream API、密封类、模式匹配等新特性,通过实操案例讲解数据处理、并行计算、响应式编程等场景下的高级应用,帮助开发者提升集合操作效率与代码质量。
424 1
|
监控 网络协议 安全
在Linux中,如何进行系统性能的峰值测试?
在Linux中,如何进行系统性能的峰值测试?
|
存储 缓存 自然语言处理
(三)JVM成神路之全面详解执行引擎子系统、JIT即时编译原理与分派实现
执行引擎子系统是JVM的重要组成部分之一,在JVM系列的开篇曾提到:JVM是一个架构在平台上的平台,虚拟机是一个相似于“物理机”的概念,与物理机一样,都具备代码执行的能力。
507 1
|
小程序 前端开发 测试技术
微信小程序|ssm基于微信小程序的高校课堂教学管理系统
微信小程序|ssm基于微信小程序的高校课堂教学管理系统
380 1
|
域名解析 网络协议
WeCom——通过企业微信来搭建自己的域名邮箱
WeCom——通过企业微信来搭建自己的域名邮箱
2079 0

热门文章

最新文章