共计 4184 个字符,预计需要花费 11 分钟才能阅读完成。
作者 | 蚂蝗
背景
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.hepa
go tool pprof -inuse_space http://qa:3033/debug/pprof/heap > current.hepa
go 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/