2010-01-26 61 views
3

这似乎是一个非常流行的问题/问题这些天,但我似乎无法找到解决问题的方法。内存泄漏在C#Windows服务发送电子邮件

我在c#中创建了一个简单的Windows服务用于发送电子邮件。该应用程序工作很好,除了它的内存使用情况。该应用程序的前端是基于Web的,该服务通过在目录中创建的文本文件排队。阅读文本文件后,该服务从MS SQL数据库收集新闻简报信息和电子邮件地址,并开始每4秒发送一封电子邮件。在观察通过任务管理器运行的服务时,可以看到CPU每4秒钟使用一次,但立即降低。另一方面,记忆似乎并不是每一封电子邮件,而是每3-4封邮件增加50-75k。这将继续增加,直到发送所有电子邮件。我刚刚发出约。 2100封电子邮件,内存使用量高达100MB。我注意到的另一件事是,在发送完所有电子邮件之后,内存使用量将保持在此总和,直到我重新启动服务。当服务空闲时,内存运行在大约6500k。任何人有任何建议,我可以如何减少这些内存使用情况,并在完成邮件后处理掉?我的代码如下。任何帮助将不胜感激..

namespace NewsMailer 
{ 
    public partial class NewsMailer : ServiceBase 
    { 
     private FileSystemWatcher dirWatcher; 
     private static string filePath = @"E:\Intranets\Internal\Newsletter\EmailQueue"; 
     private static string attachPath = @"E:\Intranets\Internal\Newsletter\Attachments"; 
     private string newsType = String.Empty; 
     private string newsSubject = String.Empty; 
     private string newsContent = String.Empty; 
     private string userName = String.Empty; 
     private string newsAttachment = ""; 
     private int newsID = 0; 
     private int emailSent = 0; 
     private int emailError = 0; 

     public NewsMailer() 
     { 
      InitializeComponent(); 
     } 

     protected override void OnStart(string[] args) 
     { 
      dirWatcher = new FileSystemWatcher(); 
      dirWatcher.Path = filePath; 
      dirWatcher.Created += new FileSystemEventHandler(ReadText); 
      dirWatcher.EnableRaisingEvents = true; 
     } 

     protected override void OnStop() 
     { 
      dirWatcher.EnableRaisingEvents = false; 
      dirWatcher.Dispose(); 
     } 

     private void ClearVar() 
     { 
      newsType = String.Empty; 
      newsSubject = String.Empty; 
      newsContent = String.Empty; 
      userName = String.Empty; 
      newsAttachment = ""; 
      newsID = 0; 
      emailSent = 0; 
      emailError = 0; 
     } 

     private void ReadText(object sender, FileSystemEventArgs e) 
     { 
      ClearVar(); 
      SetLimits(); 
      string txtFile = filePath + @"\QueueEmail.txt"; 
      StreamReader sr = new StreamReader(txtFile); 
      string txtLine = String.Empty; 

      try 
      { 
       while ((txtLine = sr.ReadLine()) != null) 
       { 
        string[] lineCpl = txtLine.Split('§'); 
        newsType = lineCpl[0]; 
        userName = lineCpl[1]; 
        newsID = Convert.ToInt32(lineCpl[2]); 
       } 
      } 
      catch (IOException ex) 
      { 
       SendExByMail("ReadText() IO Error", ex); 
      } 
      catch (Exception ex) 
      { 
       SendExByMail("ReadText() General Error", ex); 
      } 
      finally 
      { 
       sr.Close(); 
       sr.Dispose(); 
      } 
      GetNews(); 
     } 

     [DllImport("kernel32.dll")] 
     public static extern bool SetProcessWorkingSetSize(IntPtr proc, int min, int max); 

     private void SetLimits() 
     { 
      GC.Collect(); 
      GC.WaitForPendingFinalizers(); 

      if (Environment.OSVersion.Platform == PlatformID.Win32NT) 
       SetProcessWorkingSetSize(Process.GetCurrentProcess().Handle, -1, -1); 

     } 

     private void DeleteText() 
     { 
      try 
      { 
       File.Delete(filePath + @"\QueueEmail.txt"); 
      } 
      catch (IOException ex) 
      { 
       SendExByMail("DeleteText() IO Error", ex); 
      } 
      catch (Exception ex) 
      { 
       SendExByMail("DeleteText() General Error", ex); 
      } 
     } 

     private void GetNews() 
     { 
      string connectionString = ConfigurationManager.ConnectionStrings["contacts"].ConnectionString; 
      SqlConnection conn = new SqlConnection(connectionString); 

      string sqlSELECT = "SELECT newsSubject, newsContents, username, attachment FROM newsArchive " + 
           "WHERE ID = " + newsID; 

      SqlCommand comm = new SqlCommand(sqlSELECT, conn); 

      try 
      { 
       conn.Open(); 
       using (SqlDataReader reader = comm.ExecuteReader()) 
       { 
        while (reader.Read()) 
        { 
         newsSubject = reader[0].ToString(); 
         newsContent = reader[1].ToString(); 
         userName = reader[2].ToString(); 
         newsAttachment = reader[3].ToString(); 
        } 
        reader.Dispose(); 
       } 
      } 
      catch (SqlException ex) 
      { 
       SendExByMail("GetNews() SQL Error", ex); 
      } 
      catch (Exception ex) 
      { 
       SendExByMail("GetNews() General Error", ex); 
      } 
      finally 
      { 
       comm.Dispose(); 
       conn.Dispose(); 
      } 
      DeleteText(); 
      GetAddress(); 
     } 

     private void GetAddress() 
     { 
      string connectionString = ConfigurationManager.ConnectionStrings["contacts"].ConnectionString; 
      SqlConnection conn = new SqlConnection(connectionString); 

      string sqlSELECT = String.Empty; 
      if (newsType == "custom") 
       sqlSELECT = "SELECT DISTINCT email FROM custom"; 
      else 
       sqlSELECT = "SELECT DISTINCT email FROM contactsMain WHERE queued = 'True'"; 

      SqlCommand comm = new SqlCommand(sqlSELECT, conn); 

      try 
      { 
       conn.Open(); 
       using (SqlDataReader reader = comm.ExecuteReader()) 
       { 
        while (reader.Read()) 
        { 
         try 
         { 
          if (CheckEmail(reader[0].ToString()) == true) 
          { 
           SendNews(reader[0].ToString()); 
           Thread.Sleep(4000); 
           emailSent++; 
          } 
          else 
          { 
           SendInvalid(reader[0].ToString()); 
           emailError++; 
          } 
         } 
         catch (SmtpException ex) 
         { 
          SendExByMail("NewsLetter Smtp Error", reader[0].ToString(), ex); 
          emailError++; 
         } 
         catch (Exception ex) 
         { 
          SendExByMail("Send NewsLetter General Error", reader[0].ToString(), ex); 
          emailError++; 
         } 
         finally 
         { 
          UnqueueEmail(reader[0].ToString()); 
         } 

        } 
        reader.Dispose(); 
       } 
      } 
      catch (SqlException ex) 
      { 
       SendExByMail("GetAddress() SQL Error", ex); 
      } 
      catch (Exception ex) 
      { 
       SendExByMail("GetAddress() General Error", ex); 
      } 
      finally 
      { 
       comm.Dispose(); 
       conn.Dispose(); 
      } 

      SendConfirmation(); 
     } 

     private bool CheckEmail(string emailAddy) 
     { 
      bool returnValue = false; 
      string regExpress = @"^[\w-]+(?:\.[\w-]+)*@(?:[\w-]+\.)+[a-zA-Z]{2,7}$"; 

      Match verifyE = Regex.Match(emailAddy, regExpress); 
      if (verifyE.Success) 
       returnValue = true; 
      return returnValue; 
     } 

     private void SendNews(string emailAddy) 
     { 
      string today = DateTime.Today.ToString("MMMM d, yyyy"); 

      using (MailMessage message = new MailMessage()) 
      { 
       SmtpClient smtpClient = new SmtpClient(); 

       MailAddress fromAddress = new MailAddress(""); 

       message.From = fromAddress; 
       message.To.Add(emailAddy); 
       message.Subject = newsSubject; 

       if (newsAttachment != "") 
       { 
        Attachment wusaAttach = new Attachment(attachPath + newsAttachment); 
        message.Attachments.Add(wusaAttach); 
       } 

       message.IsBodyHtml = true; 
       #region Message Body 
       message.Body = ""; 
       #endregion 

       smtpClient.DeliveryMethod = SmtpDeliveryMethod.Network; 
       smtpClient.Host = ""; 
       smtpClient.Credentials = new System.Net.NetworkCredential(""); 

       smtpClient.Send(message); 
       smtpClient.ServicePoint.CloseConnectionGroup(smtpClient.ServicePoint.ConnectionName); 
      } 
     } 

     private void UnqueueEmail(string emailAddy) 
     { 
      string connectionString = ConfigurationManager.ConnectionStrings["contacts"].ConnectionString; 
      SqlConnection conn = new SqlConnection(connectionString); 
      string sqlStatement = String.Empty; 

      if (newsType == "custom") 
       sqlStatement = "UPDATE custom SET queued = 'False' WHERE email LIKE '" + emailAddy + "'"; 
      else 
       sqlStatement = "UPDATE contactsMain SET queued = 'False' WHERE email LIKE '" + emailAddy + "'"; 

      SqlCommand comm = new SqlCommand(sqlStatement, conn); 

      try 
      { 
       conn.Open(); 
       comm.ExecuteNonQuery(); 
      } 
      catch (SqlException ex) 
      { 
       SendExByMail("UnqueueEmail() SQL Error", ex); 
      } 
      catch (Exception ex) 
      { 
       SendExByMail("UnqueueEmail() General Error", ex); 
      } 
      finally 
      { 
       comm.Dispose(); 
       conn.Dispose(); 
      } 
     } 

     private void SendConfirmation() 
     { 
      SmtpClient smtpClient = new SmtpClient(); 

      using (MailMessage message = new MailMessage()) 
      { 
       MailAddress fromAddress = new MailAddress(""); 
       MailAddress toAddress = new MailAddress(); 

       message.From = fromAddress; 
       message.To.Add(toAddress); 
       //message.CC.Add(ccAddress); 
       message.Subject = "Your Newsletter Mailing Has Completed"; 
       message.IsBodyHtml = true; 
       message.Body = "Total Emails Sent: " + emailSent + 
           "<br />Total Email Errors: " + emailError + 
           "<br />Contact regarding email errors if any were found"; 

       smtpClient.Host = ""; 
       smtpClient.Credentials = new System.Net.NetworkCredential(""); 
       smtpClient.Send(message); 
       smtpClient.ServicePoint.CloseConnectionGroup(smtpClient.ServicePoint.ConnectionName); 
      } 
      ClearVar(); 
      System.GC.Collect(); 
     } 

     private void SendInvalid(string emailAddy) 
     { 
      SmtpClient smtpClient = new SmtpClient(); 

      using (MailMessage message = new MailMessage()) 
      { 
       MailAddress fromAddress = new MailAddress(""); 
       MailAddress toAddress = new MailAddress(""); 

       message.From = fromAddress; 
       message.To.Add(toAddress); 
       //message.CC.Add(ccAddress); 
       message.Subject = "Invalid Email Address"; 
       message.IsBodyHtml = true; 
       message.Body = "An invalid email address has been found, please check the following " + 
           "email address:<br />" + emailAddy; 

       smtpClient.Host = ""; 
       smtpClient.Credentials = new System.Net.NetworkCredential(""); 
       smtpClient.Send(message); 
       smtpClient.ServicePoint.CloseConnectionGroup(smtpClient.ServicePoint.ConnectionName); 
      } 
     } 

     private void SendExByMail(string subject, Exception ex) 
     { 
      SmtpClient smtpClient = new SmtpClient(); 

      using (MailMessage message = new MailMessage()) 
      { 
       MailAddress fromAddress = new MailAddress(""); 
       MailAddress toAddress = new MailAddress(""); 

       message.From = fromAddress; 
       message.To.Add(toAddress); 
       //message.CC.Add(ccAddress); 
       message.Subject = subject; 
       message.IsBodyHtml = true; 
       message.Body = "An Error Has Occurred: <br />Exception: <br />" + ex.ToString(); 

       smtpClient.Host = ""; 
       smtpClient.Credentials = new System.Net.NetworkCredential(""); 
       smtpClient.Send(message); 
       smtpClient.ServicePoint.CloseConnectionGroup(smtpClient.ServicePoint.ConnectionName); 
      } 
     } 

     private void SendExByMail(string subject, string body, Exception ex) 
     { 
      SmtpClient smtpClient = new SmtpClient(); 

      using (MailMessage message = new MailMessage()) 
      { 
       MailAddress fromAddress = new MailAddress("", "MailerService"); 
       MailAddress toAddress = new MailAddress(""); 

       message.From = fromAddress; 
       message.To.Add(toAddress); 
       //message.CC.Add(ccAddress); 
       message.Subject = subject; 
       message.IsBodyHtml = true; 
       message.Body = "An Error Has Occurred:<br /><br />" + body + "<br /><br />Exception: <br />" + ex.ToString(); 

       smtpClient.Host = ""; 
       smtpClient.Credentials = new System.Net.NetworkCredential(""); 
       smtpClient.Send(message); 
       smtpClient.ServicePoint.CloseConnectionGroup(smtpClient.ServicePoint.ConnectionName); 
      } 
     } 
    } 
} 

回答

6

System.Net.Mail.Attachment实现IDisposable,所以我要求它Dispose()(使用using()UPDATE:打开MailMessage.Dispose()起来反射器是不调用Dispose任何附件。

此外,调用GC.Collect()实际上可能导致大型对象堆的碎片化。让框架照顾垃圾收集。

您是否试过下载MemProfiler? (他们有一个试用版本,通常在几分钟内付出代价)

+0

MailMessage被处置,并且afaik附件也被处置。 – 2010-01-26 15:52:35

+0

我了解处置附件的建议,但这是该应用程序的一项功能,从未使用过,因为它现在不会影响内存使用情况。 – user1017477 2010-01-26 16:05:33

+0

刚刚在Reflector中打开,你是正确的:处置一个MailMessage也会调用任何附件上的Dispose。 – 2010-01-26 16:06:26

0

由于这是一个相当大量的代码,所以我会使用的方法是评论一些块(一次一个),然后再次运行并观察内存图。例如,您可以对创建电子邮件附件的部分发表评论,或者只评论实际发送的邮件。这可能是识别记忆的最快方式。

希望有所帮助。

吕克

0

这里有几个链接,让你开始使用的WinDbg和!gcroot检测出真正的内存泄漏。这些说明看起来很丑,很痛苦,它可能很乏味,但是很难 - 如果你有内存泄漏!gcroot可以帮助你找到它们。

http://blogs.msdn.com/alikl/archive/2009/02/15/identifying-memory-leak-with-process-explorer-and-windbg.aspx

http://blogs.msdn.com/delay/archive/2009/03/11/where-s-your-leak-at-using-windbg-sos-and-gcroot-to-diagnose-a-net-memory-leak.aspx

将市售的探查可能更容易使用,但我没有与他们的经验。为了将来的参考和读者,这里有一组搜索条款的主题:

find managed memory leaks root 

希望有所帮助。

0

性能分析是一件很难做的事情。你基本上是在收集经验数据并推断没有适当控制的操作行为。

所以,首先,可能没有问题。虽然GarbageCollector [GC]算法是一个黑匣子,但根据我的经验,我已经看到了特定于流程的自适应行为。例如,我注意到GC可能需要一天的时间来分析服务的内存使用情况,并确定合适的垃圾收集策略。

此外,该内存使用情况似乎“高原”将表明您的泄漏不是无界的,并且可能意味着它按设计运行。

话虽如此,你可能仍然有内存问题。也许是泄漏,或者可能只是低效率的内存使用。运行一个分析器并尝试按类型缩小内存消耗。

在类似于你的场景中,我发现我们的应用程序在运行时产生了数千个内联字符串文字[思考日志语句],它们会膨胀第一代和第二代垃圾堆。他们会及时收集,但这是对系统征税。如果您使用大量内联字符串文字,请考虑使用public const stringpublic static readonly string替代。使用conststatic readonly将只为该应用程序的生命创建该文字的一个实例。

解决了这个问题之后,我们在我们的第三方电子邮件客户端周围发现了真正的内存泄漏。虽然我们的自定义代码会在所有情况下打开和关闭电子邮件客户端,但电子邮件客户端仍保留资源我不记得这些是COM资源[这需要明确处置],还是仅仅是一个执行不力的电子邮件客户端,但解决方案是明确调用Dispose。吸取的教训是而不是依靠其他人正确实施Dispose模式,但在可能的情况下明确调用Dispose

希望这有助于

0

我怀疑这是你的问题,但它给了我一个不好的感觉:

try 
{ 
    conn.Open(); 
    comm.ExecuteNonQuery(); 
    ... 
} 
finally 
{ 
    comm.Dispose(); 
    conn.Dispose(); 
} 

我绝对会使用嵌套using语句来代替在这里。因为虽然using语句是try/finally块的语法糖,但嵌套的using语句是嵌套try/finally块的语法糖,这不是这里发生的情况。我怀疑comm.Dispose()是抛出一个异常,但如果它确实,conn.Dispose()永远不会被调用。

另外:是否有一个原因,你在UnqueueEmail创建一个新的SqlConnection对象,而不是从调用它的方法传入它?同样,这可能不是问题的根源。

说了这么多,我在你的情况下做的第一件事就是创建一个包含所有SMTP代码的服务构建,并在运行时监视它的内存使用情况。这是确定问题与数据库还是邮件程序代码相当快的方法。如果问题消失了,接下来我要做的是实现一个模拟SmtpClient类,其中包含服务调用和再次测试的所有方法的截尾版本;它会告诉你问题是在SmtpClient类中,还是在为其构建数据的代码中。这需要花费一个小时左右的时间,并且您将获得有关您现在没有的问题的重要数据。

编辑

通过 “一个模拟SmtpClient类废止方法,” 我的意思是这样的:

public class MockSmtpClient() 
{ 
    public string From { get; set; } 
    public string To { get; set; } 
    public void Send(MailMessage message) { } 
} 

等。然后修改您的程序以创建MockSmtpClient而不是SmtpClient的实例。由于你的程序似乎没有看SmtpClient的任何属性,或者检查任何函数的返回值,或者处理任何事件,所以它应该像在你实现它之前一样执行 - 只有它将不会发送任何邮件。如果它仍然有内存问题,那么你已经取消了SmtpClient作为一个可能的原因。

0

在我看来,你不应该打开阅读器发送电子邮件。我认为你应该尽量保持事物的解耦,因为代码更容易维护,并且更易于阅读。再次打开连接等待4秒,对我来说似乎有点不自然,您应该始终获取所有数据,然后关闭连接。如果从数据库中提取的数据过大,则可以轻松实现分页机制,以便一次获得100个电子邮件。发送后,获得下一个100,等等。

我不会触及GC,除非我真的没有选择,在99%的工作属于.Net框架,所以它应该是透明的程序员大部分的时间。

Luc