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

.NET 6应用在Linux环境下内存占用过高问题求助

.NET 6 Ubuntu 环境下非托管内存泄漏排查与解决思路

问题背景

  • 应用从Windows平台(.NET Framework → .NET Core 5)迁移至Ubuntu .NET 6后,内存占用飙升:原Windows环境仅150-350MB,Linux下长期运行可达6-8GB,且持续增长不释放
  • 内存dump显示90%为非托管内存,服务器部署50个实例后快速耗尽内存导致崩溃
  • Windows下为32位IIS运行,Linux下为64位;无请求场景(夜间)内存仍无回收,应用包含Liquid解析的CMS服务及多个定时托管任务(邮件发送、文件变更检测等)

已尝试无效措施

  • 调整GC环境变量:DOTNET_GCConserveMemory、DOTNET_gcServer,未改善内存问题
  • 已收集分析数据:memory dump、dotnet-gcdump、dottrace、dotnetdump,但缺乏分析经验

邮件发送托管服务代码

public override Task Execute()
{
    if (_isProcessingEmailSender)
        return Task.CompletedTask;
    _isProcessingEmailSender = true;
    return Task.Run(() =>
    {                             
        try
        {
            using (var scope = ServiceActivator.GetScope())
            {
                var dataContext = scope?.ServiceProvider?.GetRequiredService<IDataContext>();
                if (dataContext == null) return;
                var readyToSent = GetReadyToSend(dataContext);
                if (readyToSent.Any())
                {
                    var emailService = scope?.ServiceProvider?.GetRequiredService<IEmailService>();
                    if (emailService == null)
                        return;
                    var messagesToSend = new Queue<IEmailQueue>(readyToSent);
                    SendMessages(messagesToSend, dataContext, emailService);
                }
            }
            _isProcessingEmailSender = false;
        }
        catch (Exception ex)
        {
            _isProcessingEmailSender = false;
            Log.Error(ex, $"Error while sending emails. Error: {ex.Message}");
        }
        finally
        {
            _isProcessingEmailSender = false;
        }
    });
}       

private void SendMessages(Queue<IEmailQueue> messages, IDataContext dataContext, IEmailService emailService)
{
    while (messages.Any())
    {
        var email = messages.Dequeue();
        Send(email, dataContext, emailService);
    }
}
private IList<IEmailQueue> GetReadyToSend(IDataContext dataContext)
{
    var maxAttempts = _treeplSettings.EmailSender.MaxAttemptsCount;
    var updatedDateTime = DateTime.UtcNow.AddSeconds(-15);
    var countLimit = 15;
    var virtualPoints = dataContext.DatabaseProvider.GetPreparedEmail(maxAttempts, updatedDateTime, countLimit);
    if (!virtualPoints.Any())
        return new List<IEmailQueue>();
    return dataContext.Query<IEmailQueue>()
         .Where(e => virtualPoints.Any(vp => e.VirtualPointer.InstanceId == vp)).ToList();           
}
private void Send(IEmailQueue message, IDataContext dataContext, IEmailService emailService)
{
    if (message == null)
    {
        return;
    }
    message.SendAttempts++;
    try
    {
        Log.Information($"Sending email. Email Id {message?.VirtualPointer.Pointer.ToString()}, message to: {message?.To}, message body: {message?.Body}");
        if (TrySend(message.ToMailMessage(), emailService, out string errorMessage, out string sentServer))
        {
            message.State = EmailMessageState.Sent;
            message.SentServer = sentServer;
        }
        else
        {
            message.State = EmailMessageState.Failed;
            message.ErrorMessage = errorMessage;
        }
    }
    catch (Exception ex)
    {
        message.State = EmailMessageState.Failed;
        message.ErrorMessage = ex.Message;
        Log.Error($"Error while sending email. Email Id {message?.VirtualPointer.Pointer.ToString()}, message to: {message?.To}, message body: {message?.Body}");
        throw;
    }
    finally
    {
        dataContext.SerializeAll();
    }
}
public bool TrySend(MailMessage mail, IEmailService emailService, out string errorMessage, out string sentServer)
{
    SmtpSettings settings;
    var isSent = true;
    errorMessage = null;
    sentServer = null;

    if (CheckMailDomainIsVerified(mail.From, emailService))
    {
        settings = _treeplSettings.EmailSender.AwsSmtpSettings;
        sentServer = "AWS";
    }
    else
    {
        settings = _treeplSettings.EmailSender.CustomSmtpSettings;
        sentServer = "CUSTOM";
    }
    using (var client = GetClient(settings))
    {
        try
        {
            client.Send(mail);
        }
        catch (Exception ex)
        {
            errorMessage = ex.Message;
            isSent = false;
            throw new Exception(
                $"Error while sent email. Subject: {mail.Subject}, From: {mail.From}, To: {string.Join(",", mail.To.Select(x => x.Address))}", ex);
        }
    }
    return isSent;
}
private SmtpClient GetClient(SmtpSettings settings)
{
    var client = new SmtpClient(settings.Host, settings.Port)
    {
        Credentials = new NetworkCredential(settings.Username, settings.Password),
        EnableSsl = settings.EnableSsl
    };

    return client;
}
private bool CheckMailDomainIsVerified(MailAddress from, IEmailService emailService)
{
    try
    {
        var domainKey = $"{from.Host}-MailDomain";
        if (_emailDomainCache.TryGetValue(domainKey, out IMailDomain val))
        {
            return val != null && val.TxtStatus.Equals(SUCCESS);
        }
        else
        {
            var domain = emailService.GetIdentityVerificationAttribute(from.Host);
            _emailDomainCache.AddOrUpdate(domainKey, domain, (k, v) => domain);
            return domain != null && val.TxtStatus.Equals(SUCCESS);
        }
    }
    catch
    {
        return false;
    }
}

排查与解决建议

非托管内存泄漏核心排查点

  1. SmtpClient资源泄漏:尽管代码用using包裹SmtpClient,但.NET 6的System.Net.Mail.SmtpClient在Linux下可能存在底层非托管资源未释放问题。建议替换为MailKit等第三方邮件库验证是否解决问题。
  2. Liquid解析引擎:检查Liquid解析依赖是否包含Linux原生库,部分原生库在跨平台环境下可能存在内存泄漏,可临时禁用Liquid解析功能测试内存变化。
  3. 数据库驱动:GetPreparedEmail和Query<IEmailQueue>的底层数据库驱动(如ODBC、自定义原生驱动)在Linux下的内存管理是否存在问题,替换为官方托管驱动(如Npgsql、SqlClient)测试。
  4. 缓存与非托管资源关联:_emailDomainCache若缓存的IMailDomain对象持有非托管资源,需确认缓存清理逻辑是否完备,避免长期持有导致泄漏。

内存分析实操步骤

  1. 用dotnet-dump analyze加载dump文件:
    • 执行eeheap -gc确认托管内存占比,排除托管内存泄漏可能
    • 执行!dumpheap -type System.Runtime.InteropServices.SafeHandle,查找未释放的SafeHandle实例,这类对象通常关联非托管资源
  2. 使用dottrace分析非托管内存分配调用栈,定位到具体的非托管代码路径,明确泄漏源头

代码优化(针对提供的邮件服务)

  • CheckMailDomainIsVerified方法中,val.TxtStatus.Equals(SUCCESS)存在空引用风险,修复为val?.TxtStatus.Equals(SUCCESS) ?? false
  • GetReadyToSend中的Where(e => virtualPoints.Any(vp => e.VirtualPointer.InstanceId == vp))可优化为Join查询,避免多次内存遍历,提升性能同时减少临时对象创建
  • Execute方法中的_isProcessingEmailSender建议用Interlocked.CompareExchange实现原子操作,避免并发场景下的状态错误:
    if (Interlocked.CompareExchange(ref _isProcessingEmailSender, true, false) == true)
        return Task.CompletedTask;
    

内容的提问来源于stack exchange,提问作者Denis Logachov

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.04 03:01:09