数小时后 RAM 密集型 C# 进程变慢

RAM intensive C# process getting slower after several hours

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

当我启动进程时,它处理 n pages/s。几个小时后,速度下降到 n/2 pages/s 左右。几天后它可以下降到 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);
        }
    }
}

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

已排除所有明显原因:

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

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

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

我发现当一个 yield 的数据开始 space 泄漏编译器生成的局部变量 classes 开始引起问题。由于没有可用代码,这是我的急救箱。

  1. 确保设置the right strategy,在app.config中更改GC收集策略将有助于碎片化。

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

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

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

  5. HTML敏捷最糟糕的地方在于fragments that ends up being a real issue

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

怎么做。

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

  2. 然后通过输入

    将执行加载到您的内存中
    .loadby sos clr
    
  3. 然后输入

    !dumpheap -stat
    

然后你会得到在你的应用程序中分配的内存项,内存地址和大小按类型分组并从低头到高头排序你会看到类似 System.String[] 的东西有大量前面的数字,就是你要先调查的内容。

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

!dumpheap -mt <heap address>

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

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

!gcroot <address>

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

这就是所谓的“生产调试”,如果您可以访问服务器,我想您可以访问它,它就可以工作。