Perl线程缓慢地消耗内存

9
我正在运行一个带有10个线程的Perl服务器。除非程序退出,否则它们永远不会被销毁,但我想让它尽可能长时间运行,所以这对我来说是一个问题。这些线程多次处理一个简单的任务。当我启动服务器并启动所有线程后,我发现还剩下288.30 MB的空闲内存。经过几次迭代后,每个线程报告285.96 MB的可用内存。那还不错...也许在这些迭代过程中只是分配了一些堆栈空间之类的东西。但是15分钟后,可用内存下降到了248.24 MB!我的内存怎么了?有趣的是,它确实达到了平台期。它继续缓慢消耗,但不像开始那样快。我以为是我的问题,所以我尝试仔细检查所有变量的范围,甚至在线程循环结束时取消定义它们。
我在每个线程迭代后打印出可用空间,以便观察其缓慢下降。现在有趣的是,它不是每次都会减少。有时在迭代后,可用内存保持不变。
我正在使用从源代码构建的Perl 5.8.8,在Linux 2.6上运行。
有人有任何想法或建议吗,甚至是可能导致这种情况的建议?我考虑升级我的Perl到一个更高版本,以排除Perl核心内存泄漏的可能性。
更新:这可能是线程堆栈大小问题吗?我是否分配了比所需更多的堆栈内存。当我创建线程时,我没有改变设置。我应该吗?线程文档说默认值通常为16MB,具体取决于系统。16x10个线程=160MB->这可能是罪魁祸首。你怎么看?
更新:我构建并安装了Perl 5.12.1,并重新构建了模块和其他一切。现在已经运行脚本约一个小时了,这是我的发现。内存使用现在可管理,但不是理想的。
  • 刚开始生成线程后,我的线程似乎要少一些。从分配给我的10个线程的大约60-66MB下降到了45-50MB左右。
  • 经过一些迭代,它们的使用量总共增加了3MB(与之前大致相同)。
  • 到这个点为止,这正是我预期的。所有的内存都用在了生成线程上,然后只有一点点变量用于我的线程。这是我不喜欢的部分。运行了大约10分钟后,我失去了额外的65MB!为什么会这样?如果已经迭代了几次,只用了3MB,为什么还要继续分配?
  • 它已经运行了一个半小时,它们不再使用额外的65MB了,而是额外的84MB!
  • 它慢慢地占用更多的内存,但奇怪的是每次迭代时可用内存的数量并没有减少。我在每次迭代之前和之后打印出可用内存,并且它会在一段时间内保持不变或者在某个数字周围波动,然后突然变化5-10MB。我不能让它运行超过一两天,因为它开始接近我可用内存的80/90%。

还有其他的想法吗?我已经取消了所有变量的定义。

更新: 我真的希望最后能重新编译Perl与glibc,因为我发现在某些Linux版本上它会segfault。所以自从我上次发布以来,我进一步探索了哈希表中的循环可能性。没有发现什么。所以我花了最近几天时间分析我的子例程并缓存任何在另一个迭代中使用的东西。每次都会创建很多新的东西,即使我明确地取消定义所有东西,Perl也不会清理所有东西。所以如果它不合作,我就不会摧毁它。看看缓存我的对象是否有所帮助。稍后会发布内存使用情况统计数据。

更新: 嗯,非常奇怪。即使缓存我的数据以便以后重用,内存增长速度也大致相同。现在开始较高,因为我正在缓存,但随后持续上升,尽管它主要使用我的缓存对象。这很令人困惑。猜想是时候尝试glibc了……否则这只是选择Perl的一个缺点,将不得不每隔几天重启服务器。

更新: 没有缓存,没有glibc,再次尝试。一段时间内运行良好,几个小时后开始增长。只是想让你看到一个图表。
http://tinypic.com/r/311nc08/3
http://i32.tinypic.com/311nc08.jpg

更新: 这里是一份记录了每个线程在大约一分钟内使用前后的空闲内存的日志摘录。也许这可以帮助某些人更好地理解问题。它似乎稳定了一段时间,然后偶尔会像这样吞噬更多的内存。在这里,我失去了近40 MB!

[9:8:30, Fri Jul 23, 2010] [0] Memory usage at end thread 1: 253.812736MB (obj cache: 136)
[9:8:30, Fri Jul 23, 2010] [0] Memory usage at idle thread 1: 253.812736MB (obj cache: 136)
[9:8:34, Fri Jul 23, 2010] [204] Sending data to thread
[9:8:34, Fri Jul 23, 2010] [0] 3 - Creating a new obj
[9:8:34, Fri Jul 23, 2010] [206] Sending data to thread
[9:8:34, Fri Jul 23, 2010] [0] 4 - Creating a new obj
[9:8:35, Fri Jul 23, 2010] [0] Memory usage at end thread 3: 253.812736MB (obj cache: 136)
[9:8:35, Fri Jul 23, 2010] [0] Memory usage at idle thread 3: 253.812736MB (obj cache: 136)
[9:8:35, Fri Jul 23, 2010] [0] Memory usage at end thread 4: 253.812736MB (obj cache: 136)
[9:8:35, Fri Jul 23, 2010] [0] Memory usage at idle thread 4: 253.812736MB (obj cache: 136)
[9:8:41, Fri Jul 23, 2010] [225] Sending data to thread
[9:8:41, Fri Jul 23, 2010] [0] 2 - Creating a new obj
[9:8:42, Fri Jul 23, 2010] [0] Memory usage at end thread 2: 253.681664MB (obj cache: 136)
[9:8:42, Fri Jul 23, 2010] [0] Memory usage at idle thread 2: 253.681664MB (obj cache: 136)
[9:8:47, Fri Jul 23, 2010] [243] Sending data to thread
[9:8:47, Fri Jul 23, 2010] [0] 1 - Creating a new obj
[9:8:48, Fri Jul 23, 2010] [0] Memory usage at end thread 1: 253.935616MB (obj cache: 136)
[9:8:48, Fri Jul 23, 2010] [0] Memory usage at idle thread 1: 253.935616MB (obj cache: 136)
[9:9:1, Fri Jul 23, 2010] [277] Sending data to thread
[9:9:1, Fri Jul 23, 2010] [0] 3 - Creating a new obj
[9:9:2, Fri Jul 23, 2010] [280] Sending data to thread
[9:9:2, Fri Jul 23, 2010] [0] 4 - Creating a new obj
[9:9:2, Fri Jul 23, 2010] [0] Memory usage at end thread 3: 253.935616MB (obj cache: 136)
[9:9:2, Fri Jul 23, 2010] [0] Memory usage at idle thread 3: 253.935616MB (obj cache: 136)
[9:9:3, Fri Jul 23, 2010] [283] Sending data to thread
[9:9:3, Fri Jul 23, 2010] [0] 2 - Creating a new obj
[9:9:4, Fri Jul 23, 2010] [284] Sending data to thread
[9:9:4, Fri Jul 23, 2010] [0] 1 - Creating a new obj
[9:9:4, Fri Jul 23, 2010] [0] Memory usage at end thread 2: 253.935616MB (obj cache: 136)
[9:9:4, Fri Jul 23, 2010] [0] Memory usage at idle thread 2: 253.935616MB (obj cache: 136)
[9:9:5, Fri Jul 23, 2010] [287] Sending data to thread
[9:9:5, Fri Jul 23, 2010] [0] 3 - Creating a new obj
[9:9:5, Fri Jul 23, 2010] [0] Memory usage at end thread 4: 253.93152MB (obj cache: 136)
[9:9:5, Fri Jul 23, 2010] [0] Memory usage at idle thread 4: 253.93152MB (obj cache: 136)
[9:9:6, Fri Jul 23, 2010] [290] Sending data to thread
[9:9:6, Fri Jul 23, 2010] [0] 2 - Creating a new obj
[9:9:7, Fri Jul 23, 2010] [0] Memory usage at end thread 3: 253.804544MB (obj cache: 136)
[9:9:7, Fri Jul 23, 2010] [0] Memory usage at idle thread 3: 253.804544MB (obj cache: 136)
[9:9:7, Fri Jul 23, 2010] [0] Memory usage at end thread 1: 253.804544MB (obj cache: 136)
[9:9:7, Fri Jul 23, 2010] [0] Memory usage at idle thread 1: 253.804544MB (obj cache: 136)
[9:9:9, Fri Jul 23, 2010] [0] 4 - Creating a new obj
[9:9:9, Fri Jul 23, 2010] [301] Sending data to thread
[9:9:9, Fri Jul 23, 2010] [0] 3 - Creating a new obj
[9:9:9, Fri Jul 23, 2010] [302] Sending data to thread
[9:9:9, Fri Jul 23, 2010] [0] 1 - Creating a new obj
[9:9:10, Fri Jul 23, 2010] [0] 3 - Creating a new obj
[9:9:11, Fri Jul 23, 2010] [0] 3 - Creating a new obj
[9:9:11, Fri Jul 23, 2010] [0] Memory usage at end thread 4: 253.93152MB (obj cache: 136)
[9:9:11, Fri Jul 23, 2010] [0] Memory usage at idle thread 4: 253.93152MB (obj cache: 136)
[9:9:12, Fri Jul 23, 2010] [308] Sending data to thread
[9:9:12, Fri Jul 23, 2010] [0] 4 - Creating a new obj
[9:9:13, Fri Jul 23, 2010] [0] Memory usage at end thread 1: 253.804544MB (obj cache: 136)
[9:9:13, Fri Jul 23, 2010] [0] Memory usage at idle thread 1: 253.804544MB (obj cache: 136)
[9:9:14, Fri Jul 23, 2010] [0] Memory usage at end thread 4: 253.804544MB (obj cache: 136)
[9:9:14, Fri Jul 23, 2010] [0] Memory usage at idle thread 4: 253.804544MB (obj cache: 136)
[9:9:14, Fri Jul 23, 2010] [0] Memory usage at end thread 3: 253.93152MB (obj cache: 136)
[9:9:14, Fri Jul 23, 2010] [0] Memory usage at idle thread 3: 253.93152MB (obj cache: 136)
[9:9:15, Fri Jul 23, 2010] [313] Sending data to thread
[9:9:15, Fri Jul 23, 2010] [0] 1 - Creating a new obj
[9:9:16, Fri Jul 23, 2010] [0] Memory usage at end thread 2: 214.482944MB (obj cache: 136)
[9:9:16, Fri Jul 23, 2010] [0] Memory usage at idle thread 2: 214.482944MB (obj cache: 136)
[9:9:16, Fri Jul 23, 2010] [315] Sending data to thread
[9:9:16, Fri Jul 23, 2010] [0] 4 - Creating a new obj
[9:9:17, Fri Jul 23, 2010] [0] Memory usage at end thread 1: 214.355968MB (obj cache: 136)
[9:9:17, Fri Jul 23, 2010] [0] Memory usage at idle thread 1: 214.355968MB (obj cache: 136)
[9:9:18, Fri Jul 23, 2010] [316] Sending data to thread
[9:9:18, Fri Jul 23, 2010] [0] 3 - Creating a new obj
[9:9:18, Fri Jul 23, 2010] [317] Sending data to thread
[9:9:18, Fri Jul 23, 2010] [0] 2 - Creating a new obj
[9:9:18, Fri Jul 23, 2010] [318] Sending data to thread
[9:9:18, Fri Jul 23, 2010] [0] 1 - Creating a new obj
[9:9:19, Fri Jul 23, 2010] [0] Memory usage at end thread 4: 214.355968MB (obj cache: 136)
[9:9:19, Fri Jul 23, 2010] [0] Memory usage at idle thread 4: 214.355968MB (obj cache: 136)
[9:9:19, Fri Jul 23, 2010] [0] Memory usage at end thread 1: 214.355968MB (obj cache: 136)
[9:9:19, Fri Jul 23, 2010] [0] Memory usage at idle thread 1: 214.355968MB (obj cache: 136)
[9:9:20, Fri Jul 23, 2010] [0] Memory usage at end thread 3: 214.482944MB (obj cache: 136)
[9:9:20, Fri Jul 23, 2010] [0] Memory usage at idle thread 3: 214.482944MB (obj cache: 136)
[9:9:20, Fri Jul 23, 2010] [0] Memory usage at end thread 2: 214.482944MB (obj cache: 136)
[9:9:20, Fri Jul 23, 2010] [0] Memory usage at idle thread 2: 214.482944MB (obj cache: 136)
更新(8/12/2010):我刚刚使用带有线程和系统malloc的新编译版本的Perl 5.12运行了一天。奇怪的是,我得到了相同的行为。每次失去几个MB,缓慢地。我可能会尝试使用Valgrind来查看我为什么失去它。然而,当我在玩其他东西时,我想到了另一件事情。我的脚本创建并销毁(据称)许多SSL套接字。像IO::Socket::SSL这样广泛使用的模块是否可能泄漏一点?或者是OpenSSL?(使用v0.9.8o)。要尝试同步访问SSL模块,看看它是否有任何影响,可能会遇到线程访问它的问题。

更新:尝试在每个线程中单独加载模块,更快的内存使用率。尝试锁定使用套接字函数的区域,以便只有一个线程同时使用它们,仍然会像以前一样丢失内存。将工作线程数从4增加到10,完成相同数量的工作。内存没有持续30分钟。这让我相信,它要么是Perl内部的线程实现问题,要么是堆栈问题(不是故意的)。我尝试使用内置线程方法更改堆栈大小,但结果相同。要寻找另一种方法。也许是一种更低级别的方法。增加线程数会使内存更快地消耗...似乎与线程的堆栈实现或堆栈的大小有关。

更新(9/15/2010):在IO::Socket::SSL文档中发现了这个有趣的片段...

这是因为需要循环引用才能使IO::Socket::SSL套接字同时像对象和全局引用一样工作。

"循环引用"?另一个可能的解释是,即使我明确地取消引用了它们,这些套接字也会一直保留一段时间。要研究Weaken,看看它是否对套接字产生影响。如果我发现任何有趣的事情,我会告诉你的。

已解决(9/16/2010):请参见我发布的包含解决方案的答案。


发布一些代码或查找循环引用:P - Jeff Ober
1
你看过这个问题吗:https://dev59.com/5XRC5IYBdhLWcg3wCMnX - dwarring
更改线程的堆栈大小没有任何帮助...将尝试升级我的 Perl 到 5.12。 - casey
4
如果您没有使用glibc,而是使用perl malloc(默认值),则永远不会将内存释放给操作系统。进程大小将代表perl曾经占用的最大大小。尝试重新构建并使用glibc malloc(需要重新编译)来查看是否提供了不同的内存配置文件。除此之外,现在应该展示代码了。 - Evan Carroll
1
您可能正在经历内存碎片化问题。这是一个棘手的问题,没有好的解决方法。 - Gabe
显示剩余6条评论
4个回答

16

你的Perl版本已经有4年半了,建议升级到5.12。查看一下5.12版本的构建说明,注意其中大量的线程改进,这些可能会神奇地解决你那不明确的问题:

  • 5.12版本:在多线程情况下@_和$_不再泄漏(RT# 34342和# 41138,还有# 70602、# 70974)
  • 5.10版本:在ithreads下,PL_reg_curpm中的正则表达式现在是引用计数的。这消除了许多应对它没有被引用计数而进行hackish(即妥协性)的工作。
  • 5.9版本:线程:多个修复,例如处理join()问题和内存泄漏。在一些使用glibc的平台(如Linux)中,一个ithread的最小内存占用量已经减少了几百KB。
  • 5.9版本:threads::shared已针对许多内存泄漏进行了修复。

当涉及四年的线程开发以及可能导致此问题的各种广泛因素时,清单还在继续,请查看threads::shared的现代更改日志。

我已在你的帖子上发表了评论,这是我的下一组建议:如果你没有使用glibc且使用perl malloc(默认值),你将永远无法将内存释放给操作系统。进程大小将表示perl曾经占用的最大大小。尝试使用glibc malloc进行重建(需要重新编译),看看是否提供了不同的内存配置文件。除此之外,现在需要展示代码了。


为什么这个被踩了?像这样的问题,这个答案可能会有所帮助。 - Konerak
1
这很有帮助。我已经将5.10下载到服务器上,但还没有构建它。现在我想我可能会下载5.12并构建那个版本。我仍然在想这是否可能是线程默认堆栈大小的问题? - casey
3
在v5.12.1上运行我的脚本已经一个小时了,这是朝着正确方向迈出的一大步。谢谢。我把结果发布到问题中了。 - casey

6
最终解决了泄漏问题。首先,我想向您展示改进情况。由于用户基数自第一张图以来已经增加,请不要看实际数字,只需看斜率的差异。
之前的内存使用图:http://i32.tinypic.com/311nc08.jpg
之后的内存使用图:http://i51.tinypic.com/29goill.jpg 几个月来,我一直在每隔几天重启服务器,但在过去的14个小时里,内存使用量没有增加。
我用来帮助开发服务器的每个教程、示例、演示文稿和书籍都省略了一个非常非常重要的关于IO::Socket::SSL的事实。而且,所有在线程应用程序中使用该模块的人最好听从。没有人曾强调过IO::Socket::SSL文档中的最后一行,这使我非常愚蠢地假设我创建的任何套接字,就像几乎任何其他数据结构一样,在超出范围后将被释放(是的,我知道这条规则的例外)。我想为大家提供帮助并指出我所指的那行。
引用如下:
“...IO::Socket::SSL套接字将保持打开状态,直到程序结束或您明确关闭它们。这是因为需要一个循环引用来使IO::Socket::SSL套接字同时像对象和全局引用一样操作。” http://search.cpan.org/dist/IO-Socket-SSL/SSL.pm#LIMITATIONS 我从未意识到这些套接字中有一个循环引用,如果我不知道它们,那么我阅读的每个人的博客和书籍也不知道(因此需要指出)。
因此,可以想象,这有一个非常简单的解决方案。在我的线程工作循环中,在每次迭代中创建一个套接字时,我只需在底部放置一个eval { close $socket; };undef $socket;以确保在处理下一个客户端之前关闭它。我启动了服务器并等待观察内存使用情况,正如您在我的第二张图中所看到的那样,内存使用量很稳定。因此,在两个月的间歇性故障排除之后,我终于找到了解决方案。希望这能为其他业余编程人员提供一些见解。感谢所有提供答案/评论/建议的人,每一点都有帮助,并且有一个地方可以弹簧式思考确实有所帮助。

恭喜!对我来说,IO::Socket::SSL的开发者应该 sub CLONE { warn 'read the docs LIMITATION section' } 并要求你选择退出以关闭它。我看不出为什么需要循环引用。我敢打赌这是由一个C语言黑客转到Perl而不是Perl专家编写的。提交错误报告并推动修复或添加更多反馈。 - Evan Carroll
我想补充一下,这听起来被引用错误了,它说如果你有Scalar::Util,就不会发生这种情况。而且,如果你使用的是5.8.8,尤其是如果你升级了,那么你就有了Scalar::Util。也许你应该抱怨一下在Red Hat或者你使用的其他发行版中决定不包含核心模块的混蛋们... 或者根据你的数字,pod文档是错的。但我仍然可以看到这个模块可能更加冗长。 - Evan Carroll
@Evan 我从源代码安装了标准发行版,升级到了5.12.1。它确实带有Scalar::Util,但出于某种原因它并没有起作用。我尝试将Scalar::Util升级到最新版本,并安装了(文档中说也会有帮助的)WeakenRef。仍然存在同样的问题。非常奇怪,但最终在超出范围之前手动关闭它们解决了问题。这就是为什么我省略了关于那些模块的内容,因为它们似乎对我没有帮助,但是,是的,它们应该为您处理这个问题。 - casey
那么你不仅仅是遇到了一个小错误,而是遇到了一个巨大的错误。你应该提供一个测试用例并与作者联系。你已经做了很多工作,我不想看到其他人再次重复这个过程。 - Evan Carroll

1

尝试升级threads.pm和threads::shared。升级到perl 5.12.1也是一个不错的选择。


是的,我首先升级了这些模块。并没有什么帮助。我只是重新构建了perl 5.12.1,并再次安装了所有模块。等待几个小时后再看看内存使用情况如何。 - casey

1

我在5.10版本中遇到了同样的问题,尽管有很多人声称使用新版Perl时线程不会泄漏内存,但实际上使用线程时Perl确实会泄漏内存。

我的解决方案是使用Thread::Pool::Simple创建线程池,而不是创建新线程。在我的情况下,我并不希望有很多同时运行的线程,也不希望它们持续很长时间(最多可能只有30秒)。我不知道这是否适用于您,但如果您只是让线程处理简单任务,那么这可能是一个选择。


问题在于我一直有十个同时运行的线程。在最初的10个之后,我不会创建或销毁任何线程。因此,我不需要像Thread::Pool这样的池管理对象。但是,如果你说切换到Thread::Pool可以避免内存泄漏,那我会试一试。我的第二个犹豫是我必须在开始时创建所有线程,不确定这是否允许我这样做。也许你有这方面的信息。父进程的内存变得太大了,我希望将线程仅作为工作者。 - casey
我的情况有很多线程来回操作,而不是停留,所以对我有效的可能对你没有用。我注意到的问题是:即使创建的线程什么也不做并立即退出,也会泄漏一点内存。过了一段时间,它就会逐渐累积到实际使用中。我对Thread::Pool::Simple的实现不太熟悉,但它确实有“min”和“max”选项,这表明在启动池时会创建“min”个线程,但直到调用池的“add”方法才会执行任何工作。 - Phil

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接