3

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

我在 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);
            }
        }
    }
}
4

6 回答 6

6

System.Net.Mail.Attachment实现IDisposable,所以我会调用Dispose()它(使用using()更新:在反射器中打开 MailMessage.Dispose() 确实调用任何附件上的 Dispose。

此外,调用GC.Collect()实际上会导致大对象堆的碎片。让框架负责垃圾收集。

你试过下载MemProfiler吗?(他们有试用版。它通常会在使用几分钟后收回成本!)

于 2010-01-26T15:46:57.090 回答
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-泄漏.aspx

市售的分析器可能更容易使用,但我没有使用它们的经验。为了将来的参考和读者,以下是该主题的一组搜索词:

find managed memory leaks root

希望有帮助。

于 2010-01-26T16:21:35.483 回答
0

Since this is a fairly large amount of code to watch at, the approach I would use is to comment some of the blocks (one at a time), than run again and watch the memory graph. For example, you could comment the part in which you create an attachment to the email, or just comment the actual sending of the message. This probably would be the fastest way to identify where the memory goes.

Hope that helps.

Luc

于 2010-01-26T16:03:37.027 回答
0

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

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

此外,内存使用似乎“平稳”表明您的泄漏不是无限的,并且可能意味着它按设计运行。

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

在与您类似的场景中,我发现我们的应用程序会动态生成数千个内联字符串文字 [think log statements],导致第一代和第二代垃圾堆膨胀。它们会及时被收集,但这对系统造成了负担。如果您使用大量内联字符串文字,请考虑使用public const stringorpublic static readonly string代替它们。使用constorstatic readonly只会在应用程序的生命周期中创建该文字的一个实例。

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

希望这可以帮助,

于 2010-01-26T16:36:09.167 回答
0

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

除非我真的别无选择,否则我不会碰 GC,99% 的这项工作属于 .Net 框架,因此它在大多数情况下对程序员来说应该是透明的。

卢克

于 2010-01-27T07:26:27.343 回答
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了可能的原因。

于 2010-01-26T22:10:54.420 回答