作者 | 蚂蝗
背景
Erda 是集 DevOps、微服务治理、多云治理以及快数据管理等多功能的开源一站式企业数字化平台。其中,在 DevOps 模块中,不仅有 CI/CD、我的项目协同等性能,同时还反对自动化测试、测试用例治理等。
本文讲述的是一次源于 Erda 平台的测试用例导入产生的事变。一个只有 2M,看起来人畜有害的 excel 测试用例文件,把咱们的 qa 服务(DevOps 模块里的一个组件)间接干 OOM 了。
Tips:OOM 即 Out Of Memory,指程序应用内存过多,超过限度,被停止掉了。
排查过程
线索
关上上述 excel 文件,感觉一切正常,数据不多,格局也很标准。但依然对它存有疑虑,话不多说,咱们间接在开发环境进行一个重试,重启,但它仍旧稳固 OOM。我想了很久,逐步开始慌了,是不是我又做错了什么?不会吧不会吧,咱们不会真出大 Bug 了吧?!
进去吧,debug 大杀器 go pprof
,因为现场比拟好复现,所以咱们抉择了开始导入和导入中两个点的 inuse_space
内存来进行对照。通过工具,咱们能够看到:使得内存飙升的办法,一个是 xlsx 解析的开源库;一个是 golang 解析 xml 的规范库。
go tool pprof -inuse_space http://qa:3033/debug/pprof/heap > base.hepago tool pprof -inuse_space http://qa:3033/debug/pprof/heap > current.hepago tool --base base.hepa current.hepa
规范库和 star 4k+ 的开源库怎么可能会呈现这种问题呢?咱们首先狐疑是不是调用这个 xlsx 解析库的时候出了问题,然而这一共 14 行的代码,不管怎么看,感觉都不会出错。
func Decode(r io.Reader) ([][][]string, error) { tmpF, err := ioutil.TempFile("", "excel-") if err != nil { return nil, err } if _, err := io.Copy(tmpF, r); err != nil { return nil, err } data, err := xlsx.FileToSlice(tmpF.Name()) if err != nil { return nil, err } return data, nil}
理解“受害者-xlsx文件”
没方法,只能看看这个开源库的代码了。
func FileToSlice(path string, options ...FileOption) ([][][]string, error) { f, err := OpenFile(path, options...) if err != nil { return nil, err } return f.ToSlice()}// OpenFile will take the name of an XLSX file and returns a populated// xlsx.File struct for it. You may pass it zero, one or// many FileOption functions that affect the behaviour of the file.func OpenFile(fileName string, options ...FileOption) (file *File, err error) { var z *zip.ReadCloser wrap := func(err error) (*File, error) { return nil, fmt.Errorf("OpenFile: %w", err) } z, err = zip.OpenReader(fileName) if err != nil { return wrap(err) } file, err = ReadZip(z, options...) if err != nil { return wrap(err) } return file, nil}// ReadZip() takes a pointer to a zip.ReadCloser and returns a// xlsx.File struct populated with its contents. In most cases// ReadZip is not used directly, but is called internally by OpenFile.func ReadZip(f *zip.ReadCloser, options ...FileOption) (*File, error) { defer f.Close() file, err := ReadZipReader(&f.Reader, options...) if err != nil { return nil, fmt.Errorf("ReadZip: %w", err) } return file, nil}// ReadZipReader() can be used to read an XLSX in memory without// touching the filesystem.func ReadZipReader(r *zip.Reader, options ...FileOption) (*File, error) { ······ ······ for _, v = range r.File { _, name := filepath.Split(v.Name) switch name { case `sharedStrings.xml`: sharedStrings = v case `workbook.xml`: workbook = v case `workbook.xml.rels`: workbookRels = v case `styles.xml`: styles = v case `theme1.xml`: themeFile = v default: if len(v.Name) > 17 { if v.Name[0:13] == "xl/worksheets" || v.Name[0:13] == `xl\worksheets` { if v.Name[len(v.Name)-5:] == ".rels" { worksheetRels[v.Name[20:len(v.Name)-9]] = v } else { worksheets[v.Name[14:len(v.Name)-4]] = v } } } } } ······ ······ return file, nil}
看完之后,我忽然就 get 到了新的知识点:xlsx 文件原来是一个由很多不同的 xml 文件通过 zip 压缩起来的货色。解压后的它长这样:
依据代码的逻辑,worksheets/sheet1.xml 是单元格里次要的数据,外面的具体数据是指向 sharedStrings.xml 里的一个个索引;sharedStrings.xml 里存储的就是索引和理论文本内容的对应关系;theme/theme1.xml 和 styles.xml 顾名思义就是 excel 的格局和主题。
破案
因为 go pprof 指向的问题是 xml 解析消耗了大量的内存,所以咱们第一工夫狐疑:是不是主题或格局太简单导致解析慢,然而当咱们点进去发现内容啥的都十分失常。
直到点到 worksheets/sheet1.xml 后,我的 IDE 忽然就没了反馈,电脑风扇也响了起来,这时我的心田兴奋中夹杂着一点小不安,兴奋的是如同要破案了,不安的是它该不会要把我的电脑干爆吧。
千呼万唤始进去,在加载了十分久之后,咱们看到这个文件里有大量这样的字段,总共大概有 70w+ 行,他们的意思就是这些单元格的值都是 null,而有的值上面都会有 <c r="A1" s="13" t="s"><v>15</v></c>
这样的字段。这个 15 就是 sharedStrings.xml 里的索引了,咱们通过 excel 工具去定位空值也是定位到了大量的 null,而且都是表分外的、无意义的。到这里,咱们根本确认了问题就是用户的这个文件有大量的 null 导致 xml.Decode 解析大量无意义的 null 时耗费了很多内存和工夫。
解决方案
本着开源精力,咱们向社区提交了一个 issue,在和热心的负责人聊完之后,具体的探讨能够看下这个 issue,外面还有一些其余的解决办法。比方应用 Disk 存储,然而这个方法对咱们来说还是太慢;外面也有一个 RowLimit 的 option,这个也解决不了咱们的场景,因为咱们无奈确定须要限定成多少个行,而且列里的 null 还是会被解析。
issue: https://github.com/tealeg/xlsx/issues/700
所以咱们提供了一个 valueOnly 的 option,在这个 option 被选中时,咱们会在 xml.Decode 对其进行解析之前简化这个 xml 文件,也就是删掉这些 null 值的单元格,从而能够节俭大量的内存和解析工夫。然而这个选项也有肯定危险,因为 null 不肯定就是没意义,它也有可能代表是空的意思,但也合乎这是一个 option 的定义并且咱们对它加了危险提醒的正文。
相干 PR 链接:[_https://github.com/tealeg/xlsx/pull/701_]()
思考
不得不说,开源社区也太好玩了!对于本人应用的我的项目有什么想要的性能,想修的 Bug,齐全不必反馈完再等排期,本人间接写完提交 PR 就好了,现杀现吃。更重要的是能够和大家一起分享、欠缺本人的想法或创意,甚至还能够进步本人 2.5 级的英语水平。
最初十分欢送大家退出咱们的开源我的项目 Erda,一起打造这个企业级一站式 PaaS 平台,不论是好的创意、需要还是 Bug,都欢送提交 issue 来探讨嗷!(有 PR 当然就更好更欢送咯,Talk is cheap. Show me the code),这里的人个个都超有才,谈话又好听,我超爱这里的。
如果对于 Erda 我的项目你有其它想要理解的内容,欢送增加小助手微信(Erda202106)退出交换群!
欢送参加开源
Erda 作为开源的一站式云原生 PaaS 平台,具备 DevOps、微服务观测治理、多云治理以及快数据治理等平台级能力。点击下方链接即可参加开源,和泛滥开发者一起探讨、交换,共建开源社区。欢送大家关注、奉献代码和 Star!
- Erda Github 地址:https://github.com/erda-project/erda
- Erda Cloud 官网:https://www.erda.cloud/