IO.RecyclableMemoryStream内存泄漏排查求助
问题描述
对API进行内存转储分析后,发现RecyclableMemoryStream与FileBufferingReadStream占用内存规模最大。怀疑问题出在记录请求/响应的中间件上,但相同中间件在其他API中无此现象。本地运行API时无法复现该问题,代码未发现异常,且除该中间件外其余代码未使用任何流。
中间件代码
public class LoggingMiddleware { private readonly RequestDelegate _next; private readonly RecyclableMemoryStreamManager _recyclableMemoryStreamManager; private const string CorrelationIdHeaderKey = "X-Correlation-ID"; private readonly ILogger _logger; public LoggingMiddleware(RequestDelegate next, ILoggerFactory loggerFactory, RecyclableMemoryStreamManager recyclableMemoryStreamManager) { _next = next; _logger = loggerFactory.CreateLogger<LoggingMiddleware>(); _recyclableMemoryStreamManager = recyclableMemoryStreamManager; } public async Task InvokeAsync(HttpContext context) { if (context.WebSockets.IsWebSocketRequest) { return; } var skippable = new[] { HttpMethods.Connect, HttpMethods.Options, HttpMethods.Trace }; if (skippable.Any(x => x.Equals(context.Request.Method, StringComparison.OrdinalIgnoreCase))) { return; } if (context.Request.Path.HasValue && context.Request.Path.Value.ToLower().Contains("swagger")) { await _next(context); return; } string correlationId = null; if (context.Request.Headers.TryGetValue(CorrelationIdHeaderKey, out StringValues correlationIds)) { correlationId = correlationIds.FirstOrDefault(k => k.Equals(CorrelationIdHeaderKey)); } else { correlationId = Guid.NewGuid().ToString(); context.Request.Headers.Add(CorrelationIdHeaderKey, correlationId); } context.Response.OnStarting(() => { if (!context.Response.Headers. TryGetValue(CorrelationIdHeaderKey, out correlationIds)) context.Response.Headers.Add(CorrelationIdHeaderKey, correlationId); return Task.CompletedTask; }); await LogRequest(context); await LogResponse(context); } private async Task LogRequest(HttpContext context) { context.Request.EnableBuffering(); using (var requestStream = _recyclableMemoryStreamManager.GetStream()) { await context.Request.Body.CopyToAsync(requestStream); var queryString = context.Request.QueryString.HasValue ? $"?{context.Request.QueryString.Value}" : ""; _logger.LogInformation($">> {context.Request.Scheme}://{context.Request.Host}{context.Request.PathBase}{context.Request.Path}{queryString} headers: {string.Join("; ", context.Request.Headers.Select(x => $"{x.Key}: {x.Value}"))} Body: {ReadStreamInChunks(requestStream)}"); context.Request.Body.Position = 0; } } private async Task LogResponse(HttpContext context) { var originalBodyStream = context.Response.Body; using (var responseBody = _recyclableMemoryStreamManager.GetStream()) { context.Response.Body = responseBody; await _next(context); context.Response.Body.Seek(0, SeekOrigin.Begin); var text = await new StreamReader(context.Response.Body).ReadToEndAsync(); context.Response.Body.Seek(0, SeekOrigin.Begin); var queryString = context.Request.QueryString.HasValue ? $"?{context.Request.QueryString.Value}" : ""; var message = $"<< {context.Request.Scheme}://{context.Request.Host}{context.Request.PathBase}{context.Request.Path}{queryString} {context.Response.StatusCode} X-Correlation-ID: {context.Request.Headers["X-Correlation-ID"]} headers: {string.Join("; ", context.Response.Headers.Select(x => $"{x.Key}: {x.Value}"))} Body: {text}"; if (context.Response.StatusCode >= 500) _logger.LogError(message); else if (context.Response.StatusCode >= 400) _logger.LogWarning(message); else _logger.LogInformation(message); await responseBody.CopyToAsync(originalBodyStream); } } private static string ReadStreamInChunks(Stream stream) { const int readChunkBufferLength = 4096; stream.Seek(0, SeekOrigin.Begin); using (var textWriter = new StringWriter()) { using (var reader = new StreamReader(stream)) { var readChunk = new char[readChunkBufferLength]; int readChunkLength; do { readChunkLength = reader.ReadBlock(readChunk, 0, readChunkBufferLength); textWriter.Write(readChunk, 0, readChunkLength); } while (readChunkLength > 0); var body = textWriter.ToString(); return body; } } } }
分析与解决方案
可能的问题根源
- 流管理器配置差异:不同API的
RecyclableMemoryStreamManager注册参数可能不同,默认配置在处理大请求/高并发时,会因块大小、最大缓冲限制不合理,导致流无法及时回收或占用过多内存。 - 请求/响应体规模差异:当前API处理的请求/响应体远大于其他API,中间件会将整个内容加载到内存日志,高流量下内存持续积累。本地测试无大请求场景,因此无法复现。
- 默认缓冲流的产生:
context.Request.EnableBuffering()默认会创建FileBufferingReadStream,当请求体超过默认内存阈值(32KB)时会写入磁盘,但并发场景下大量接近阈值的请求会导致内存占用飙升。 - 日志阻塞延迟流释放:如果日志后端写入性能差,会阻塞流的释放逻辑,高并发下流对象被长时间持有,无法回收。
具体修复建议
1. 优化流管理器配置
显式配置参数适配当前API的请求规模:
services.AddSingleton<RecyclableMemoryStreamManager>(_ => new RecyclableMemoryStreamManager( blockSize: 65536, // 64KB块大小 bufferSize: 65536, maxBufferSize: 5242880, // 5MB最大缓冲 maxStreamCapacity: 20971520) // 20MB最大流容量 );
2. 替换默认请求缓冲流
在LogRequest方法中,使用RecyclableMemoryStreamManager创建请求缓冲流,避免默认的FileBufferingReadStream:
// 替换原有的context.Request.EnableBuffering(); context.Request.EnableBuffering(_recyclableMemoryStreamManager);
3. 限制日志内容大小
对过大的请求/响应体进行截断,避免全量加载到内存:
// 在LogResponse中读取响应体后添加截断逻辑 var maxLogBodySize = 10240; // 10KB var text = await new StreamReader(context.Response.Body).ReadToEndAsync(); if (text.Length > maxLogBodySize) { text = text.Substring(0, maxLogBodySize) + "... [内容已截断]"; }
4. 异步处理日志输出
如果使用的日志框架支持异步日志(如Serilog的WriteTo.Async),将日志输出改为异步模式,避免阻塞流的释放:
// 示例:Serilog异步配置 Log.Logger = new LoggerConfiguration() .WriteTo.Async(a => a.Console()) .CreateLogger();
5. 调整中间件顺序
确保日志中间件处于管道最前端(除必要的CorrelationId等中间件),避免后续中间件重复缓冲请求/响应体。
内容的提问来源于stack exchange,提问作者pakerinjo
相关产品推荐
相关产品推荐

