作者 | 蚂蝗

背景

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. Linux修改bashrc

    .bashrc是一个隐藏的文件,要打开并修改该文件需要: (1) 查看:ll -a 找到文件 .bashrc: (2) 打开:vi .bashrc (或者 vim .bashrc) 打开文件: (3) ...

  2. PCIE基本知识

    转载:https://zhuanlan.zhihu.com/p/139656925 前言 之前主要都在做FPGA算法层面的东西,最近觉得对于接口方面的知识比较欠缺,打算以PCI-E为例来系统的学习一下 ...

  3. Fiddler抓包工具简介:(二)下载安装及配置证书和代理

    Fiddler下载安装及配置 一.安装过程: 下载官网:https://www.telerik.com/fiddler 安装过程:一路next即可 启动Fiddler:当你启动了Fiddler,程序将 ...

  4. RISCV 入门 (学习笔记)

    文章目录 1. risv 相关背景 1.1 arm 授权费 1.2 riscv 发展历史 1.3 riscv 风险 2. 指令集 2.1 可配置的通用寄存器组 2.2 规整的指令编码 2.3 简洁的存 ...

  5. macos proxy_bypass_macosx_sysconf exception

    macos, 在rpc调用request请求时,在proxy_bypass_macosx_sysconf 无法返回 解决方法: import requests session = requests.S ...

  6. 南京大学OS笔记(1)-应用眼中的操作系统

    南京大学OS笔记(1)-应用眼中的操作系统 早就想刷一刷南大JYY老师的os课.之前稍微看过几节,果然讲的风趣幽默,而且现场写代码展示水平确实很高,这次准备认真刷一刷然后好好记一下笔记.当然lab就不 ...

  7. ipython和pip,模块安装方法

    先下载 pip-.tar.gz 解压文件 cmd进入这个加压后的文件 执行 python setup.py install 然后配置环境变量 把 python 下的 Scripts 文件目录添加到 P ...

  8. SpringCloud升级之路2020.0.x版-39. 改造 resilience4j 粘合 WebClient

    本系列代码地址:https://github.com/JoJoTec/spring-cloud-parent 要想实现我们上一节中提到的: 需要在重试以及断路中加一些日志,便于日后的优化 需要定义重试 ...

  9. 躺平吧,平铺的窗口「GitHub 热点速览 v.21.47」

    作者:HelloGitHub-小鱼干 用 macOS 系统经常会遇到的一个问题便是多开窗口如何快速找寻的问题,本周特推项目 yabai 便是来解决这个问题的.直接把所有窗口平铺,是不是很"正 ...

  10. [hdu4388]Stone Game II

    不管是否使用技能,发现操作前后所有堆二进制中1的个数之和不变.那么对于一个堆其实可以等价转换为一个k个石子的堆(k为该数二进制的个数),然后就是个nim游戏. 1 #include<bits/s ...