几个小时后,RAM 密集型 C# 进程变慢

Posted

技术标签:

【中文标题】几个小时后,RAM 密集型 C# 进程变慢【英文标题】:RAM intensive C# process getting slower after several hours 【发布时间】:2018-10-21 07:00:04 【问题描述】:

我在负责连续解析 html 页面的服务器上运行 C# 进程(服务)。它依赖于 HTMLAgilityPack。症状是随着时间的推移,它变得越来越慢。

当我启动进程时,它每秒处理 n 页。几个小时后,速度下降到大约 n/2 页/秒。几天后它可以下降到 n/10。这种现象已被多次观察到,而且是相当确定的。任何时候重新启动该过程,一切都会恢复正常。

非常重要:我可以在同一进程中运行其他计算并且它们不会变慢:我可以随时使用任何我想要的东西达到 100% CPU。这个过程本身并不慢。只有 HTML 解析会变慢。

我可以用最少的代码重现它(实际上原始服务中的行为有点极端,但这段代码仍然重现了行为):

public static void Main(string[] args) 
    string url = "https://en.wikipedia.org/wiki/History_of_Texas_A%26M_University";
    string html = new HtmlWeb().Load(url).DocumentNode.OuterHtml;
    while (true) 
        //Processing
        Stopwatch sw = new Stopwatch();
        sw.Start();
        Parallel.For(0, 10000, i => new HtmlDocument().LoadHtml(html));
        sw.Stop();
        //Logging
        using(var writer = File.AppendText("c:\\parsing.log")) 
            string text = DateTime.Now.ToString() + ";" + (int) sw.Elapsed.TotalSeconds;
            writer.WriteLine(text);
            Console.WriteLine(text);
        
    

使用这个最小的代码,它显示速度(每秒页数)作为进程启动后经过的小时数的函数:

已排除所有明显原因:

HTML 页面更大或不同(在最小代码中它是同一页面) 完整 RAM:进程在 32 GB 上使用大约 500 MB 其他进程使用 CPU 或 RAM

这可能与 RAM 和内存分配有关。我知道 HTMLAgilityPack 会分配大量的小对象内存(HTML 节点和字符串)。很明显内存分配和多线程不能很好地协同工作。但我不明白这个过程是如何变得越来越慢的。

您是否知道有关 CLR 或 Windows 的任何信息可能会导致某些 RAM 密集型(许多分配)处理变得越来越慢? 例如,以某种方式惩罚执行内存分配的线程?

【问题讨论】:

很难说没有一些代码可以推理,但如果您找不到原因,您应该将处理移至单独的进程。对于每个文件(或批处理),该服务仅为此执行一个单独的进程。 您需要发布代码,否则太宽泛,我们无法为您提供帮助 你能显示一些代码吗?回答你的问题:吨。这样我们就需要看代码了。 寻求调试帮助的问题(“为什么这段代码不起作用?”)必须包括所需的行为、特定的问题或错误以及在问题本身中重现它所需的最短代码。没有明确问题陈述的问题对其他读者没有用处。 你使用后有没有明确dispose过filestream?会不会是windows持有很多文件句柄等待GC? 【参考方案1】:

我注意到使用 HTMLAgilityPack 的类似行为。

我发现当一个 yield 的数据开始空间泄漏时,编译器生成的类上的局部变量开始导致问题。由于没有可用的代码,这是我的急救箱。

    请务必设置the right strategy,在app.config中更改GC收集策略将有助于分片。

    确保在不需要它们时将它们清空,一旦不需要它们,不要等待范围清理内存,因为 IEnumerables 在调用方法和方法变量的范围中被调用,并且可以活得比你想象的要长得多!在 ILSpy 中打开您的代码并查看 d__0(0) 生成的类。你会看到像 d__.X=X; 这样生成的东西。在这种情况下,X 可以保存一个片段或整个页面。

    您的局部变量被提升到堆中,因为如果它们不存在,则无法在 IEnumable 迭代中访问它们。

    锁定开始成为一个问题,第 4 代 ram 中的大项目正在流血,实际上会开始阻塞 GC。 GC 正在暂停您的线程以执行垃圾回收。

    HTMLAgility 最糟糕的地方在于它fragments that ends up being a real issue

    我很确定,当您开始考虑 HTML 片段的范围时,您会发现事情会开始顺利。使用WinDbg in SOS 查看您的执行情况,然后转储您的内存并查看。

如何做到这一点。

    打开 WinDebug,按 F6 并附加到进程(在字段中输入进程 ID 并按确定)

    然后通过输入将执行加载到您的内存中

    .loadby sos clr
    

    然后输入

    !dumpheap -stat
    

然后,您将获得应用程序中分配的内存项目,其中包含按类型分组的内存地址和大小,并从低标头到高标头排序,您将看到 System.String[] 之类的内容,前面有大量数字它,这就是你要先调查的东西。

现在看看谁有你可以输入的

!dumpheap -mt <heap address>

您将看到正在使用该内存表 (MT) 的地址以及它使用的 ram 大小。

现在它变得有趣了,而不是你输入的 x100 行代码

!gcroot <address>

它将打印分配内存的文件和代码行、编译器生成的类和导致您悲伤的变量以及它包含的字节。

这就是所谓的“生产调试”,如果你可以访问服务器,我猜你有。

【讨论】:

谢谢。我会一步一步地尝试这个。 您好 Bernoid,WinDbg 绝对是一个值得研究的神奇工具,youtube.com/results?search_query=WinDbg 中有一些关于如何使用它的精彩视频 我使用了错误的 GC。我切换到服务器并禁用了并发。解析从一开始就快 10 倍并且保持快速。也许您可以在回答中坚持:明确必须使用服务器GC。尽管我已经阅读了有关它的内容,但我的脑海中并不清楚。你救了我的命(至少):-) 我很快就会看看 WinDbg。 很高兴能帮上忙 我想知道有多少其他用户会意识到 app.config 中的更改可以使他们的应用程序“快 10 倍”这一事实。 GC 经常被忽视,我发现大多数人甚至不知道它有设置

以上是关于几个小时后,RAM 密集型 C# 进程变慢的主要内容,如果未能解决你的问题,请参考以下文章

如何避免从 C# 构建的 Sql Server 2005 参数化查询变慢

BerkeleyDB 变胖变慢

Redis变慢了(六) - 绑定CPU-开启AOF

Multiprocessing.Pool 使 Numpy 矩阵乘法变慢

关于Windows系统不会变慢的设想

MySQL负载查询变慢