一个嘴贱引发的内存异常bug


记得我在上一篇文章《 一个耗时4小时的内存泄漏问题链接中的封面说过这样的一句话:

当出现bug时不要疑惑不要心急
有bug的日子里需要镇静
相信吧
下一个bug很快来临

让你嘴贱!下一个bug真的很快就来临了。

上一篇文章中讲到的bug出自笔者正在进行的一个并行化项目,由于该项目改动较大,因此代码分多次提交,上一篇文章提到的bug就出在了第一次提交当中。最近该项目代码已全部开发完成,至于最终效果其实笔者自己也是没底的,尽管在线下进行了充分的测试,但是我知道这种涉及到并行化多线程的改动在几台机器上测试可能都不会有问题,因为机器较少可能都不会触发问题,真正接受线上检验时就不一定了。
但是项目已经到deadline了,代码开发完成也经过了测试,管不了那么多了,上线吧。

项目上线

怀着忐忑的心情开始上线了,毕竟之前看到过太多类似的问题,而且涉及到多线程的bug很难排查,这次改动涉及到并行的代码太多了,尽管在开发过程中仔细检查了可能存在的线程安全问题的代码,但毕竟人不是机器不可能做到滴水不漏,不管怎样,上线有问题大不了就回滚,也没什么大不了的。
结果代码一次上线成功,没有任何问题! 没有影响线上业务,也没有core dump,一切顺利。
这。。不科学啊,墨菲定律没有生效啊,哈哈,我的代码写的没有这么好吧,一瞬间对自己的自信来到了新的一个高峰,天才啊。

冷静之后觉得这也太顺利了吧,期待中的各种问题都没有出现,这反而让自己犯起了嘀咕,这个项目不会就这样完成了吧,没有惊天动地的bug反而觉得稍显平淡了一些,就好比你满心期待的去看一部预期很棒的电影但是剧情无比平庸没有各种跌宕起伏没有奇妙反转,总之如鸡肋一般让人食之无味。
我一度怀疑自己是不是平时被bug虐出精神病了,没有bug不是好事吗,竟然还有人期待自己代码出现bug的,这不是没事儿找抽的沙雕么。
要的就是没有bug。

期待的剧情出现了

第二天笔者又像往常一样例行检查线上机器,检查到昨天上线的机器时,那种所谓奇妙反转的剧情出现了,包含笔者代码的机器内存使用情况是这样的:

在这里插入图片描述

程序在运行过程中会突然大量申请内存然后就挂了,接着进程被重启,运行一段时间后又突然大量申请内存然后接着挂,但是也没有core dump,显然笔者的进程是被什么东西给kill掉了,显然我的代码有bug才导致进程被kill掉了,而是还是一个没有core dump的bug,So excited!!!

笔者一瞬间释然了,这才是我想要的啊,原来墨菲定律从来没有失效过,这太科学了,我的代码终于出现期待中的bug了,一边尽最大努力写出没有bug的代码一边又期待自己的代码出现bug,原来被bug虐出的精神病进一步加重,这时已经升级为精神分裂症了,就是这么魔幻,就是这么沙雕。

问题分析

没有办法,本来我还想这个项目完成后可以摸几天鱼,现在看来是不可能了,没有薅到公司的羊毛现在极有可能要被公司反薅被迫加班修复bug了,到底是出现什么问题了呢?
进程挂掉后没有core dump这下问题就极难定位了,又仔细分析了一遍上图,进程在一瞬间会突然大量申请内存然后就被kill掉了,那么谁能因为大量申请内存而kill掉一个进程呢?想到了吧,很有可能就是oom killer了。
想到这里赶紧登陆一台问题机器,输入以下命令,该命令可以告诉你哪个进程被oom killer给终止掉了:

$ dmesg | egrep -i 'killed process'

[30212270.539366] Killed process 516918 (my_server) total-vm:229555536kB, anon-rss:66166148kB, file-rss:0kB, shmem-rss:0kB

果然不出我所尿,服务器进程就是被oom killer给挂掉了。
其中:

  • total-vm告诉你进程的虚拟内存大小是多少,注意是虚拟内存不是物理内存,不是所有的虚拟内存都会映射到物理内存
  • anon-rss告诉你映射到物理内存的部分有多少,从上述结果中可以看到该进程占据的物理内存大约有63G,而该机器的内存上限就64G,难怪被oom killer给终止掉了
  • file-rss告诉你进程虚拟内存映射到文件的部分有多少

有的同学可能会有疑惑,什么是oom killer?

Oom Killer

Oom Killer,也就是Out Of Memory Killer,从字面意思上我们就知道其作用了。
oom killer是Linux的一个内核进程,当系统中极度缺乏内存时oom killer就是开始运行,那么什么时候系统开始极度缺乏内存呢?就像我的进程那样,本来系统就64G内存,问题进程自己就占用了63G,这时oom killer开始运行并巡视各个进程,选择一个进程终止掉以释放其占用的内存。

那么oom killer是如何来选择一个被终止掉的进程呢?原来oom killer巡视一遍所有进程后就给每个进程分配一个分数,也就是"badness score",坏蛋分,分数最高的那个将被kill掉,打分的原则简单讲就是占用内存越高的那个进程得分就越高,很显然我们的进程就是得分最高的那个,然后就被kill掉了,没有留下任何线索。

问题排查

这下我也不知道该从哪里着手排查问题了,排查oom killer问题没有什么章法可寻,也没有core dump无法定位问题,大海捞针,谁让你开始期待有bug了。
现在好消息就是问题一定出在了我的改动上,坏消息就是我的改动几乎涉及到了整个工程,酸爽吧,少年。
没有办法,bug是一定要修复的,不记得是谁说的了,所有的bug最终都是能修复,只不过就是时间问题而已。

在这里再次介绍一下我的改动,实际上比较简单,假设main函数中需要调用4个函数,func1、func2、func3、func4:

void main() {
	func1();
	func2();
	func3();
	func4();
}

我们可以看到这四个函数时依次串行执行的,但是从我们的业务角度上看实际上func3、func4和func1、func2没有依赖关系,这样func1、func2和func3、func4可以分别放到两个线程中并行执行从而加快速度,就像下面这样:

void thread1() {
	func1();
	func2();	
}

void thread2() {
	func3();
	func4();	
}

void main() {
	create_thread(thread1);
	create_thread(thread2);
}

这就是我一向很头疼的业务代码的并行化改造,极其容易出问题,之前的《一个耗时4小时的内存泄漏问题》链接也是在代码并行化改造中出现的,尽管在写代码的过程中非常仔细的进行了检查而且也经过了线下测试,但是你依然不知道代码真正上线后会出现什么样稀奇古怪的问题。

这个问题出现的背景就是这样了,很显然问题就是出在了业务代码从之前的串行变为了现在的并行。但是这基本上就是一句正确的废话,由于涉及到的代码很多,直接一行行读代码排查问题无疑大海捞针,那么如何在大量的改动中排查问题呢?

如何在大量的改动中排查问题

迫不得已,只能拿出我的杀手锏了,我请出了上学那会儿应对选择题的绝技,是的,你没有猜错,那就是伟大的排除法,排除法的实现原理应用到了马列哲学当中矛盾的辩证思想,将选项带入原题得出矛盾的结论即可排除掉该选项,正是凭借这一方法我在选择题上从来都是所向披靡,搞得我甚至一度幻想如果高考只考选择的话那么清华北大简直不要太容易,哈哈,扯远了,回到我们的问题中来,不过你应该知道这也是没有办法的办法,如果我能知道答案就会直接选择而不会一个个排除了,这个方法相对低效,但是,真的管用。

于是我就精心设计了几组实验,就和神农尝百草一样,每一组实验我都只将一小部分业务代码并行处理,其它的依然串行执行,设计完实验后我就下班了,哈哈,是的,你没有看错,因为我不认为像格物致知盯着竹子七天七夜一样死磕代码代码就会自己开口告诉我问题出在哪里,况且这个问题的复现也需要很长时间,当你想不出一个问题的解时出去换换脑子是很好的方法,下班就是换换脑子最好的方法,这个理由是如此的充分以至于出现bug后反而让我下班更早了,一度让同事觉得我已经破罐子破摔摔或者早就手里一堆offer了。

经过了一晚上的运行,第二题一早检查发现有一组实验bug复现了,这个结果让我振奋不已,因为这极大的缩小了排查问题的范围,我仿佛已经听到了bug的求饶声,经过我无比细致的检查果然发现了问题,这再一次验证了之前的那句话:

多线程问题的根源在于共享资源

这个问题是这样的,为加快内存分配速度,我们维护了一个内存池,这个内存池是一个共享资源,也就是所有的线程都可以向其申请内存,这个内存池提供了一个内存申请接口:

void* GetMemory(int size);

这个函数的实现是加锁的,也就是说这个函数是线程安全的,多个线程调用这个函数申请内存不会出现问题,但可恶的是这个内存池的实现还提供了另外一个内存分配函数也叫做GetMemory:

void* GetMemory();

区别仅在于第一个函数你可以申请一批内存,后一个函数只能申请特定数量的内存,因为第一个函数的存在让我一度认为第二个函数也是线程安全的,在经过仔细的阅读代码后发现第二个函数竟然不是线程安全,第二个函数的实现中根本没有线程安全相关的代码,之前只所以没有问题是因为调用该函数的业务是串行的,现在这个函数被放到多个线程中并行执行从而突然大量申请内存导致服务进程被oom killer终结掉了,也就是服务器内存使用率出现了开头图中的现象。

至此,内存的bug已经找到,此时距离bug发现已经过去了一天半,

总结

代码的并行化改造不是一件容易的事情,在改造过程中一定要多注意线程间使用的共享变量,这是线程安全问题出现的根源,当然发现此类问题后第一时间需要排除的就是代码中的共享资源,同时如果你要进行多线程编程的话那么在使用锁之前一定要问问自己真的有必要使用共享资源吗,这个非常重要,当然,最最重要的是,不要嘴贱。

这个项目中还涉及到其它有意思的问题,有机会也会给大家介绍一下。

更多计算机内功文章,欢迎关注微信公共账号:码农的荒岛求生

在这里插入图片描述
彻底理解操作系统系列文章
1,什么程序?
2,进程?程序?傻傻分不清
3,程序员应如何理解内存:上篇
4,程序员应如何理解内存:下篇
 

计算机内功决定程序员职业生涯高度

发布了38 篇原创文章 · 获赞 30 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/github_37382319/article/details/102971297
今日推荐