.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; } }
排查与解决建议
非托管内存泄漏核心排查点
- SmtpClient资源泄漏:尽管代码用
using包裹SmtpClient,但.NET 6的System.Net.Mail.SmtpClient在Linux下可能存在底层非托管资源未释放问题。建议替换为MailKit等第三方邮件库验证是否解决问题。 - Liquid解析引擎:检查Liquid解析依赖是否包含Linux原生库,部分原生库在跨平台环境下可能存在内存泄漏,可临时禁用Liquid解析功能测试内存变化。
- 数据库驱动:
GetPreparedEmail和Query<IEmailQueue>的底层数据库驱动(如ODBC、自定义原生驱动)在Linux下的内存管理是否存在问题,替换为官方托管驱动(如Npgsql、SqlClient)测试。 - 缓存与非托管资源关联:
_emailDomainCache若缓存的IMailDomain对象持有非托管资源,需确认缓存清理逻辑是否完备,避免长期持有导致泄漏。
内存分析实操步骤
- 用
dotnet-dump analyze加载dump文件:- 执行
eeheap -gc确认托管内存占比,排除托管内存泄漏可能 - 执行
!dumpheap -type System.Runtime.InteropServices.SafeHandle,查找未释放的SafeHandle实例,这类对象通常关联非托管资源
- 执行
- 使用dottrace分析非托管内存分配调用栈,定位到具体的非托管代码路径,明确泄漏源头
代码优化(针对提供的邮件服务)
CheckMailDomainIsVerified方法中,val.TxtStatus.Equals(SUCCESS)存在空引用风险,修复为val?.TxtStatus.Equals(SUCCESS) ?? falseGetReadyToSend中的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
相关产品推荐
相关产品推荐

