Распространенные ошибки при отправке почты
Что ж, можно приступить к использованию электронной почты для отправки извещений. Однако когда мы начнем писать программы для выполнения этой функции, то быстро обнаружим, что вопрос как посылать почту, отнюдь не так интересен, как вопросы когда и что посылать.
В данном разделе мы исследуем эти вопросы, действуя «от противного». Если рассмотреть, что и как не надо посылать, то сами вопросы можно изучить глубже. Так что поговорим об ошибках, допускаемых наиболее часто при написании программ системного администрирования, отправляющих почту.
Слишком частая отправка сообщений
Самая распространенная ошибка - это отправка слишком большого количества сообщений. Иметь сценарии, отправляющие почту, вообще говоря, отличная идея. Если с какой-либо службой случится что-то неприятное, то отправка обычного электронного сообщения или сообщения на пейджер - очень хороший способ привлечь внимание человека к случившейся проблеме. Но в большинстве случаев, раз в пять минут отправлять по сообщению о неприятностях - это очень плохое решение. Слишком усердные почтовые генераторы очень быстро попадают в почтовые фильтры тех, кто должен был их читать. В результате оказывается, что важная почта просто игнорируется.
Контроль над частотой отправки почты
Самый простой способ избежать лишней почты - добавить в программу меры предосторожности, чтобы устанавливать задержку между сообщениями. Если сценарий запущен постоянно, то очень просто запомнить время отправки последнего сообщения:
$last_sent = time;
Если программа запускается один раз в N минут или часов через сгоп в Unix или механизмы планирования задач NT, эту информацию можно переписать в файл, состоящий из одной строки, и считывать его при следующем запуске программы. В подобном случае обязательно обратите внимание на меры предосторожности, перечисленные в главе 1 «Введение».
В зависимости от ситуации можно поэкспериментировать с временем задержки. В этом примере показана экспоненциальная задержка (exponential backoff):
$max = 24*60*60; и максимальная задержка в секундах (1 день)
Sunit = 60;
увеличиваем задержку относительно этого значения (1минута)
# интервал времени, прошедший с момента отправки предыдущего
# сообщения и последняя степень 2, которая использовалась для
# расчета интервала задержки. Созданная нами подпрограмма
# возвращает ссылку на анонимный массив с этой информацией
sub time_closure {
my($stored_sent,$stored_power)=(0,-1); return sub {
(($stored_sent,$stored_power) = @_) if @_; [$stored_sent,$stored_power]; > };
$last_data=&time_closure; # создаем замыкание
ft возвращаем значение "истина" при первом вызове и затем после
# задержки
sub expbackoff {
my($last_sent,$last_power) = @{&$last_data};
# возвращаем true, если это первое наше обращение или если
# текущая задержка истекла с тех пор, как мы спрашивали
последний раз. Если мы возвращаем значение true, мы
запоминаем время последнего утвердительного ответа и
увеличиваем степень двойки, чтобы вычислить задержку.
if (!$last_sent or ($last_sent +
(($unit -.$last_power >= $max) 9
$max : $unit * 2**$last_power) <= time())){
&$last_data(time().++$last„power); return 1;
}
else {
return 0; } >
Подпрограмма expbackoffQ возвращает значение true (1), если нужно отправить сообщение, и false (0), если нет. При первом вызове она возвращает true, а затем быстро увеличивает время задержки до тех пор, пока значение t rue не станет появляться лишь раз в день.
Чтобы сделать программу более интересной, я применил необычную конструкцию под названием замыкание (closure) для хранения времени последней отправки сообщения и последней степени двойки, используемой для расчета задержки. Замыкание используется как способ скрытия важных переменных от остальной программы. В данной маленькой программе это было сделано из любопытства, но польза от такой технологии очень быстро становится очевидной в программах большего размера, где более вероятно, что другой код может случайно перезаписать значения этих переменных. Вот, вкратце, как работают замыкания.
Подпрограмма &time_closure() возвращает ссылку на анонимную подпрограмму, по существу, на небольшой отрывок кода без имени. Позже данная ссылка будет вызывать этот код, используя стандартный синтаксис символических ссылок: &$last_data. Код из анонимной подпрограммы возвращает ссылку на массив, поэтому и используется такая масса знаков пунктуации, чтобы получить доступ к возвращаемым данным:
my($last_sent,$last_power) = @{&$last_data};
Вот и вся тайна, которая скрывается за замыканиями: поскольку ссылка создается в том же блоке, что и переменные $stored_seri: и $sto-red_power (посредством my()), то они схватываются в уникальном контексте. Переменные $stored_sent и $stored_power можно прочитать и изменить только при выполнении кода из этой ссылки. Кроме того, они сохраняют свои значения между вызовами. Например:
создаем замыкание $last_data=&time._closure:
вызываем подпрограмму, устанавливающую значения переменных
&$last_data(1,1);
и пытаемся изменить их за пределами подпрел раммы
$stored__sent - $stored_power = 2:
выводим их текущие значения, используя подпрограмму
print "@{&$last_data}\n":
Результатом выполнения этого кода будет "1 1", хотя и создается впечатление, что в третьей строке были изменены значения переменных $stored_sent и $stored_power. Да, значения глобальных переменных с теми же именами были изменены, но невозможно затронуть копии, защищенные замыканиями.
Можно говорить о переменной из замыкания как о спутнике на орбите планеты. Спутник удерживается гравитацией планеты, так что куда движется планета, туда перемещается и спутник. Позицию спутника можно описать только относительно планеты: чтобы найти спутник, сначала нужно отыскать планету. Каждый раз, когда вы находите планету, там же будет и спутник, на том же месте, где был и прошлый раз. Можно считать, что переменные из замыкания находятся на орбите вокруг ссылки на анонимную подпрограмму, отдельно от вселенной остальной программы.
Но оставим астрофизику в покое и вернемся к рассуждениям об отправке почты. Иногда лучше, чтобы программа вела себя как двухлетний ребенок, жалующийся с течением времени все чаще. Вот еще одна программа, похожая на предыдущий пример. На этот раз со временем увеличивается количество дополнительно посылаемых сообщений. Начинаем мы с отправки сообщений один раз в день, а затем уменьшаем время задержки до тех пор, пока минимальная задержка не станет равной пяти минутам:
$тах = 60*60»24;
максимальная задержка в секундах (1 день)
$min = 60*5; tt минимальная задержка в секундах (5 минут)
$unit = 60; tt уменьшаем задержку относительно этого значения (1 минута)
$start_power = int log($max/$unit)/log(2): # ищем ближайшую степень двойки
sub time_closure {
my($last_sent,$last_power)=(0,$start_power+l); return sub {
(($last_sent, $last_p<wer) = @_) if ё>_; n keep exponent positive
$last_power = ($last_power > 0) 9
$last:_power : 0; [Siast^sent,$last_power]; } };
$last_data=&t ime_clusiire;
n создаем замыкание
возвращаем ггче
при первом вызове и затем после роста
it экспоненты sub exprampup {
my($last_sent,$last_power) = @{&$last_data}.
возвращаем true, если это первое обращение или если
текущая задержка истекла с момента последнего обиащен/я.
Если сообщение отправляется, то мш запоминаем время
последнего ответа и увеличиваем
$min : $unit * 2**$last_power) <= time())){
&$last_data(time(),++$last_power1): return 1;
}
else
{
return 0; } }
В обоих примерах вызывалась дополнительная подпрограмма (&$last_data), которая позволяла выяснить, когда было отправлено последнее сообщение и как вычислялась задержка. Позже, при необходимости изменить программу, такое деление позволит изменить способ хранения состояния. Например, если переписать программу так, чтобы она выполнялась периодически, а не постоянно, то замыкание совсем нетрудно заменить обычной подпрограммой, сохраняющей нужные данные в текстовом файле и потом считывающей их оттуда.
Контролируем количество сообщений
Другая разновидность синдрома «чрезмерной отправки почты» - это проблема «каждый в сети за себя». Если все машины из сети решат послать вам чуточку почты, вы вполне можете пропустить что-то действительно важное в этом потоке сообщений. Было бы лучше, если бы все сообщения отправлялись в центральный репозиторий. А затем в собранном виде почта поступала бы в одном сообщении.
Давайте рассмотрим несколько надуманный пример. Предположим, что каждая машина в сети записывает в разделяемый каталог файл, состоящий из одной строки. Имя каждого файла совпадает с именем машины и в каждом из них хранятся результаты научных вычислений, сделанных прошлой ночью. В файле будет одна строка следующего формата:
имя-узла
удача-или-неудача
количество-завершенных-вычислений
Программа, проверяющая эту информацию и отсылающая результаты, может выглядеть так:
use Mail::Mailer; use Text::Wrap;
tf список машин, отправляющих сообщения
$repolist = "/project/machinelist";
ft каталог, куда они записывают файлы
Srepodir = "/project/reportddir";
it
разделитель файловой системы, используется для переносимости.
Можно было бы использовать модуль File::Spec
$separator= "/";
# отправляем почту "с" этого адреса
$reportfromaddr = "project\@example. corn";
# отправляем почту на этот адрес Sreporttoaddr = "project\@example.com";
tf считываем список машин в хэш. Позже будем вынимать из этого
# хэша по мере доклада машин, оставив только те машины, которые
не принимали участие в действии
open(LIST,$repolist) or die "Невозможно открыть список
$repolist:$!\n"; while(<LIST>){
chomp;
$missing{$_}=1;
$machines++; }
# считываем все файлы из центрального каталога
и замечание: этот каталог должен автоматически очищаться другим
# сценарием
opendir(REPO,Srepodir) or die "Невозможно открыть каталог $repodir:$!\n";
while(defined($statfile=readdir(REPO))){
next unless -f Srepodir.$separator.$statfile;
# открываем каждый файл и считываем информацию о состоянии
open(STAT,$repodir.$separator.$statfile) or die "Невозможно открыть Sstatfile:$!\n";
chornp($report = <STAT>);
($hostname.$result,$details)=spiit(' ',$report,3);
warn " В файле Sstatfile утверждается, что он был сгенерирован машиной
Shostname1 \n" if($hostname ne Sstatfile);
имя узла больше не считается пропущенным
delete $missing{$nostname}; # заполняем значениями хэши
(Sresult eq "success")}
$success{$nostname}=$details;
$succeeded++: } else {
$fail{$hostname}=$details:
$failed++; }
close(STAT); } closedir(REPO);
# создаем информативный заголовок для сообщения if (Ssucceeded == $machines){
Ssubject = "[report] Success: $machines"; }
elsif ($failed == Smachines or scalar keys %missing >= Smachines) {
Ssubject = "[report] Fail: Smachines"; } else
{ ;
Ssubject = "[report] Partial: Ssucceeded ACK, Sfailed NACK".
((%missing) ? ", ".(scalar keys %missing)1." MIA" : ""); }
# создаем объект mailer и заполняем заголовки $type="sendmail";
my Smaller = Mail::Mailer->new($type) or die "Невозможно создать новый объект:$!\п";
$mailer->open({From=>$reportf romaddr,
To=>$reporttoaddr. Subject=>$subject})! or die "Невозможно заполнить объект mailer:$!\n";
» создаем тело сообщения !
print $mailer "Run report from $0 on " . scalar localtime(tine) . "\n":
if (keys %success){
print Smaller "\n==Succeeded==\n";
foreach $hostname (sort keys %success){
print Smaller "$hostname: $success{$hostname}\n":
} } 308
if (keys %fail){
print Smaller "\n==Failed==\n";
foreach Snostname (sort, keys %fail){
print Smaller "Shostname: $fail{$hostname}\n"
} }
if (keys %missing){
print Smaller "\n==Missing==\n";
print Smaller wrap("","".join(" ".sort keys %missing)),"\n"; }
# отправляем сообщение $mailer->close;
Сначала программа считывает список имен машин, участвующих в данном предприятии. Затем, чтобы проверить, встречались ли машины, не поместившие файл в общий каталог, используется хэш, созданный на основе этого списка. Мы открываем каждый файл из данного каталога и выделяем из него информацию о состоянии. Получив результаты, создаем сообщение и отправляем его.
Получается такой отчет:
Date: Wed, 14 Apr 1999 13:06:09 -0400 (EOT)
Message-Id: <199904141706.NAA08780@example.com> Subject: [report]
Partial: 3 ACK, 4 NACK, 1 MIA To: project(s>example. con From: project@example.com
Run report from reportscript on Wed Apr 14 13:06:08 1999
==Succeeded==
barney: computed 23123 oogatrons betty: computed 6745634
oogatrons fred: computed 56344 oogatrons
==Failed==
bambam: computed 0 oogatrons dino: computed 0
oogatrons pebbles: computed 0
oogatrons wilma: computed 0 oogatrons
==Missing== mrslate
Другой способ изучить подобные результаты состоит в том, чтобы создать демон журналов регистрации и посылать отчет от каждой машины через сокет. Сначала взгляните на код для сервера. Он совпадает с кодом из предыдущего примера. Рассмотрим новую программу и обсудим ее важные особенности:
use 10::Socket;
use Text::Wrap: ft используется для создания аккуратного вывода
список машин, посылающих отчеты Srepolist = "/project/machinelist":
номер порта для соединения с клиентами Sserverport = '9967' ;
Sloadmachines: # загружаем список ма^ин
# настраиваем нашу сторону сокета
Sreserver = 10::Socket::INET->new(LocalPort => Sserverport,
Proto => "tcp",
Type => SOCK_STREAM,
Listen => 5,
Reuse => 1) or die "Невозможно настроить сокет на нашей стороне: $!\п";
и начинаем слушать порт в ожидании соединений while(
($connectsock,Sconnectaddr) = $reserver->accept()){
# имя подсоединившегося клиента
Sconnectname = gethostbyaddr((sockaddr_in($connectadar))[1],AF_INЈT):
chomp($report=$connectsock->getline);
($hostname,$result,$details)=split(' ',$report,3);
в если нужно сбросить информацию, выводим готовое к
# отправке сообщение и заново инициализируем все
хэши/счетчики
if (Shostname eq "DUMPNOW'H
&printmail($connectsock);
close($connectsock):
undef %success;
undef %fail:
Ssucceeded = Sfailed = 0:
&loadmachines;
next: }
warn "$connectname говорит, что был сгенерирован $nostnarce' \r-."
if($hostname ne Sconnectnaiie): delete $n;issiP.g{Shostna"rie}:
($result eq "success")!
$success{Shostnare}=$deraiIs:
$succeeded++: / else 1
$fail{$hostrame}=$dera;ls:
$fai!ed++. }
close($connectsock); } close($reserver):
# загружаем список машин из заданного файла sub loadmachines
undef %missing;
undef Smachines;
open(LIST,$repolist) or die "Невозможно открыть список Srepolist:$!\n";
while(<LIST>){
chomp;
$missing{$_}=1;
$machines++; } }
выводим готовое к отправке сообщение. Первая строка - тема,
# последующие строки - тело сообщения sub printmail<
(Ssocket) = $_[0];
if (Ssucceded == $machines){
Ssubject = "[report] Success: Smachines"; }
elsif ($failed == Smachines or scalar keys %missing >= Smachines) {
Ssubject = "[report] Fail: Smachines"; } else {
Ssubject = "[report] Partial: Ssucceeded ACK, Sfailed NACK".
((%missing) ? ", ".(scalar keys %missing)1." MIA" : ""); }
print Ssocket "$subject\n":
print Ssocket "Run report from $0 on ".scalar localtime(time)."\n";
if (keys %success){
print Ssocket "\n==Succeeded==\n";
foreach Shostname (sort keys %success){ print
Ssocket "Shostname: $success{$hostname}\n":
}
}
if (keys %fail){ print Ssocket "\n==Failed==\n":
foreach Shostname (sort keys %fail)< print $socket "Shostname: $fail{$hostname}\n"; } }
if (keys %nissing){ print Ssocket " \n==Missing==\n":
print $socket wrapC"1."" join(" ".sort keys Vrissi^g) ).'"
}
Кроме переноса части кода в отдельные подпрограммы, главное изменение заключается в том, что добавлен код для работы с сетью. Модуль 10: : Socket позволяет без труда открывать и использовать сокеты, которые можно сравнить с телефоном. Сначала нужно установить свою сторону сокета (10: :Socket->new()), как бы включая свой телефон, а затем ждать «звонка» от клиента (10: :Socket»accept()). Программа приостановлена (или «заблокирована») до тех пор, пока не установлено соединение. Когда соединение установлено, запоминается имя подсоединившегося клиента. Затем из сокета считывается строка ввода.
Мы предполагаем, что строка ввода выглядит точно так же, как и строки из отдельных файлов в предыдущем примере. Единственное различие - это загадочное имя узла DUMPNOW. Если это имя встречается, подсоединившемуся клиенту выводится тема и тело готового к отправке сообщения, при этом сбрасываются все счетчики и хэш-таб-лицы. За отправку сообщения, полученного от сервера, ответственен клиент. Теперь посмотрим на пример клиента и узнаем, что он может сделать с этим сообщением:
use 10::Socket;
номер порта для соединения с клиентом
Sserverport = "9967";
и имя сервера
$servername = "reportserver";
ft преобразуем имя в IP-адрес
Sserveraddr = inet_ntoa(scalar gethostbyname($servername));
Sreporttoaddr = "project\@example.com";
Sreportf romaddr = "project\(g>example.com";
Sreserver = 10::Socket::INET->new(PeerAddr => Jserveraddr.
PeerPort => $serverport Proto => "tcp". Type => SCCK^STREAM) or ale
"Невозможно создать сокет -а нашей стороне:: $!\п":
if ($ARGV;;0] re "-ni"){
print Sreserver $ARGV[0]: v else {
use Mail::Mailer;
print Sreserver "DUMPNOW\T;
chomp($subject = <$reserver.>) $body = join("",<$reserver>);
$type="send!rmil";
my Smaller = Mail::Mailer->new($type) or die
"Невозможно создать новый обьект niailer-$' \n";
$mailer->open({
From => $reportfromaddr To => $reporttoaddr, Subject => Ssubject » or die
"Невозможно заполнить объект mailer:$!\n";
print Smaller $body; $mailer->close; }
close($reserver);
Эта программа проще. Сначала открывается сокет с сервером. В большинстве случаев ему передается информация о состоянии (полученная в командной строке как $ARGV[0]) и соединение закрывается. При желании создать клиент-серверную систему регистрации, подобную этой, вероятно, нам пришлось бы перенести данный клиентский код в подпрограмму и вызывать ее из другой, гораздо более крупной.
Если передать сценарию ключ -т, он отправит серверу «DUMPNOW» и прочитает полученную от него строку темы сообщения и тело сообщения. Затем этот вывод передается модулю Mail: : Mailer и отправляется в виде почтового сообщения при помощи той же программы, которую мы видели раньше.
Для ограничения размера примера и для того, чтобы не уходить в сторону от дискуссии, здесь представлен лишь костяк кода для клиента и сервера. В нем нет ни проверки ошибок или ввода, ни управления доступом, ни авторизации (в сети любой, получивший доступ к серверу, может взять с него данные), ни постоянного хранилища данных (а что, если машина не работает?), ни даже мало-мальских мер предосторожности. Мало того, в каждый момент времени можно обрабатывать только один запрос. Если клиент остановится в середине транзакции, мы «влипли». Более изощренные примеры можно найти в книгах «Advanced Perl Programming» (Углубленное программирование на Perl) Шрирама Шринивасана (Sriram Srinivasan) и «Perl Cookbook» («Perl: Библиотека программиста») Тома Кристиансена (Tom Christiansen) и Натана Торкингтона (Nathan Torkington), обе выпущены издательством O'Reilly. Модуль Net:: Daemon Джошена Вьедмана (Jochen Wi-edmann) также поможет создавать более сложные программы-демоны.
Однако пора вернуться к рассмотрению других ошибок, допускаемых в программах для системного администрирования, отправляющих почтовые сообщения.
Пропуск темы сообщения
Строка Subject:- это такая «вещица», которую не стоит пропускать. При автоматической отправке почты можно на лету создавать информативную строку
Subject: для каждого сообщения. Так что практически нет прощения тому, кто отправляет в почтовый ящик сообщения с заголовками:
Super-User File history database merge report
Super-User File history database merge report
Super-User File history database merge report
Super-User File history database merge report
Super-User File history database merge report
Super-User File history database merge report
Super-User File history database merge report
в то время как они могли бы выглядеть так:
Super-User Backup OK, 1 tape, 1.400 GB written.
Super-User Backup OK, 1 tape, 1.768 GB written.
Super-User Backup OK, 1 tape, 2.294 GB written.
Super-User Backup OK, 1 tape, 2.817 GB written.
Super-User Backup OK, 1 tape, 3.438 GB written.
Super-User Backup OK, 3 tapes, 75.40 GB written.
Строка Subject: должна содержать краткую и четкую информацию, описывающую происходящее. Из темы сообщения должно быть очевидно, докладывает ли программа об успехе, неудаче или о чем-то среднем. Незначительные добавочные усилия с лихвой окупаются экономией времени при чтении почты.
Недостаточная информация в теле сообщения
Эта ошибка попадает в ту же категорию, что и предыдущая. Если сценарий собирается сообщать о проблемах или ошибках в почтовых сообщениях, значит сгенерировать какую-то информацию, которая должна там присутствовать. Они сводятся к каноническим вопросам журналистики:
Кто?
Какой сценарий сообщает об ошибке? Добавьте содержимое переменной $0 (если не устанавливали ее явно), чтобы показать полный путь к текущему сценарию. Сообщите о версии сценария, если таковая у него имеется.
Где?
Сообщите что-то о том месте в сценарии, где возникает проблема. Функция caller () из Perl возвращает всю нужную для этого информацию:
замечание: то, что возвращает са!1ег(). может зависеть от
версии Perl, так что обязательно загляните в документацию по perlfunc
(Spackage, $filename, $line, $subroutine, Shasargs,
$wantarray, Sevaltext, $is_require) = caller($frames);
Где $f rames - это количество нужных фреймов на стеке (если вызывались подпрограммы из подпрограмм). Чаще всего вы будете устанавливать $f rames в 1. Вот пример списка, возвращаемого функцией caller() в середине кода для сервера из последнего полного примера:
('main','repserver1,32,'main:iprintmail1,1,undef)
Подобная запись указывает, что сценарий, запущенный из файла repserver в строке 32, находился в пакете main. В этот момент выполнялся код из подпрограммы main: .printmail (у нее есть аргументы, кроме того, она не вызывается в списочном контексте).
Если вы не хотите вручную применять caller(), можете воспользоваться отчетом о проблемах, предоставляемым модулем Carp.
Когда?
Опишите состояние программы в момент возникновения ошибки. К примеру, какой была последняя строка прочтенных данных?
Почему?
Если сумете, ответьте на незаданный читателем вопрос: «Зачем беспокоить меня этим почтовым сообщением?» Ответ может быть очень простым, например: «данные об учетных записях не были полностью обработаны», «DNS-сервер сейчас недоступен» или «в серверной пожар». Это даст читающему представление о предмете разговора (и, возможно, побудит к изучению).
Что?
Ну и наконец, надо сказать о том, что же пошло не так раньше всего. Вот небольшой пример на Perl, который охватывает все эти пункты:
use ext::Wrap;
sub problemreport {
9 $shortcontext - описание проблемы в одной строке
# Susercontext - подробное описание проблемы Получение почты 315
Snextstep - лучшее предположение о том, что делать, чтобы исправить
проблему
my($shortcontext,Susercontext,Snextstep) = @_: my
($filename, $line, Ssubroutine) = (caller(1) )[1,2,3]:
push(@return,"Проблема с Sfilename $shortcortext\n '):
push(@return,"** Сообщение о проблеме с Sfilename ***\n\n"):
push(@return,fill("","","- Проблема: Susercontext") "\r\n")
push(@return,"- Место: строка Sline файла Sfilename в
$subroutine\n\n"); push(@return,"- Произошла: ".scalar localtime(time)."\n\n");
push(@return,"- Дальнейшие действия: $nextstep\n"):
\@return; }
sub fireperson {
Sreport = &problemreport("компьютер горит ", «EOR, «EON);
При составлении отчета загорелась задняя часть компьютера.
Случилось это сразу же после обработки пенсионного плана для ORA. EOR
Пожалуйста, потушите пожар, а потом продолжайте работу. EON
print @{$report}; } &fireperson;
Обращение к &problemreport выведет, начиная с темы сообщения, отчет о проблеме, согласующийся с Mail: : Mailer, как и в предыдущих примерах.
Теперь, разобравшись с отправкой почты, перейдем к другой стороне медали.