C#进程占用大量内存,在数小时后变得更慢

4
我在服务器上运行一个 C# 进程(服务),负责持续解析 HTML 页面,它依赖于 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);
        }
    }
}

使用这个简洁的代码,它将显示随着时间(自进程启动以来经过的小时数)变化的速度(每秒页面数):
这里是图片:enter image description here 明显的原因都已排除:
- HTML页面更大或不同(在最小代码中是相同的页面) - 内存已满:进程在32 GB上使用约500 MB - 其他进程使用CPU或RAM
可能与内存和内存分配有关。我知道HTMLAgilityPack会进行许多小对象的内存分配(HTML节点和字符串)。很明显,内存分配和多线程并不协作良好。但我不明白为什么进程会变得越来越慢。
你是否了解CLR或Windows中有什么可能导致需要大量内存(许多分配)的处理变得越来越慢?例如惩罚执行某种内存分配的线程?

1
很难在没有代码的情况下进行推理,但如果您找不到原因,应将处理移动到单独的进程中。对于每个文件(或批处理),服务仅为其执行单独的进程。 - Adriano Repetti
2
你需要发布代码,否则这个问题太宽泛了,我们将无法帮助你。 - Camilo Terevinto
1
你能展示一些代码吗?回答你的问题:有很多种可能。这就是为什么我们需要看到这段代码的原因。 - Stefan
1
寻求调试帮助的问题(“为什么这段代码不起作用?”)必须在问题本身中包含所需的行为、具体问题或错误以及重现它所需的最短代码。没有明确问题陈述的问题对其他读者没有用处。 - Enigmativity
1
你在使用完文件流后是否显式地进行了处理?Windows 是否可能会持有许多文件句柄等待垃圾回收? - ricky
显示剩余8条评论
1个回答

4
我注意到使用HTMLAgilityPack时有类似的行为。
我发现当一个yield返回数据时,它会开始在编译器生成的类上泄漏本地变量,从而引起问题。由于没有可用的代码,这是我的急救包。
  1. 确保您设置了正确的策略,更改app.config中的GC收集策略将有助于减少碎片。
  2. 确保您在不需要时将事物置空,一旦您不需要它们,不要等待范围清理内存,因为IEnumerables在调用方法和方法变量的作用域中被调用,并且可以存在比您想象的时间更长!在ILSpy中打开您的代码并查看生成的类<>d__0(0)。在这种情况下,您将看到生成的内容如d__.X=X;,其中X可能保存一个片段或整个页面。
  3. 您的本地变量被提升到堆上,因为如果它们不在那里,就无法在IEnumable迭代中访问它们。
  4. 锁定开始成为一个问题,大型项目正在流失到第四代RAM中,实际上会开始阻塞GC。GC暂停您的线程以执行垃圾回收。
  5. HTMLAgility最糟糕的是碎片最终成为真正的问题

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

如何做到。
  1. open WinDebug, press F6 and attach to the process (enter the process ID in the field and press ok)

  2. then load the execution in your memory by entering

    .loadby sos clr
    
  3. then enter

    !dumpheap -stat
    
你会看到按类型分组并从低头部到高头部排序的内存项,每个内存项都有其内存地址和大小,例如 System.String[] 前面跟着一个巨大的数字,这是你想要首先调查的内容。
现在,为了查看谁拥有它,你可以输入
!dumpheap -mt <heap address>

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

现在变得有趣了,您不必再浏览100行代码,只需输入:

!gcroot <address>

它打印的是分配内存的文件和代码行、编译器生成的类以及引起问题的变量以及它所持有的字节。这就是所谓的“生产调试”,只有当你可以访问服务器时才能使用。

嗨Bernoid,WinDbg绝对是值得研究的神奇工具,这里有一些关于如何使用它的不错视频:https://www.youtube.com/results?search_query=WinDbg。 - Walter Verhoeven
我使用了错误的GC。我切换到服务器并禁用了并发。从一开始解析速度提高了10倍,而且保持快速。也许你可以在你的回答中坚持这一点:必须使用服务器GC。尽管我已经阅读了相关资料,但在我的脑海中仍不清楚。你救了我的命(至少):-) 我很快就会看WinDbg。 - Benoit Sanchez
我想知道还有多少其他用户会注意到更改app.config可以使他们的应用程序“快10倍”。GC经常被忽视,我发现大多数人甚至不知道它有设置。 - Walter Verhoeven
之前我有些困惑,不太清楚gcConcurrent和gcServer标记的区别,所以花了一周时间在HAP和AngleSharp项目中提问,试图解决问题。实际上,除非您分配大块内存并运行密集的多线程,否则默认的GC配置根本无法工作,症状各异。详情请参见https://github.com/AngleSharp/AngleSharp/issues/670。或许微软应该将这些内容用红色字体标注出来。 - Benoit Sanchez
如果您可以访问源代码并进行更改,请打开一个缺陷,并明确地将由解析器生成的本地变量清空,然后将其设置为null。清空将只是删除指针并允许HTML文件保留在内存中,而清空它将“覆盖”它。 - Walter Verhoeven
显示剩余2条评论

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