Утечка памяти в C # Windows Сервис для отправки писем - PullRequest
3 голосов
/ 26 января 2010

В наши дни это довольно популярная проблема / вопрос, но я не могу найти решение этой проблемы.

Я создал простую службу Windows в C # для отправки электронных писем. Приложение прекрасно работает, за исключением использования памяти. Внешний интерфейс приложения является веб-интерфейсом, а служба ставится в очередь в виде текстового файла, создаваемого в каталоге. После прочтения текстового файла служба собирает информацию о новостях и адреса электронной почты из базы данных MS SQL и начинает отправлять 1 сообщение каждые 4 секунды. Наблюдая за тем, как служба запускается через диспетчер задач, вы можете видеть, как загрузка процессора увеличивается каждые 4 секунды, но сразу же падает. С другой стороны, кажется, что память увеличивается не на каждое письмо, а на каждые 3-4 письма на 50-75 тыс. Это будет увеличиваться, пока не будут отправлены все электронные письма. Я только что отправил ок. 2100 электронных писем и использование памяти было до 100 МБ. Еще одна вещь, которую я заметил, заключается в том, что после отправки всех электронных писем использование памяти будет оставаться на этом уровне до тех пор, пока я не перезапущу службу. Когда служба работает вхолостую, объем памяти составляет около 6500 КБ. У кого-нибудь есть какие-либо предложения относительно того, как я могу уменьшить использование памяти и избавиться от нее после завершения рассылки? Мой код ниже. Любая помощь будет принята с благодарностью ..

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 ]

6 голосов
/ 26 января 2010

System.Net.Mail.Attachment реализует IDisposable, поэтому я бы назвал Dispose() на нем (используйте using()) ОБНОВЛЕНИЕ : открытие MailMessage.Dispose () вверх в Reflector DOES вызывает Dispose для любых вложений .

Кроме того, вызов GC.Collect() может фактически привести к фрагментации кучи больших объектов. Пусть каркас позаботится о сборке мусора.

Вы пытались загрузить MemProfiler ? (У них есть пробная версия. Обычно она окупается за несколько минут использования!)

0 голосов
/ 27 января 2010

На мой взгляд, вам не следует отправлять электронные письма с открытым читателем. Я думаю, что вы должны стараться держать вещи как можно более отделенными, так как код легче поддерживать и легче читать. Ожидание 4 секунд с вновь открытым соединением кажется мне немного неестественным, вы всегда должны получать все свои данные и затем закрывать свои соединения. Если данные, извлеченные из базы данных, слишком велики, вы можете легко внедрить механизм подкачки, чтобы получить, скажем, 100 электронных писем за раз. Отправив их, получите следующие 100 и т. Д.

Я бы не трогал GC, если бы у меня действительно не было выбора, в 99% эта работа принадлежит .Net Framework, поэтому большую часть времени она должна быть прозрачной для программистов.

Люк

0 голосов
/ 27 января 2010

Я сомневаюсь, что это ваша проблема, но она вызывает у меня плохое предчувствие:

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 в качестве возможной причины.

0 голосов
/ 26 января 2010

Профилирование производительности - сложная задача. Вы в основном собираете эмпирические данные и выводите операционное поведение без надлежащего контроля.

Так что, в первую очередь, не может быть проблемы. Хотя алгоритмы GarbageCollector [GC] являются черным ящиком, по своему опыту я видел адаптивное поведение, специфичное для процесса. Например, я заметил, что сборщик мусора может занять до дня, чтобы проанализировать использование памяти службой и определить подходящую стратегию для сбора мусора.

Кроме того, использование памяти, как представляется, «плато» будет означать, что ваша утечка не является неограниченной, и может означать, что она работает в соответствии с планом.

Сказав это, у вас все еще могут быть проблемы с памятью. Возможно, утечка или просто неэффективное использование памяти. Запустите профилировщик и попробуйте уменьшить потребление памяти по типу.

В сценарии, похожем на ваш, я обнаружил, что наше приложение генерирует тысячи встроенных строковых литералов [логических операторов] на лету, раздувая кучу мусора первого и второго поколения. Со временем они будут собраны, но это обременительно для системы. Если вы используете много строковых литералов, используйте вместо них public const string или public static readonly string. Использование const или static readonly создаст только один экземпляр этого литерала для жизни приложения.

После решения этой проблемы мы обнаружили настоящую утечку памяти вокруг нашего стороннего почтового клиента. В то время как наш пользовательский код открывал и закрывал почтовый клиент при любых обстоятельствах, почтовый клиент сохранял ресурсы. Я не помню, были ли это COM-ресурсы [которые потребовали бы явного удаления], или просто плохо реализованный почтовый клиент, но решение состояло в том, чтобы явно вызывать Dispose. Извлеченный урок состоит в том, что не полагается на других людей для правильной реализации шаблона Dispose, но явно вызывает Dispose, когда это возможно.

Надеюсь, это поможет,

0 голосов
/ 26 января 2010

Вот пара ссылок, с которых вы можете начать использовать 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 голосов
/ 26 января 2010

Поскольку это довольно большой объем кода для просмотра, я бы применил подход, чтобы прокомментировать некоторые блоки (по одному за раз), а затем запустить снова и посмотреть график памяти. Например, вы можете прокомментировать часть, в которой вы создаете вложение в электронное письмо, или просто прокомментировать фактическую отправку сообщения. Это, вероятно, самый быстрый способ определить, куда идет память.

Надеюсь, это поможет.

Люк

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...