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

时间:2018-05-10 22:43:14

标签: c# multithreading performance memory-management html-agility-pack

我在负责连续解析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);
        }
    }
}

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

enter image description here

排除了每一个明显的原因:

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

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

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

1 个答案:

答案 0 :(得分:4)

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

我发现当一个yield的数据开始太空时,会在编译器生成的类上泄漏本地变量,从而导致出现问题。因为没有代码可用...... bla bla,这是我的急救工具包

  1. 确保设置the right strategy,更改app.config中的GC集合状态将有助于碎片化。
  2. 当你不需要它们时,确保你没有它们,一旦你不需要它们,不要等待范围来清理你的内存,因为IEnumerables在调用方法和方法范围中被调用并且可以住得比你想象的要长得多!在ILSpy中打开您的代码,然后查看<> d__0(0)生成的类。您将看到生成的内容,如d __。X = X;在这种情况下,X可以保存片段或整页。
  3. 您的本地变量被提升到堆中,因为如果它们不存在,则无法在IEnumable迭代中访问它们。
  4. 锁定开始成为一个问题,大型物品在你的第4代ram中淹没,它们正在开始阻止GC。 GC正在暂停您的线程以执行垃圾收集。
  5. HTMLAgility最糟糕的是fragments that ends up being a real issue

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

  6. 怎么做。

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

      将执行加载到您的记忆中

      .loadby sos clr

    3. 然后输入

      !dumpheap -stat

    4. 然后,您将获得在应用程序中分配的内存项,其中包含内存地址和按类型分组的大小,并从低标题到高标题排序,您将看到类似System.String []的内容,前面有一个masive数字它,这就是你想先调查的后缀。

      现在看看谁可以输入

      !dumpheap -mt <heap address>
      

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

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

      !gcroot <address>
      

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

      这就是所谓的“生产调试”,如果你有权访问服务器,那我觉得你有。

      希望得到帮助,

      沃尔特