2011-06-02 94 views
58

我有一个C#Windows服务,我最近从.NET 3.5移到.NET 4.0。没有其他代码更改。.NET 4.0中非常高的内存使用率

在3.5上运行时,给定工作负载的内存利用率大约为1.5 GB内存,吞吐量为每秒20 X。 (在这个问题中,X无关紧要。)

运行在4.0上的完全相同的服务使用3GB和5GB +内存之间,并且每秒获得小于4 X的内存。事实上,随着内存使用量不断攀升,服务通常会停滞不前,直到我的系统使用率达到99%,并且页面文件交换变得不稳定。

我不确定这是否与垃圾回收有关,或者是什么,但我很难搞清楚。我的窗口服务通过使用配置文件切换“服务器” GC如下所示:

<runtime> 
    <gcServer enabled="true"/> 
    </runtime> 

更改此选项设置为false似乎没有有所作为。此外,从我在4.0版本中对新GC进行的阅读中,重大更改仅影响工作站GC模式,而不影响服务器GC模式。所以也许GC与这个问题无关。

想法?

+9

只需双重检查:_only_ .NET框架已更改。还是你从32位机器转到64位?你是否从DEP转到非DEP?你是否从PAE转到非PAE?你从ngen去JIT吗?只是提示触发更多信息 – sehe 2011-06-02 22:07:11

+1

这里没有足够的信息甚至冒险猜测。你偶然使用'BlockingCollection'或'ConcurrentQueue'类吗? 'ConcurrentQueue'有内存泄漏,这可能是一个问题。 – 2011-06-02 22:08:54

+2

ConcurrentQueue在.NET 3.5中不存在,所以这不成问题。 – phoog 2011-06-02 22:11:29

回答

80

这是一个有趣的问题。

根本原因结果是在.NET 4.0上运行时,SQL Server Reporting Services的LocalReport类(v2010)的行为发生了变化。

基本上,微软改变了RDLC处理的行为,以便每次处理报告时,都是在单独的应用程序域中完成的。这实际上是专门解决由于无法从应用程序域卸载程序集而造成的内存泄漏。当LocalReport类处理RDLC文件时,它实际上会创建一个程序集并将其加载到应用程序域中。

在我的情况下,由于我正在处理大量的报告,导致创建了大量的System.Runtime.Remoting.ServerIdentity对象。这是我的原因,因为我很困惑为什么处理RLDC需要远程处理。

当然,要调用另一个应用程序域中的类的方法,远程处理正是您使用的方法。在.NET 3.5中,这是没有必要的,因为默认情况下,RDLC程序集被加载到同一个应用程序域中。但是,在.NET 4.0中,默认情况下会创建一个新的应用程序域。

该修复相当简单。首先,我需要去使用以下配置启用传统的安全策略:

<runtime> 
    <NetFx40_LegacySecurityPolicy enabled="true"/> 
    </runtime> 

接下来,我需要通过调用下面以迫使相同的应用程序域作为我的服务要处理的RDLCs:

myLocalReport.ExecuteReportInCurrentAppDomain(AppDomain.CurrentDomain.Evidence); 

这解决了这个问题。

+1

Thx报告此事。感到羞愧的是,我们无法多次提出这个问题,因为这是一个非常好的发现,并且值得在SO – sehe 2011-06-02 23:49:00

+2

上得到很好的索引。我不确定你是如何追踪这个修复程序的,但是很有效! – 2011-06-03 00:18:59

+5

根据文档已弃用ExecuteReportInCurrentAppDomain。即使有了它,仍然存在内存泄漏,因为每次运行报告时,这些计算DLL都会重新创建,然后保留未使用状态但未卸载。我们发现解决这个问题的唯一方法就是每次我们想要运行报告时都会产生一个新的进程。当进程死亡时,一切都会自动释放,包括不再需要的DLL。想一想你何时使用MS报告。 – Andy 2011-10-04 18:24:26

4

你可能想

也许有些API已经改变了语义或者甚至有可能成为4.0版本框架中的一个bug

+0

有趣的是,我没有跑CLR分析器。当任务管理器显示1.5 GB的内存使用量时,“根”堆显示少于400 MB的使用量。 换句话说,除非我明白这个错误,否则有1.1 GB的未收集垃圾。 同样,根据任务管理器,服务使用2.4 GB时,CLR Profiler显示根为1.1 GB。 – RMD 2011-06-02 22:51:26

+0

我提出了你的答案,因为这是一个很好的建议,而不是完整的答案。 – RMD 2011-06-02 23:42:53

2

只是为了保持完整性,如果有人正在寻找相当于ASP.Net web.config设置,它是:

<system.web> 
    <trust legacyCasModel="true" level="Full"/> 
    </system.web> 

ExecuteReportInCurrentAppDomain的工作原理相同。由于此Social MSDN reference

+2

这种方法的问题是,您不能使用具有传统cas模型的动态类型 - ViewBag的使用将导致异常。 – pkmiec 2014-01-21 10:19:52

+0

是的,我们正在研究此问题,并在Web版本上运行时发生此异常:安全异常 说明:应用程序试图执行安全策略所不允许的操作。要授予此应用程序所需的权限,请联系您的系统管理员或更改配置文件中的应用程序信任级别。 异常详细信息:System.Security.SecurityException:请求失败。 – MikeG 2014-04-05 16:46:51

+0

'我在基于ASP.NET的应用程序的.NET 4.5中遇到了同样的问题,大量使用动态类型进行序列化和反序列化。所以我不能使用,' Adrian Nichols有一个帮手代码 *** https://github.com/AdrianNichols/ssrs-non-native -functions/blob/17ee83c9988acc638eb11f961caf0b2a6b77b555/SSRS_Demo/Business/reportHelper.cs *** 'Keys':***'RenderReportToMemoryAsPDFInAnotherAppDomain' ***方法和***'ReportHelperInAppDomain' *** class – Kiquenet 2015-12-01 07:32:30

8

我遇到了这个确切的问题。确实应用程序域被创建并且没有被清理。不过,我不建议恢复到传统。它们可以通过ReleaseSandboxAppDomain()进行清理。

LocalReport report = new LocalReport(); 
... 
report.ReleaseSandboxAppDomain(); 

一些其他的事情我也做清理:

退订任何SubreportProcessing事件, 清除数据源, 处置的报告。

我们的windows服务每秒处理几个报告,并且没有泄漏。

+0

谢谢,我看了之后测试你上面说的是在工作,我看到报告不再泄漏。 :) 这是我特别做的。我希望它可以帮助别人: localReport.SubreportProcessing - = reportDataProcessor.SubreportProcessingHandler; localReport.DataSources.Clear(); localReport.ReleaseSandboxAppDomain(); localReport.Dispose(); – Brian 2016-11-05 20:46:14

+0

这似乎没有改变任何东西给我。即使有所有建议的清理,仍然有令人讨厌的内存泄漏。 – Jim 2017-07-20 06:27:32

+0

因此,这适用于我,但只有当我通过app.config启用CAS时。 sues999,我想你有这个标志打开或运行.NET 3.5或更早版本。除非创建了沙箱应用程序域,否则ReleaseSandboxAppDomain()不会执行任何操作,这需要CAS。默认情况下,在启用CAS的情况下,LocalReport将使用沙盒AppDomain,所以它看起来像选定的答案海报倒退。我期望在.NET 4.0中默认情况下报告在当前应用程序域中运行,这就是它泄漏的原因。只有AppDomain拆解似乎可以释放报告生成的内容。 – JonathanN 2017-11-22 16:55:16

1

似乎微软试图将报告放入自己独立的内存空间中,以解决所有内存泄漏问题而不是解决问题。在这样做的时候,他们引入了一些硬性崩溃,并最终导致更多的内存泄漏无论如何。他们似乎缓存了报告定义,但从不使用它并从不清理它,并且每个新报告都会创建一个新的报告定义,占用越来越多的内存。

我玩弄同样的事情:使用一个单独的应用程序域并编组报告。我认为这是一个糟糕的解决方案,并且很快就会搞得一塌糊涂。

我所做的是相似的:将您的程序的报告部分分成它自己的单独的报告程序。无论如何,这是组织代码的好方法。

棘手的部分是将信息传递给单独的程序。使用Process类启动报告程序的新实例,并在命令行上传递它需要的所有参数。第一个参数应该是一个枚举或类似的值,表示应该打印的报告。我给这家在主程序代码看起来是这样的:

const string sReportsProgram = "SomethingReports.exe"; 

public static void RunReport1(DateTime pDate, int pSomeID, int pSomeOtherID) { 
    RunWithArgs(ReportType.Report1, pDate, pSomeID, pSomeOtherID); 
} 

public static void RunReport2(int pSomeID) { 
    RunWithArgs(ReportType.Report2, pSomeID); 
} 

// TODO: currently no support for quoted args 
static void RunWithArgs(params object[] pArgs) { 
    // .Join here is my own extension method which calls string.Join 
    RunWithArgs(pArgs.Select(arg => arg.ToString()).Join(" ")); 
} 

static void RunWithArgs(string pArgs) { 
    Console.WriteLine("Running Report Program: {0} {1}", sReportsProgram, pArgs); 
    var process = new Process(); 
    process.StartInfo.FileName = sReportsProgram; 
    process.StartInfo.Arguments = pArgs; 
    process.Start(); 
} 

而且报告程序看起来像:

[STAThread] 
static void Main(string[] pArgs) { 
    Application.EnableVisualStyles(); 
    Application.SetCompatibleTextRenderingDefault(false); 

    var reportType = (ReportType)Enum.Parse(typeof(ReportType), pArgs[0]); 
    using (var reportForm = GetReportForm(reportType, pArgs)) 
     Application.Run(reportForm); 
} 

static Form GetReportForm(ReportType pReportType, string[] pArgs) { 
    switch (pReportType) { 
     case ReportType.Report1: return GetReport1Form(pArgs); 
     case ReportType.Report2: return GetReport2Form(pArgs); 
     default: throw new ArgumentOutOfRangeException("pReportType", pReportType, null); 
    } 
} 

GetReportForm方法应该拉报表定义,利用相关的参数,以获得数据集,将数据和任何其他参数传递给报表,然后将报表放置在表单的报表查看器中,并返回对表单的引用。请注意,可以提取这个过程的大部分内容,以便基本上可以说'使用这些数据和这些参数为这个汇编的报告提供一个表单'。

另请注意,这两个程序都必须能够看到与此项目相关的数据类型,因此希望您已将数据类提取到它们自己的库中,这两个程序都可以共享引用。在主程序中没有所有的数据类,因为在主程序和报表程序之间会有一个循环依赖关系。

不要过多地与参数做它,要么。在报告程序中查询您需要的任何数据库;不要传递大量的对象(这可能不会起作用)。你应该传递简单的东西,如数据库ID字段,日期范围等。如果你有特别复杂的参数,你可能需要将UI的那部分推到报表程序中,而不是在命令行上将它们作为参数传递。

您还可以在主程序中添加对报告程序的引用,生成的.exe和任何相关的.dll文件将被复制到相同的输出文件夹中。然后,您可以运行它而不指定路径,只使用可执行文件名(例如:“SomethingReports.exe”)。您还可以从主程序中删除报告dll。

与此相关的一个问题是,如果您从未实际发布过报告程序,您将收到明显错误。只是虚拟发布一次,生成一个清单,然后它将工作。

一旦你有这个工作,在打印报告时看到你的常规程序的内存保持不变是非常好的。报告程序出现了,占用了比主程序更多的内存,然后消失,完全清除了主程序占用的内存。

另一个问题可能是每个报表实例现在占用比以前更多的内存,因为它们现在是完全独立的程序。如果用户打印大量报告并且从不关闭它们,则会非常快地耗尽大量内存。但我认为这还是好得多,因为只要关闭报告就可以轻松回收记忆。

这也使得您的报告独立于您的主程序。即使在关闭主程序后,它们也可以保持打开状态,并且您可以手动从命令行生成它们,也可以从其他源生成它们。