在一个风和日丽的下午,刚打算饮茶,线上就开始报警了,一看情况网关报 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 的玩意去清理长时间无用的文件,一般会定时去清理。
而且重启系统的话,这里面的文件也会被清空。
至此,产生问题的原因应该非常清晰了。那如何解决?
解决
-
利用 tmpwatch 勤快点去清理,比如近几个小时没用的就直接干了。
-
不用 HttpMessageReader
-
定时重启
-
修改源码
-
看看后续版本
第一点不太好,因为有几率误删有用的文件。第二点理论上用了 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。