【问题标题】:How to gain control of a 5GB heap in Haskell?如何在 Haskell 中控制 5GB 堆?
【发布时间】:2011-10-01 04:35:02
【问题描述】:

目前我正在试验一个用 Snap 编写的小型 Haskell Web 服务器,它可以加载并向客户端提供大量数据。而且我很难控制服务器进程。在随机时刻,该进程会在几秒钟到几分钟内使用大量 CPU,并且对客户端请求无响应。有时内存使用量会在几秒钟内达到数百兆字节(有时会下降)。

希望有人对使用大量内存的长时间运行的 Haskell 进程有更多经验,并且可以给我一些指示以使事情更稳定。我已经调试了好几天了,现在我开始有点绝望了。

我的设置的一点概述:

  • 在服务器启动时,我将大约 5 GB 的数据读入内存中的大型(嵌套)Data.Map 结构。嵌套映射是值严格的,并且映射内的所有值都是数据类型,并且它们的所有字段也都是严格的。我花了很多时间来确保不会留下未评估的重击。导入(取决于我的系统负载)大约需要 5-30 分钟。奇怪的是连续运行的波动比我预期的要大得多,但这是一个不同的问题。

  • 大数据结构位于“TVar”中,由 Snap 服务器生成的所有客户端线程共享。客户端可以使用小型查询语言请求数据的任意部分。请求的数据量通常很小(最多300kb左右),只涉及数据结构的一小部分。所有只读请求都使用“readTVarIO”完成,因此它们不需要任何 STM 事务。

  • 服务器使用以下标志启动:+RTS -N -I0 -qg -qb。这将以多线程模式启动服务器,禁用空闲时间和并行 GC。这似乎大大加快了进程。

服务器大部分运行没有任何问题。但是,有时客户端请求会超时,CPU 会飙升至 100%(甚至超过 100%),并且会持续很长时间。同时服务器不再响应请求。

我能想到的几个原因可能会导致 CPU 使用率:

  • 这个请求需要很多时间,因为有很多工作要做。这有点不太可能,因为有时会发生在以前的运行中被证明非常快的请求(快速我的意思是 20-80 毫秒左右)。

  • 在处理数据并将其发送到客户端之前,仍然需要计算一些未评估的 thunk。这也不太可能,原因与上一点相同。

  • 不知何故垃圾收集开始扫描我的整个 5GB 堆。我可以想象这会占用很多时间。

问题是我不知道如何弄清楚到底发生了什么以及如何解决这个问题。因为导入过程需要很长时间,所以分析结果并没有向我显示任何有用的信息。似乎没有办法在代码中有条件地打开和关闭分析器。

我个人怀疑 GC 是这里的问题。我正在使用 GHC7,它似乎有很多选项可以调整 GC 的工作方式。

在使用具有通常非常稳定的数据的大型堆时,您推荐哪些 GC 设置?

【问题讨论】:

  • 嗯.. 有趣.. 运行此服务器应用程序的盒子有多少 RAM
  • 我的机器上总共有 8GB RAM。应该够了。
  • 是的.. 这似乎足以避免页面错误
  • 一开始就传入一个大的-H 值可能是值得的。我不认为这对暂停有帮助,但它可以帮助加载时间。
  • 你能使用开销更少的数据类型吗?例如,HashMap 的开销比 Map 小。

标签: performance haskell garbage-collection memory-management


【解决方案1】:

大量的内存使用和偶尔的 CPU 峰值几乎肯定是 GC 开始的原因。您可以通过使用像 -B 这样的 RTS 选项来查看情况是否确实如此,这会导致 GHC 在有主要收集时发出哔哔声,@987654322 @ 会在事后告诉您统计信息(特别是,查看 GC 时间是否真的很长)或 -Dg,它会打开 GC 调用的调试信息(尽管您需要使用 -debug 进行编译)。

您可以采取多种措施来缓解此问题:

  • 在最初导入数据时,GHC 浪费了大量时间来增长堆。您可以通过指定一个大的-H 来告诉它一次获取您需要的所有内存。

  • 具有稳定数据的大堆将被提升为老年代。如果您使用-G 增加代数,您可能能够将稳定数据放在最老的、很少被 GC 处理的一代中,而您有更传统的年轻堆和年老堆。

  • 根据应用程序其余部分的内存使用情况,您可以使用-F 调整 GHC 允许老年代增长多少,然后再收集它。您也许可以调整此参数以收集此非垃圾。

  • 1234563永远。

这些都是推测,所以请使用您的特定应用程序进行测试。

【讨论】:

  • 使用压缩 GC 也很有用。
  • 我还没有彻底测试过,但是增加代数似乎确实有积极的作用。
【解决方案2】:

我在 1.5GB 的嵌套地图堆中遇到了非常相似的问题。默认情况下,在空闲 GC 开启的情况下,每次 GC 都会冻结 3-4 秒,而在空闲 GC 关闭(+RTS -I0)的情况下,我会在数百次查询后冻结 17 秒,从而导致客户端时间-out。

我的“解决方案”首先是增加客户端超时,并要求人们容忍这样的情况,虽然 98% 的查询大约是 500 毫秒,但大约 2% 的查询会非常慢。但是,为了获得更好的解决方案,我最终运行了两台负载均衡的服务器,并让它们从集群中脱机,以便每 200 次查询执行一次 GC,然后再恢复运行。

雪上加霜,这是对原始 Python 程序的重写,从未出现过此类问题。公平地说,我们确实获得了大约 40% 的性能提升、非常容易的并行化和更稳定的代码库。但是这个讨厌的 GC 问题...

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-01-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多