You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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;
            }
        }
    }
}
分析与解决方案

可能的问题根源

  1. 流管理器配置差异:不同API的RecyclableMemoryStreamManager注册参数可能不同,默认配置在处理大请求/高并发时,会因块大小、最大缓冲限制不合理,导致流无法及时回收或占用过多内存。
  2. 请求/响应体规模差异:当前API处理的请求/响应体远大于其他API,中间件会将整个内容加载到内存日志,高流量下内存持续积累。本地测试无大请求场景,因此无法复现。
  3. 默认缓冲流的产生:context.Request.EnableBuffering()默认会创建FileBufferingReadStream,当请求体超过默认内存阈值(32KB)时会写入磁盘,但并发场景下大量接近阈值的请求会导致内存占用飙升。
  4. 日志阻塞延迟流释放:如果日志后端写入性能差,会阻塞流的释放逻辑,高并发下流对象被长时间持有,无法回收。

具体修复建议

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.08 20:59:56