作者 | 蚂蝗

背景

​

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!

​

紧张 + 刺激,源自一次 OOM 历险的更多相关文章

  1. 一个紧张刺激的聊天器,要不要进来看看(Python UDP网络模型)

    先来哔哔两句:(https://jq.qq.com/?_wv=1027&k=QgGWqAVF) 互联网的本质是什么?其实就是信息的交换.那么如何将自己的信息发送到其他人的电脑上呢?那就需要借助 ...

  2. OO电梯系列总结与反思

    目录 前言 HW5 度量分析 UML类图与协作图 bug分析 HW6 度量分析 UML类图与协作图 bug分析 HW7 度量分析 UML类图与协作图 bug分析 SOLID原则 感想 前言 紧张刺激的 ...

  3. 第一个独立开发的游戏 怪斯特:零 已经上线APP STORE!

    今天是个值得纪念的日子,而且是双喜临门 2年多来的摸爬滚打,终于有了回报 第一喜:自己独立开发的游戏 怪斯特:零 已经通过审核并上架APP STORE! 第二喜:迈入了自己期待2年之久的游戏行业,年后 ...

  4. 【BZOJ-2055】80人环游世界 上下界费用流 (无源无汇最小费用最大流)

    2055: 80人环游世界 Time Limit: 10 Sec  Memory Limit: 64 MBSubmit: 321  Solved: 201[Submit][Status][Discus ...

  5. NOIP201103瑞士轮【B002】

    [B002]瑞士轮[B级]出自附中OJ————————————————————————————————————————————————————————————————————————————————— ...

  6. 利用NABC模型进行竞争性需求分析

    利用NABC模型进行竞争性需求分析:   1>N(Need 需求)   越来越多的,各式各样的游戏层出不穷,大的小的中等的都已经琳琅满目了,用户貌似都看不过眼了.现在大游戏费时伤神,当然了得在有 ...

  7. NOIP2011 普及组 T3 洛谷P1309 瑞士轮

    今天题做太少,放道小题凑数233 题目背景 在双人对决的竞技性比赛,如乒乓球.羽毛球.国际象棋中,最常见的赛制是淘汰赛和循环赛.前者的特点是比赛场数少,每场都紧张刺激,但偶然性较高.后者的特点是较为公 ...

  8. NOIP201103瑞士轮

    试题描述 [背景]在双人对决的竞技性比赛,如乒乓球.羽毛球.国际象棋中,最常见的赛制是淘汰赛和循环赛.前者的特点是比赛场数少,每场都紧张刺激,但偶然性较高.后者的特点是较为公平,偶然性较低,但比赛过程 ...

  9. 《QQ欢乐斗地主》山寨版

    使用Cocos2d-x编写,模仿<QQ欢乐斗地主>的界面实现了一个具有简单AI的单机版斗地主游戏. 游戏的详细说明请查看游戏目录下的help.txt文件. 下载地址: http://dow ...

随机推荐

  1. TCP/IP简述

    一.TCP/IP简述 TCP/IP从字面异议看起来是指TCP和IP两种协议,实际上,它只是利用IP进行通信时必须用到的协议群的统称.具体的来说,IP或ICMP.TCP或UDP.Telnet或FTP.以 ...

  2. 奔跑吧linux-第三章实验

    基于树莓派+openeuler平台 实验 3-2:汇编语言练习--查找最大数 1.实验目的 通过本实验了解和熟悉 ARM64 汇编语言. 2.实验要求 使用 ARM64 汇编语言来实现如下功能:在给定 ...

  3. longest-consecutive-sequence leetcode C++

    Given an unsorted array of integers, find the length of the longest consecutive elements sequence. F ...

  4. Obsidian中使用Calendar插件快捷建立日记、周记

    Calendar插件 Calendar插件是我第一个安装使用的插件,插件可以帮助我们很便捷的记录每天的工作 插件效果图 插件下载 下载地址 插件安装 # Obsidian如何手动下载并安装插件-以看板 ...

  5. Openeuler安装完整man手册

    Openeuler安装完整man手册 ​ 在 Debian 和 Ubuntu 中安装了Shell 前端软件包管理器apt(Advanced Packaging Tool),可以通过如下方式安装. ​ ...

  6. Docker 18.03导入导出

    docker中分容器和镜像,简单可以理解为容器是运行中的实例,镜像是运行实例所需的静态文件. 导入导出既可以对容器做操作,也可以对镜像做操作.区别在于镜像可以随时导出,容器必须要停止之后才可以导出,否 ...

  7. APP自动化环境搭建之appium工具介绍(二)

    1.下载解压android-sdk-windows-appium //配置环境: ANDROID_HOME:D:\android-sdk-windows-appium path:...;%ANDROI ...

  8. 讲分布式唯一id,这篇文章很实在

    分布式唯一ID介绍 分布式系统全局唯一的 id 是所有系统都会遇到的场景,往往会被用在搜索,存储方面,用于作为唯一的标识或者排序,比如全局唯一的订单号,优惠券的券码等,如果出现两个相同的订单号,对于用 ...

  9. S2-002漏洞分析

    漏洞概述 Struts2-002是一个 XSS 漏洞,该漏洞发生在 <s:url> 和 <s:a>标签中,未对标签内字符进行转义,当标签的属性 includeParams=al ...

  10. LiteFlow 2.6.4版本发行注记,里程碑版本!

    一 这个版本做的很折腾.期间几个issue推翻重做了好几次. 但我最终还是带来了LiteFlow 2.6.4这个重要版本. 虽然版本是小版本号升级,但是带来的更新可一点也不少.并完全向下兼容. 如果你 ...