C#Windows服务中的内存泄漏用于发送电子邮件

时间:2010-01-26 15:32:02

标签: c# windows memory-leaks service smtpclient

这些似乎是一个非常受欢迎的问题/问题,但我似乎无法找到问题的解决方案。

我在c#中创建了一个简单的Windows服务来发送电子邮件。该应用程序工作得很好,除了它的内存使用情况。应用程序的前端是基于Web的,服务由在目录中创建的文本文件排队。在阅读文本文件后,服​​务从MS SQL db收集新闻稿信息和电子邮件地址,并开始每4秒发送1封电子邮件。在观察通过任务管理器运行的服务时,您可以看到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 个答案:

答案 0 :(得分:6)

System.Net.Mail.Attachment实施IDisposable,因此我会在其上调用Dispose()(使用using()更新:打开MailMessage.Dispose()在Reflector中调用处理任何附件。

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

您是否尝试过下载MemProfiler? (他们有一个试用版。它通常会在使用几分钟内收回成本!)

答案 1 :(得分:0)

由于这是需要观察的相当大量的代码,我将使用的方法是对一些块(一次一个)进行注释,而不是再次运行并观察内存图。例如,您可以评论为电子邮件创建附件的部分,或者仅评论实际发送的邮件。这可能是识别内存去向的最快方法。

希望有所帮助。

吕克

答案 2 :(得分: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

希望有所帮助。

答案 3 :(得分:0)

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

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

此外,内存使用情况似乎“高原”表示您的泄漏不是无限制的,而可能意味着它按设计运行。

话虽如此,您可能仍会遇到内存问题。也许是泄漏,或者可能只是低效的内存使用。运行探查器并尝试按类型缩小内存消耗。

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

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

希望这有帮助,

答案 4 :(得分:0)

我怀疑这是你的问题,但它让我有一种不好的感觉:

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

我绝对会在这里使用嵌套的using语句。因为虽然using语句是try/finally块的语法糖,但嵌套的using语句是嵌套 try/finally块的语法糖,而这不是这里发生了什么。我怀疑comm.Dispose()是否会抛出异常,但如果确实如此,conn.Dispose()永远不会被调用。

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

所有这一切,我在你的情况下做的第一件事是创建一个这个服务的构建,所有的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作为可能的原因消除。

答案 5 :(得分:0)

在我看来,您不应该在打开阅读器的情况下发送电子邮件。我认为您应该尽可能地将事物分离,因为代码更容易维护,更容易阅读。再次打开连接等待4秒似乎对我来说有点不自然,你应该总是得到你所有的数据,然后关闭你的连接。如果从数据库中升级的数据太大,您可以轻松实现分页机制,让我们一次说100封电子邮件。发送后,获得下一个100等等。

除非我真的没有选择,否则我不会触摸GC,99%这个工作属于.Net Framework,因此大多数时候它应该对程序员透明。

吕克