记一次内存爆炸的经历
2018-02-10 13:27
190 查看
本文原载于https://base.admirable.one/t/topic/149,作者亦是本人,特此申明。
最近有做一个程序,内容其实很简单:调用几百万次一个api,然后把结果保存到 MongoDB 数据库。一开始我没有想到用 coroutine 等高级方法,就简单采用了“生产者消费者”的模式:第一组线程负责分配任务,第二组线程负责从api获取结果,然后保存到一个队列,第三组线程负责从队列中拿任务并保存到数据库。程序很快就写出来了,在本机上跑也没有出现问题,但是后来部署到正式机器上后就出现了问题:程序启动后,内存一直拼命增长,直到把内存占满,然后被内核 OOM。一开始,出于不自信,我想的是会不会是我的程序哪个地方内存泄漏了。但是经过资料的查找,发现Python的内存是实时回收的,而且我的程序也着实看不出哪里可能内存泄露了。后来我把程序 log level 开到最高,并且不同线程的输出使用不同的颜色,才发现问题在于:生产环境中配置文件中,生产者和消费者的线程数被改得面目全非——原来是15/20(其实这个数字是我随便写的),后来被别人改成了120/240。其实我一开始就发现了这个数字被改了,只是我认为这个数字应该是对方经过调试之后试出来的,而不是拍脑门想出来的,所以没有考虑是这个上面的问题。然而实际上,这么大的线程数就是拍脑门想出来的… 而我在本机上一直使用的自己的 development 配置,没有开这么高的线程,所以一直没有办法复现这个问题。其实仔细思考一下就会发现,Python 的多线程受制于 GIL 这把大锁,线程越多,上下文切换花的时间反倒是越多。因此即使不出现爆内存的问题,设置这么大的线程数在执行效率上也是有问题的(所有线程都是同一类型的IO密集型除外)。再说说为什么线程数被改成这样之后就导致爆内存:初期我自己的假设是调取一次 API 的时间会比存一次数据库的时间慢很多,因此生产者应该比消费者线程数量多一些。然而这个假设没有任何实验基础,完全是自己的猜测。事实上,因为api服务器在本机,所以调用api反而比存数据库更快。因此『待存数据库』队列里的数据越来越多,逐渐占满内存,然后被系统 OOM。不仅生产者生产快,更为可怕的是,而且由于生产者和消费者在一个进程内,因此无论是生产者还是消费者,每个thread被execute的时间是大体相等的(我并不清楚 Python 是怎么选择线程执行的,假设是时间片轮转吧),因此 消费者线程/生产者线程 比例过低会导致消费者的执行时间不够,从而无法消耗生产者的产品。通过这件事,我得出了以下教训:
队列要设置最大数量限制。生产者/消费者的比例实际上是很难准确估计出来的,设置最大数量限制,反馈调节生产者速度才不会出现队列太大爆内存的问题
Python的多线程是残废。不妨使用 multiprocessing / concurrent.future
开始学习协程吧!
最近有做一个程序,内容其实很简单:调用几百万次一个api,然后把结果保存到 MongoDB 数据库。一开始我没有想到用 coroutine 等高级方法,就简单采用了“生产者消费者”的模式:第一组线程负责分配任务,第二组线程负责从api获取结果,然后保存到一个队列,第三组线程负责从队列中拿任务并保存到数据库。程序很快就写出来了,在本机上跑也没有出现问题,但是后来部署到正式机器上后就出现了问题:程序启动后,内存一直拼命增长,直到把内存占满,然后被内核 OOM。一开始,出于不自信,我想的是会不会是我的程序哪个地方内存泄漏了。但是经过资料的查找,发现Python的内存是实时回收的,而且我的程序也着实看不出哪里可能内存泄露了。后来我把程序 log level 开到最高,并且不同线程的输出使用不同的颜色,才发现问题在于:生产环境中配置文件中,生产者和消费者的线程数被改得面目全非——原来是15/20(其实这个数字是我随便写的),后来被别人改成了120/240。其实我一开始就发现了这个数字被改了,只是我认为这个数字应该是对方经过调试之后试出来的,而不是拍脑门想出来的,所以没有考虑是这个上面的问题。然而实际上,这么大的线程数就是拍脑门想出来的… 而我在本机上一直使用的自己的 development 配置,没有开这么高的线程,所以一直没有办法复现这个问题。其实仔细思考一下就会发现,Python 的多线程受制于 GIL 这把大锁,线程越多,上下文切换花的时间反倒是越多。因此即使不出现爆内存的问题,设置这么大的线程数在执行效率上也是有问题的(所有线程都是同一类型的IO密集型除外)。再说说为什么线程数被改成这样之后就导致爆内存:初期我自己的假设是调取一次 API 的时间会比存一次数据库的时间慢很多,因此生产者应该比消费者线程数量多一些。然而这个假设没有任何实验基础,完全是自己的猜测。事实上,因为api服务器在本机,所以调用api反而比存数据库更快。因此『待存数据库』队列里的数据越来越多,逐渐占满内存,然后被系统 OOM。不仅生产者生产快,更为可怕的是,而且由于生产者和消费者在一个进程内,因此无论是生产者还是消费者,每个thread被execute的时间是大体相等的(我并不清楚 Python 是怎么选择线程执行的,假设是时间片轮转吧),因此 消费者线程/生产者线程 比例过低会导致消费者的执行时间不够,从而无法消耗生产者的产品。通过这件事,我得出了以下教训:
队列要设置最大数量限制。生产者/消费者的比例实际上是很难准确估计出来的,设置最大数量限制,反馈调节生产者速度才不会出现队列太大爆内存的问题
Python的多线程是残废。不妨使用 multiprocessing / concurrent.future
开始学习协程吧!
相关文章推荐
- 昨晚经历了一次严重的高速公路惊险事故!沃尔沃卧铺车左后轮两个轮胎同时爆炸!
- 端午在即,难忘的经历——记一次php单次任务处理对内存超大需求的解决
- 一次难忘的JVM内存调试经历(CUDA+Opencv+JNI+Storm)
- 一次bug死磕经历之Hbase堆内存小导致regionserver频繁挂掉
- 一次bug死磕经历之Hbase堆内存小导致regionserver频繁挂掉
- 一次bug死磕经历之Hbase堆内存小导致regionserver频繁挂掉
- 一次bug死磕经历之Hbase堆内存小导致regionserver频繁挂掉
- 一次bug死磕经历之Hbase堆内存小导致regionserver频繁挂掉
- 一次内存不能为read/write的bug解决经历
- [转]一次Ajax查错的经历
- 纪念一次Ubuntu8.04下的GTK源码安装经历
- 一次死锁追踪经历
- 不断优化配置,逐步提高性能——我的一次性能测试经历
- 记一次JAVA面试经历 ~
- printf大坑等着很多人------一次core dump经历及定位过程(printf打印C++ string的时候忘了.c_st()转化)
- after Normal block(#47) at 0x001D3908 错误的一次解决经历
- $.getScript(url, callback),callback不执行,一次查错经历
- 每个程序员都应该经历一次软考
- 一次Windows CE下调试内存泄露的经历
- 由一次坎坷的缴费经历谈互联网的未来