WCF服务因未处理OutOfMemoryException异常终止问题排查
我先梳理下你的场景:你开发了一个WCF服务,核心功能是接收客户消息、解析为指定格式后,通过TCP/HTTP/FTP等协议转发给其他客户端。这个Windows服务采用TPL为每个客户端创建长期运行的线程,日志方案使用NLog,配置了文件日志和事件查看器日志,具体配置如下:
<target xsi:type="File" name="flatfile" layout="${longdate} ${uppercase:${level}} ${message} ${exception:format=tostring,StackTrace}" archiveAboveSize="2000000" archiveFileName="${basedir}/logs/archive/${shortdate}-{#####}.engine.log" archiveNumbering="Sequence" archiveEvery="None" maxArchiveFiles="100" fileName="${basedir}/logs/engine.current.log" keepFileOpen="true" concurrentWrites="true" /> <target xsi:type="EventLog" name="eventlog" layout="${longdate} ${uppercase:${level}} ${message} ${exception:format=tostring} ${StackTrace}" log="Application" source="Nuance Interface Engine Service" eventId="${event-properties:EventID}" />
尽管已经配置了concurrentWrites="true",服务运行20多小时后,事件查看器中出现未处理的System.OutOfMemoryException,异常栈如下:
Application: MyWcfService.exe Framework Version: v4.0.30319 Description: The process was terminated due to an unhandled exception. Exception Info: System.OutOfMemoryException at System.Text.StringBuilder..ctor(System.String, Int32, Int32, Int32) at NLog.Layouts.SimpleLayout.GetFormattedMessage(NLog.LogEventInfo) at NLog.Targets.FileTarget.GetFormattedMessage(NLog.LogEventInfo) at NLog.Targets.FileTarget.GetBytesToWrite(NLog.LogEventInfo) at NLog.Targets.FileTarget.Write(NLog.Common.AsyncLogEventInfo[]) at NLog.Targets.Target.WriteAsyncLogEvents(NLog.Common.AsyncLogEventInfo[]) at NLog.Targets.Wrappers.AsyncTargetWrapper.ProcessPendingEvents(System.Object) at System.Threading.TimerQueueTimer.CallCallbackInContext(System.Object) at System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object, Boolean) at System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object, Boolean) at System.Threading.TimerQueueTimer.CallCallback() at System.Threading.TimerQueueTimer.Fire() at System.Threading.TimerQueue.FireQueuedTimerCompletion(System.Object) at System.Threading.QueueUserWorkItemCallback.System.Threading.IThreadPoolWorkItem.ExecuteWorkItem() at System.Threading.ThreadPoolWorkQueue.Dispatch() at System.Threading._ThreadPoolWaitCallback.PerformWaitCallback()
紧接着1秒后,进程崩溃的错误日志:
Faulting application name: Hl7ic.Engine.View.exe, version: 18.0.1.160, time stamp: 0x5af5cd1f Faulting module name: KERNELBASE.dll, version: 6.3.9600.18938, time stamp: 0x5a7dd8a7 Exception code: 0xe0434352 Fault offset: 0x00015ef8 Faulting process id: 0x1074 Faulting application start time: 0x01d3ea7338d9851c Faulting application path: C:\Program Files (x86)\MyServices\MyWcfService.exe Faulting module path: C:\windows\SYSTEM32\KERNELBASE.dll Report Id: 59b36929-5688-11e8-80ca-005056a80aaa Faulting package full name: Faulting package-relative application ID:
你最初怀疑是多线程日志访问冲突,添加了concurrentWrites="true"但问题依旧,下面是我整理的排查方向和解决方案:
问题分析与排查方向
从异常栈来看,内存溢出发生在NLog格式化日志消息的环节——具体是SimpleLayout.GetFormattedMessage调用StringBuilder构造的时候,这说明根本不是文件写入的并发冲突问题,而是日志消息本身或者NLog内部的内存累积导致的。接下来给你几个重点排查方向:
1. 日志消息包含超大内容
如果你的服务在处理消息时,把整个原始消息(比如大体积的HL7报文、二进制数据)直接写入日志,会导致每次日志条目都占用大量内存,长期运行后累积引发OOM。
- 检查
${message}字段对应的日志内容,是否存在几MB甚至几十MB的超大字符串。 - 建议对大消息做截断处理,比如只记录消息ID、关键字段,或者限制日志消息的最大长度:
<!-- 在layout中对message做截断,保留前1000个字符 --> layout="${longdate} ${uppercase:${level}} ${message:maxLength=1000} ${exception:format=tostring,StackTrace}"
2. NLog AsyncTargetWrapper的队列溢出
默认情况下,NLog的AsyncTargetWrapper会使用无界队列来缓存待处理的日志事件。如果你的服务产生日志的速度远大于写入磁盘的速度,队列会持续膨胀,最终耗尽内存。
- 检查NLog配置中是否显式设置了
AsyncTargetWrapper的队列限制。如果没有,建议添加queueLimit和overflowAction参数:
设置队列上限后,当队列满时会丢弃旧日志,避免内存无限增长。<targets> <target xsi:type="AsyncWrapper" name="asyncFlatFile" queueLimit="10000" overflowAction="Discard"> <target xsi:type="File" name="flatfile" ... /> <!-- 原文件target配置 --> </target> <!-- 事件日志也建议添加异步包装 --> <target xsi:type="AsyncWrapper" name="asyncEventLog" queueLimit="5000" overflowAction="Discard"> <target xsi:type="EventLog" name="eventlog" ... /> <!-- 原事件日志target配置 --> </target> </targets>
3. 长期运行线程的内存泄漏
你的服务使用TPL创建长期运行的线程,需要检查这些线程是否持有大对象引用(比如未释放的消息对象、日志上下文),导致GC无法回收。
- 使用内存分析工具(比如Visual Studio内存诊断、dotMemory)捕获服务运行20小时后的内存快照,查看哪些对象占用了大量内存:
- 检查是否有大量
LogEventInfo对象堆积,说明NLog的异步队列没有及时处理。 - 检查是否有未释放的消息对象、网络连接资源等。
- 检查是否有大量
4. NLog版本问题
旧版本的NLog可能存在内存泄漏的bug,比如在处理异步日志、大日志消息时的内存管理问题。
- 检查当前使用的NLog版本,建议升级到最新的稳定版本(比如4.7.x及以上),新版本修复了很多内存相关的问题。
5. 事件日志的性能瓶颈
事件日志的写入速度远慢于文件日志,如果大量日志写入事件日志,可能导致NLog的异步队列阻塞,进而累积内存。
- 评估事件日志的必要性,是否需要记录所有级别的日志?可以降低事件日志的日志级别(比如只记录Error和Fatal),减少写入量:
<rules> <logger name="*" minlevel="Error" writeTo="eventlog" /> <logger name="*" minlevel="Info" writeTo="flatfile" /> </rules>
验证步骤
- 先对日志消息做截断处理,观察服务是否还会出现OOM。
- 添加
AsyncTargetWrapper的队列限制,控制内存占用。 - 升级NLog到最新版本,排除已知bug。
- 用内存分析工具监控内存使用,定位泄漏点。
内容的提问来源于stack exchange,提问作者CSharpDev

