2010-01-26 60 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