Perl для системного администрирования

         

Проблемы с пространством на диске


Недостаток программ, ведущих полезные и подробные журналы, заключается в том, что для хранения этих данных нужно место на диске. Это касается всех трех операционных систем, рассмотренных в данной книге: Unix, MacOS и Windows NT/2000. Среди них, вероятно, в NT/2000 это вызывает меньше всего проблем, потому что центральный механизм ведения журналов имеет встроенную поддержку автоматического отсечения. В MacOS центрального механизма ведения журналов нет, зато можно запустить несколько серверов, которые с удовольствием выведут в журналы достаточно данных, чтобы заполнить пространство на диске, дай им только такую возможность.

Обычно задача поддержания приемлемого размера для журналов ложится на плечи системного администратора. Большинство производителей Unix предоставляют некий механизм управления размерами журналов вместе с операционной системой, но он часто обслуживает только определенный набор журналов на машине. Как только на машине появляется новая служба, ведущая свой отдельный журнал, возникает необходимость подправить (или даже отбросить) используемый механизм.

Ротация журналов

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

Этот метод позволяет отвести под журналы приемлемое конечное дисковое пространство. Обратите внимание на способ ротации журналов и функции Perl, необходимые для выполнения каждого шага (табл. 9.2).

Таблица 9.2. Способ ротации журналов из Perl







Процесс





Perl

Переименуйте старые журналы, присвоив им следующий номер.

renamed или &File: :Copy: :move() если переносить файлы с одной файловой системы на другую.

Если необходимо, сообщите процессу, создающему файл журнала, о необходимости закрыть текущий файл и приостановить запись до тех пор, пока она не будет разрешена.

kill () для программ, принимающих сигналы, system () или (обратные кавычки), если необходимо вызвать для этого другую программу.

Скопируйте или переместите файлы журналов, которые сейчас использовались, в другой файл.

&File: : Сору для копирования, rename( ), чтобы переименовать (или &File: :Copy: :move() при перемещении с одной файловой системы на другую).

Если необходимо, урежьте текущий файл журнала.

truncate () или open (FILE, "> filename").

Если необходимо, пошлите сигнал процессу о необходимости приостановить запись в журнал.

Шаг 2 из этой таблицы.

При желании сожмите или обработайте скопированный файл.

system( ) или обратные кавычки для запуска программы сжатия или другого программного кода, выполняющего обработку.

Удалите самые старые копии файлов.

stat( ), чтобы выяснить размер файла и даты, unlink( ) для удаления файлов.
На эту тему существует много вариаций. Все, кому не лень, писали собственные сценарии для ротации журналов. Так что не удивительно, что такой модуль существует. Рассмотрим модуль Logf lie: .Rotate Пола Гэмпа (Paul Gampe).

Logfile: : Rotate использует объектно-ориентированный подход для создания нового экземпляра объекта для журнала и для выполнения методов этого экземпляра. Сначала мы создаем новый экземпляр с заданными параметрами (табл. 9.3).

Таблица 9.3. Параметры Logflle::Rotate




Параметр



Назначение

File

Имя файла журнала для ротации

Count (необязательный, по умолчанию: 7)

Число хранимых копий файлов

Gzip (необязательный, по умолчанию: путь, найденный при сборке Perl)

Полный путь к программе сжатия gzlp

Signal

Код, выполняемый после завершения ротации, как в шаге 5 (табл. 9.2)
<


Вот небольшой пример программы, в которой используются эти параметры:

use Logfile: .'Rotate;

Slogfile = new Logfile:;Rotate(

File => "/var/adm/log/syslog", Count => 5,

Gzip => "/usr/local/bin/gzip", Signal => sub (

open PID, "/etc/syslog.pid" or

die "Невозможно открыть pid-фа/.л :$'\n"; chomp($pid = <PID>); close PID;

# сначала надо проверить допустимость kill 'HUP', $pid; } ):

В результате выполнения этого фрагмента программы указанный журнал будет заблокирован и будет подготовлен модуль для ротации данного журнала. После того как этот объект создан, сама ротация журнала не представляет никакого труда:

$logfile->rotate(); undef Slogfile;

Строка undef нужна для того, чтобы убедиться, что файл журнала будет разблокирован после ротации (он заблокирован до тех пор, пока существует объект журнала).

Как говорится в документации, если с модулем работает привилегированный пользователь (например, пользователь root), необходимо кое-что учитывать. Во-первых, Logf lie. : Rotate прибегает к системному вызову для запуска программы gzip, что является потенциальной дырой в безопасности. Во-вторых, подпрограмма Signal должна быть реализована «оборонительным» способом. В предыдущем примере мы не проверяли, что идентификатор процесса, полученный из /etc/sys log.pid, действительно является идентификатором процесса для syslog. Лучше было бы использовать таблицу процессов, о чем мы говорили в главе 4 «Действия пользователей», перед тем как посылать сигнал через kill(). Более подробно советы по «защищенному» программированию приведены в главе 1 «Введение».



Кольцевой буфер



Мы только что рассмотрели традиционный способ ротации журналов для контроля за пространством, занимаемым постоянно растущими журналами. Позвольте представить вам более необычный подход, который вы можете добавить в свою копилку.

Вот обычный сценарий: выполняется отладка сервера, который выводит целый поток данных в журнал. Нас интересует только малая часть всех этих данных, вероятно, только те строки, которые выводятся сервером после выполнения определенных тестов на определенном клиенте. Если сохранять в журнале весь вывод, как обычно, это быстро заполнит жесткий диск. Ротация журналов с нужной частотой при таком количестве выводимых данных замедлит работу сервера. Что же делать?



Я написал программу bigbuffy для решения этой головоломки, применив совершенно незамысловатый подход, bigbuffy считывает построчно поступающие на вход данные. Эти строки сохраняются в кольцевом буфере определенного размера. Когда буфер заполняется, он начинает вновь заполняться с вершины. Этот процесс чтения-записи продолжается до тех пор, пока bigbuffy не получит сигнал от пользователя. Получив сигнал, программа сбрасывает текущее содержимое буфера в файл и возвращается в свой нормальный цикл. На диске же остается лишь «окошко» в потоке данных из журнала, в котором показаны только те данные, которые нужны.

bigbuffy

можно использовать в паре с программой наблюдения за службой (подобной тем, которые можно найти в главе 5 «Службы имен TCP/IP»). Как только наблюдающая программа (монитор) замечает проблему, она может послать сигнал bigbuffy сбросить содержимое буфера на диск. Теперь у нас есть выдержка из журнала, относящаяся

как раз к нужной проблеме (считаем, что буфер достаточно велик и монитор вовремя ее заметил).

Вот упрощенная версия bigbuffy. Этот код длиннее примеров из предыдущей главы, но он не очень сложный. Мы будем его использовать в качестве трамплина для разрешения некоторых важных вопросов, таких как блокировка ввода и безопасность:

Souffsize = 200;

размер кольцевого буфера по умолчанию (строчках) use Getopt::Long;

 анализируем параметры GetOptions("buffsize=i" => \$Duffsize, "dumpfile=s" => \$dumpfile);

устанавливаем обработчик сигнала и инициализируем счетчик &setup;

 простой цикл прочитать строку - сохранить строку

while (<>){

и помещаем строку в структуру данных. Заметьте, мы делаем 8 это сначала, даже если получаем сигнал. Лучше записать 8 лишнюю строчку, чем потерять строку данных, если в 8 процессе сброса данных что-то пойдет не так.

$buffer[$whatline] = $_;

8 куда деть следующую строку? (Swhatline %= $buffsize)++;

8 если получаем сигнал, сбрасываем текущий буфер if ($dumpnow) {



&dodump(); } }

sub setup {

die "ИСПОЛЬЗОВАНИЕ: $0 [--buffsize=<lines>] --dump-ile=<filename>" unless (length($dumpfile));

$SIG{ 'USR1'} = \&di;mpnow; n устанавливаем обработчик

Swhatline = 1; и начальная строка кольцевого буфера

простой обработчик сигнала, который просто устанавливает фла-8 исключения, см. perlipc(l) sub dumpnow {

Sdumpnow = 1;

}

 флаг, существует ли уже файл my(@firststat,@secondstat); п для хранения вывода Istats

Sdumpnow = 0; № сбрасываем флаг и обработчик сигнала $SIG{ 'USR1'} = \&dumpnow;

if (-e Sdumpfile and (! -f Sdumpfile or -1 Sdurripfile)) {

warn "ПРЕДУПРЕЖДЕНИЕ: файл для сброса данных существует и не является, обычным текстовым файлом, пропускаем сброс данных.\п";

return undef; }

# необходимо принять специальные меры предосторожности при

# дописывании. Следующий набор операторов "if" выполняет

# несколько проверок при открытии файла для дописывания if (-e Sdumpfile) {

Sexists = 1;

unless(@firststat = Istat $dumpfile){

warn "Невозможно выяснить состояние Sdumpfile, пропускаем сброс данных.\n";

return undef; } if ($firststat[3] != 1) {

warn "Sdumpfile - жесткая ссылка, пропускаем сброс данных.\n";

return undef; } }

unless (open(DUMPFILE, "$dumpfile")){

warn "Невозможно открыть Sdumpfile для дописывания,

пропускаем сброс данных.\п"; return undef; > if (Sexists) {

unless (@secondstat = Istat DUMPFILE){

warn "Невозможно выяснить состояние открытого файла Sdumpfile,

пропускаем сброс данных.\п"; return undef; }

if ($firststat[0] != $secondstat[0] or

# проверяем номер устройства $firststat[1] != $secondstat[1] or

 проверяем mode $firststat[7] != $secondstat[7]) tt проверяем размеры {

warn "ПРОБЛЕМА БЕЗОПАСНОСТИ: Istats не совпадают,

пропускаем сброс данных,\п"; return undef:

}

Sline = Swhatline;

print DUMPFILE "-".scalar(Iocaltime). C'-"x50)."\n";

do < Проблемы с пространством на диске 357



И если буфер не полный

last unless (defined $buffer[$line]) print DUMPFILE $buffer[$line];

Sline = (Sline == Sbuffsize) 9 1 : $iine+l; } while (Siine '= Swhatline);

close(DUMPFILE):

П проталкиваем активный буфер, чтобы не повторяв данные

# при последующем сбросе их в файл $whatline = 1; Sbuffer = ();

return 1;

}

Подобная программа может найти несколько интересных применений.



Блокировка ввода в программах обработки журналов



Я уже говорил, что это упрощенная версия программы bigbuffy. С упрощением реализации, в особенности на различных платформах, связана неприятная особенность этой версии: во время сброса данных т диск она не может продолжать считывать ввод. Во время сброса буфера программе, посылающей свой вывод bigbuffy, операционная система может дать указание приостановить операции, пока не будет очищен ее буфер вывода. К счастью, сброс данных происходит быстро v. окно, в котором это может произойти, будет очень маленьким, но это все равно неприятно.

Вот два возможных решения этой проблемы:

  • Переписать bigbuffy, используя двойную буферизацию и многозадачность. Вместо одного буфера можно создать два. Во время получения сигнала программа будет записывать журнал во второй буфер до тех пор, пока дочерний процесс или другой поток обрабатывает сброс данных из первого буфера. При получении следующей сигнала буферы вновь меняются местами.


  • Переписать bigbuffy, чтобы разделить чтение и запись при сброс  данных в файл. Самая простая версия этого подхода предполагает что несколько строк записываются в файл каждый раз после про чтения новой строки. Это может оказаться не простым делом, если  журнал «разорван» и не поступает постоянным потоком. Вряд л! кому-то захочется ждать новую строку вывода для того, чтобы можно было сбросить буфер на диск. Так что придется использовать тайм-ауты или некий механизм внутренних часов, чтобы справиться с этой проблемой.


  • Оба этих подхода трудно реализовать так, чтобы они были преносимыми между различными платформами, отсюда и упрощенная версия; приведенная в книге.



    Безопасность в программах, обрабатывающих журналы

    Вы могли заметить, что в bigbuffy операциям открытия файлов вывода и записи в них уделяется внимания больше, чем обычно. Это пример защищенного (оборонительного) стиля программирования, о котором упоминалось уже в разделе «Ротация журналов». Если эта программа предназначена для отладки сервера, почти наверняка она будет запущена привилегированным пользователем. Очень важно продумать все ситуации, которые могут привести к тому, что программой кто-то злоупотребит.

    Например, представьте ситуацию, когда файл, в который выводятся данные, был злонамеренно заменен ссылкой на другой файл. Если наивно открыть и записать данные в этот файл, можно обнаружить, что мы перезаписали какой-нибудь важный файл, например /etc/passwd. Даже если мы проверим файл вывода данных перед самым его открытием, злоумышленник может подменить его перед тем, как мы действительно начнем записывать в него данные. Во избежание таких неприятностей можно использовать такой сценарий:

  • Мы проверяем, существует ли файл, в который выводятся данные. Если да, мы выполняем lstat(), чтобы получить о нем информацию.


  • Открываем файл в режиме до записи.


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


  • Если дописывать данные не надо, то можно открыть временный файл со случайным именем (чтобы его нельзя было угадать заранее) и потом переименовать его.

    Подобные «уловки» необходимы в большинстве Unix-систем, поскольку первоначально Unix создавался без особой заботы о безопасности. Брешь в безопасности, связанная с символическими ссылками, не является проблемой в NT4, т. к. они являются малоиспользуемой частью подсистемы POSIX, не проблема это и в MacOS, поскольку тут не существует понятия «привилегированный пользователь».






    Содержание раздела