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

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

公司某业务出现严重的线上故障,复盘发现原因竟是某接口的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。


相关文章
|
SQL 存储 缓存
OceanBase查询优化器
本文整理自OceanBase团队高级技术专家王国平,在深入浅出 OceanBase线上技术沙龙第二期的分享。
OceanBase查询优化器
CocosCreator 面试题(十六)Cocos Creator 节点池的基本原理是什么?如何使用?
CocosCreator 面试题(十六)Cocos Creator 节点池的基本原理是什么?如何使用?
1090 0
|
Java 数据库连接 Maven
分布式——Maven多模块管理
Maven的多模块管理。所谓Maven的多模块管理其实:子模块继承父模块的Maven依赖,这样在多模块开发之下,多个模块的依赖版本就是一样的,这样就不会造成因模块依赖的版本不同而造成的冲突。其实Mavne管理的就是依赖的版本号。
分布式——Maven多模块管理
|
SQL 算法 数据库
OceanBase 查询优化 | 学习笔记
快速学习 OceanBase 查询优化
OceanBase 查询优化 | 学习笔记
|
Java jvm-sandbox Perl
Jvm-Sandbox源码分析--启动简析
1.工作原因,使用jvm-sandbox比较多,遂进行源码分析,做到知己知彼,个人能力有限,如有错误,欢迎指正。 2.关于jvm-sandbox 是什么,如何安装相关环境,可移步官方文档 3.源码分析基于jvm-sandbox 最新的master代码,tag-1.2.1。
8566 1
Jvm-Sandbox源码分析--启动简析
|
6月前
|
Java 数据库 C++
Java异常处理机制:try-catch、throws与自定义异常
本文深入解析Java异常处理机制,涵盖异常分类、try-catch-finally使用、throw与throws区别、自定义异常及最佳实践,助你写出更健壮、清晰的代码,提升Java编程能力。
|
监控 网络协议 安全
在Linux中,如何进行系统性能的峰值测试?
在Linux中,如何进行系统性能的峰值测试?
|
小程序 前端开发 测试技术
微信小程序|ssm基于微信小程序的高校课堂教学管理系统
微信小程序|ssm基于微信小程序的高校课堂教学管理系统
328 1
|
算法 Java 应用服务中间件
阿里面试:说说自适应限流?
限流想必大家都不陌生,它是一种控制资源访问速率的策略,用于保护系统免受过载和崩溃的风险。限流可以控制某个服务、接口或系统在一段时间内能够处理的请求或数据量,以防止系统资源耗尽、性能下降或服务不可用。 常见的限流策略有以下几种: 1. **令牌桶算法**:基于令牌桶的方式,限制每个单位时间内允许通过的请求量,请求量超出限制的将被拒绝或等待。 2. **漏桶算法**:基于漏桶的方式,限制系统处理请求的速率,请求速率过快时将被限制或拒绝。 3. **计数器算法**:通过计数器记录单位时间内的请求次数,并根据设定的阈值进行限制。 通过合理的限流策略,可以保护系统免受恶意攻击、突发流量和资源
204 4
阿里面试:说说自适应限流?
|
域名解析 网络协议
WeCom——通过企业微信来搭建自己的域名邮箱
WeCom——通过企业微信来搭建自己的域名邮箱
1527 0