type
status
date
slug
summary
tags
category
icon
password
AI summary
背景
近期产品提出了一个要支持内嵌图片的导入需求:创建教师需要上传身份证正反面照片,所以通过excel批量导入教师时也需要支持从excel里读取出对应图片。流程大致为:
- 把excel里的图片解析出来并上传到oss
- 导入记录并把图片列替换为对应的oss链接
具体的开发同学也实现了相应的需求,但是在生产环境运行的过程中,由于打开的文件描述符数量过高产生了告警。我们先铺垫一些关于内嵌图片的基础知识,再来分析出现问题的原因。
调研
经过简单的调研和实验,我们发现原生excel应该是不支持在单元格里内嵌图片的,它的图片都是浮动的,和单元格本身并没有什么关系,可以理解成两个图层,拿到对应的图片你都很难对应上是哪一行,可能只能通过计算坐标来判断。但是咱们的国货之光WPS竟然支持内嵌图片。而产品也是用的WPS,才提出的这个需求。
那这个需求我们就只需要考虑WPS内嵌图片即可。我们观察WPS内嵌图片的单元格,发现它其实是通过一个函数来实现的:
而同样的文件在
MicroSoft Excel
里打开,内嵌图片列无法正常显示,取而代之的是一串字符串:似乎这个函数是WPS自定义的,并且通过第一个参数关联到了对应图片。
我们知道,xlsx本质上其实是个zip压缩包,我们把含有WPS内嵌图片的excel的后缀重命名为zip,然后解压出来看看里面的东西。当然你也可以直接使用vim命令观察。

截图中标红的三处就是内嵌图片实现的关键:
xl/media目录
media
目录下包含了所有的内嵌图片xl/_rels/cellimages.xml.rels
xl/_rels/cellimages.xml.rels
里记录了图片地址和RelationshipId的关系。xl/cellimages.xml
xl/cellimages.xml
里记录了RelationshipId和PicId之间的关系PicId就是单元格里内嵌图片自定义函数的参数,通过PicId找到对应的图片地址(PicId -> RelationshipId -> 图片地址)
调研过程参考了这篇文章,并且也参考了大量的代码,然而原文中的代码对于资源释放存在问题,这也是导致文件描述符过高的原因。
问题分析
既然是文件描述符数量过高,那我们查看一下对应进程打开的文件描述符先,发现确实有不少未关闭的文件描述符。里面多数都是xlsx格式的文件,并且每个文件都被打开了多次,但是被打开的次数不完全一样。另外对应的文件都已经被deleted了。看起来就是虽然删除了文件,但是有引用没有释放。我们以其中一个xlsx文件为例看一下:
而这个导入任务的执行过程中,由于导入的文件是在OSS上的,我们会先把OSS上的文件下载到本地,生成临时的xlsx文件。从文件名以及文件描述符的创建时间都对得上:
不过有2个文件描述符有点奇怪,创建时间分别是08:18和13:16,而其余都是在15:51,暂时还不清楚原因,不过这不是本篇文章的重点。
于是我去review了一下代码,初步找到了下面这段问题代码:
这里存在两个问题:
- 资源泄露:对于ZipFile和ZipInputStream都没有关闭,造成了资源泄露
- 重复打开文件:ZipFile和ZipInputStream都是通过同一个文件创建的,这是不必要的。ZipFile本身就可以用来获取ZipEntry的输入流,因此不需要同时使用ZipInputStream。
这也是造成此次文件描述符数量过高的原因,修复方案就不过多赘述了。因为Review过程中我还发现了一些其他的问题,所以这里并不打算基于原来的代码进行bugfix,准备重构一版本来彻底解决这些问题。
重构
除了上面描述的两个问题之外,原来的代码还存在如下几个问题:
- 我们团队解析Excel都使用EasyExcel,而这个需求的代码多数是直接复制过来的,所以也用了原文里的原生POI包解析的Excel,一是代码不够简洁可读性不低,二也不符合我们的规范。而了解了背后的原理之后,使用EasyExcel来处理内嵌图片也非常easy
- 尽量减少打开Zip包的次数,而不是每获取一个文件,都需要打开一次Zip包,并且尽量可以一次性获取更多的资源
- 尽量精简代码,增加可读性
重构之后的逻辑:
- 新增
@WpsImage
注解,用于在实体类上标注内嵌图片列
- 读取目标Excel里的两个xml文件,构造出id->图片地址的映射
- 使用EasyExcel读取目标Excel列
- 从
@WpsImage
注解的列上读取内嵌图片的文本信息(也就是=@_xlfn.DISPIMG("ID_49C5BB73DFE221E0BE39AA65A9284EAA",1)
)
- 解析出图片id,再拿到图片的具体地址
- 把图片上传到OSS,拿到URL
- 把
@WpsImage
注解的列替换成URL
- 进行后续的导入流程
重构后的代码如下,基类是我们原本对基于Excel导入的封装,这个子类只是前置对于读取到的数据做了预处理,然后把处理后的结果交给原来的基类处理即可。
问题记录
在测试过程中,有几个问题让我比较疑惑,并且最终也没有找到令我信服的资料,特此记录一下。
ZipFile 文件描述符可复用?
这个是测试过程中发现的,第一次
new ZipFile(fileName)
会打开一个文件描述符,但是同样的文件再次new
的时候,从系统层面观察,并没有发现创建新的文件描述符。而FileInputStream,每new一次,都会打开一个文件描述符
ZipFile 文件描述符会自动回收?
这个也是在测试过程中发现的,创建ZipFile之后,不主动close,但是过一段时间从系统层面观察,竟然发现文件描述符不存在了
基于上面这两点,资源泄露时期任务执行完稳定之后的文件描述符的数量刚好等于zip包里图片的数量 + 2(2个xml文件)
容器内的文件描述符创建时间
由于我们的应用都是跑在Docker里的,我进入容器内查看,发现文件描述符的创建时间貌似和我进入容器的时间有关:
参考
- Author:黑微狗
- URL:https://blog.hwgzhu.com/article/import-excel-with-images
- Copyright:All articles in this blog, except for special statements, adopt BY-NC-SA agreement. Please indicate the source!