性能文章>线上服务挂了 3 分钟>

线上服务挂了 3 分钟原创

326856

你好,我是yes。

在一个风和日丽的下午,刚打算饮茶,线上就开始报警了,一看情况网关报 500 了。。

网关(用的是Spring Cloud Gateway)挂了可还行,这可是对外的们,门没了岂不是所有请求都进不来了!

说好的动态调度扩缩容负载均衡呢??怎么没支棱起来?

及时处理

话不多说,直接重启。

然后看起来好了,重启大法!yyds!

问题排查

感觉有点突然,最近网关并没有发版,上一个版本还是在 4 月份,挂的有点莫名其妙。

不过这两天在执行一个动态迁移服务,有大量的请求,预估两天的请求量有 300 w,我一开始以为是被这个打挂了。

但是有关的接口我都加了限流,且请求一直是匀速的,也已经正常处理一天一夜了,服务的各项指标也没任何异常,咋就突然这样了呢?

仔细看了看,网关的服务没挂,但是请求都返回了 500。

上去一查日志:

我擦,这是什么玩意?

看起来是要创建临时目录,然后失败了,因为空间不足,但是实际上我看磁盘空间还很多,这时候我已经有点感觉了。

可以看到是 Spring 的 SynchronossPartHttpMessageReader 类触发的这个报错,网关代码确实有用到 HttpMessageReader 相关的解析。

然后进行一波源码分析:

点进源码粗略了看了下,每个请求的解析都会创建一个临时目录,往里跟了几步看了看,这个临时目录的作用是到时候用来给解析 Multipart 存储临时文件用的。

但最近的请求也都跟 Multipart 没关系了,咱也不懂为啥要这样先预创建。

看到这反正问题已经定位到了:因为网关要用到 HttpMessageReader 相关的解析,而这个解析实现类,每次都会预创建一个临时目录,用来到时候给 Multipart 用(即使实际没这玩意),因此每个经过网关的请求,都会在网关本地服务器上创建一个临时目录

而由于这两天请求非常多,创建了很多目录,把操作系统的文件 inode 占满了,使得后续的所有请求在执行到创建临时目录的方法时就报错了,因此所有请求都返回了 500。

inode

这边先介绍下 inode。

在类Unix文件系统中,文件的元数据存储的地方叫 inode,也就是文件元数据和文件的数据是分开存储的。

所谓的元数据指的是:文件的大小、创建时间、修改时间、权限等等,它也会占用磁盘空间。

因此操作系统分配给 inode 存储空间也是有限的,超过了限制就申请不了

所以有时候磁盘空间够的,但是文件还是无法创建,一种可能就是 inode 满了。

对了,对 Linux 这类系统而言,目录也是文件,一样的。

tmp

tmp 目录其实是有讲究的,临时目录。

理论上这个目录默认操作系统会有个叫 tmpwatch 的玩意去清理长时间无用的文件,一般会定时去清理。

而且重启系统的话,这里面的文件也会被清空。

至此,产生问题的原因应该非常清晰了。那如何解决?

解决

  1. 利用 tmpwatch 勤快点去清理,比如近几个小时没用的就直接干了。
  2. 不用 HttpMessageReader
  3. 定时重启
  4. 修改源码
  5. 看看后续版本

第一点不太好,因为有几率误删有用的文件。第二点理论上用了  Spring 体系,这玩意好像不太好避免,先暂定。第三点,太骚了,还是算了。第四点,改起来不难,但是后面升级版本啥的不太方便

先试试第五点。

然后我就去 spring-cloud github 搜一搜,果然有 issue。

点进去一看,关联到另一个 issue,一看巧了,一模一样的错:

然后他圈了好几个老哥,有个叫 poutsma 老哥回答了他的问题:

我简单翻译下:这样的实现是为了解决一个安全问题,为了不创建一个固定的临时目录,如果使用固定目录会带来严重的安全隐患,然后也不能在退出后删除,因为这样会删除所有上传的文件。

简单来说:不是bug

而且目录本身占用的磁盘空间可以忽略不计,只有当大量上传的文件存储在那里时,目录才会开始占用空间。

通常,操作系统会在一段时间后清理临时文件。

咳咳,老哥看起来说的没毛病,但是它这个实现确实没有考虑到大量的目录会撑爆 inode 的情况!

然后有个叫 RekaDowney 的老哥也在下面说到:

不论请求的 content-type 是不是 multipart/form-data 都会创建临时目录(这跟我前面说的一样,我这几天的几百万请求压根不是 multipart 相关的)。

然后 poutsma 老哥觉得没啥毛病:

你不用 multipart 创建这个目录也没影响,这目录里面又不会有文件,一个目录才占几 bytes,洒洒水啦。

额,把我洒哭了(3分钟线上服务不可用这是P几事故?)

不过后面马上 poutsma 老哥意识到这样好像也不好,因此他回复到:

对,他稍微妥协了下:我改我改,目录只在解析 multipart 数据的才会创建。

一天后 RekaDowney 老哥已经等不及了,他说:我已经算不清到底创建了多少个目录了,至少有 200 多w,然后我修改了源码,就调整了几行,只有在解析 multipart 的时候才创建,并且老哥还说要不要他提个 pr 贡献一波代码!

这位 RekaDowney 老哥的执行力还是强啊,自己动手丰衣足食,毕竟鬼知道这修复啥时候能发版。

好了,吃瓜暂时吃到这,最终我跟踪看到这个玩意是在 5.2.16 版本修复:

具体的替换方式是不无脑创建了,就创建一个随机的目录,然后保存这个引用,这样即使有很多 multipart 的请求,也不会创建很多目录了,整挺好:

然后我翻阅了一下 5.2.16 那个版本的 release,咳咳:

上面写着这玩意是 feat,不是 bug。

你们觉得呢?

程序员的倔强之,这不是bug:)

最终我的解决方案是:升级了 gateway 的版本到 5.2.16。

我是yes,从一点点我亿点点,我们下篇见~

 

点赞收藏
yes的练级攻略

公众号【yes的练级攻略】,专注分享后端技术

请先登录,查看5条精彩评论吧
快去登录吧,你将获得
  • 浏览更多精彩评论
  • 和开发者讨论交流,共同进步

为你推荐

JDBC PreparedStatement 字段值为null导致TBase带宽飙升的案例分析

JDBC PreparedStatement 字段值为null导致TBase带宽飙升的案例分析

随机一门技术分享之Netty

随机一门技术分享之Netty

MappedByteBuffer VS FileChannel:从内核层面对比两者的性能差异

MappedByteBuffer VS FileChannel:从内核层面对比两者的性能差异

6
5