Создание демона Perl, который работает 24/7 и читает из именованных каналов - PullRequest
7 голосов
/ 28 июля 2011

Я пытаюсь сделать анализатор логов, используя perl.Анализатор будет работать 24/7 в фоновом режиме на сервере AIX и считывать из каналов, на которые системный журнал направляет журналы (из всей сети).В основном:

logs from network ----> named pipe A -------->   | perl daemon
                  ----> named pipe B -------->   | * reads pipes
                  ----> named pipe c -------->   | * decides what to do based on which pipe

Так, например, я хочу, чтобы мой демон мог быть настроен на mail root@domain.com все журналы, которые записаны в named pipe C.Для этого я предполагаю, что у демона должен быть хеш (новый для perl, но это похоже на соответствующую структуру данных), который можно было бы изменить на лету и сказать ему, что делать с каждым каналом.

Возможно ли это?Или я должен создать файл .conf в /etc для хранения информации.Примерно так:

namedpipeA:'mail root@domain.com'
namedpipeB:save:'mail user@domain.com'

Таким образом, получение чего-либо из A будет отправлено на root@domain.com, а все, начиная с B, будет сохранено в файле журнала (как обычно) И будетотправлено на user@domain.com

Поскольку я впервые использую Perl и впервые создаю демон, могу ли я сделать это, придерживаясь принципа KISS ?Кроме того, есть ли какие-либо соглашения, которые я должен придерживаться?Если бы вы ответили на мой недостаток знаний, это было бы очень полезно.

Ответы [ 2 ]

18 голосов
/ 30 июля 2011

Я рассмотрю часть вашего вопроса: как написать долгосрочную Perl-программу, которая работает с IO.

Самый эффективный способ написать программу на Perl, которая обрабатывает много одновременных операций ввода-вывода, - это использовать цикл обработки событий. Это позволит нам писать обработчики для событий, таких как «строка появилась в именованном канале» или «электронная почта была успешно отправлена» или «мы получили SIGINT». Важно, что это позволит нам составить произвольное количество этих обработчиков событий в одной программе. Это означает, что вы можете выполнять многозадачность, но при этом легко распределять состояние между задачами.

Мы будем использовать AnyEvent framework. Это позволяет нам писать обработчики событий, называемые наблюдателями, которые будут работать с любым циклом событий, поддерживаемым Perl. Возможно, вам все равно, какой цикл обработки событий вы используете, поэтому эта абстракция, вероятно, не имеет значения для вашего приложения. Но это позволит нам повторно использовать предварительно написанные обработчики событий, доступные в CPAN; AnyEvent :: SMTP для обработки электронной почты, AnyEvent :: Subprocess для взаимодействия с дочерними процессами, AnyEvent :: Handle для работы с каналами и т. Д.

Базовая структура демона на основе AnyEvent очень проста. Вы создаете несколько наблюдателей, входите в цикл обработки событий и ... вот и все; система событий делает все остальное. Для начала напишем программу, которая будет выводить «Hello» каждые пять секунд.

Начнем с загрузки модулей:

use strict;
use warnings;
use 5.010;
use AnyEvent;

Затем мы создадим наблюдатель времени или «таймер»:

my $t = AnyEvent->timer( after => 0, interval => 5, cb => sub {
    say "Hello";
});

Обратите внимание, что мы назначаем таймер переменной. Это поддерживает таймер, пока $t находится в области видимости. Если мы скажем undef $t, таймер будет отменен, и обратный вызов никогда не будет вызван.

О обратных вызовах, это sub { ... } после cb =>, и именно так мы обрабатываем события. Когда происходит событие, вызывается обратный вызов. Мы делаем свое дело, возвращаемся, и цикл обработки событий продолжает вызывать другие обратные вызовы по мере необходимости. Вы можете делать все, что захотите в обратных вызовах, включая отмену и создание других наблюдателей. Только не делайте блокирующий вызов, как system("/bin/sh long running process") или my $line = <$fh> или sleep 10. Все, что блокирует, должно быть сделано наблюдателем; в противном случае цикл обработки событий не сможет запускать другие обработчики в ожидании завершения этой задачи.

Теперь, когда у нас есть таймер, нам просто нужно войти в цикл обработки событий. Как правило, вы выбираете цикл обработки событий, который хотите использовать, и вводите его особым образом, описанным в документации цикла обработки событий. EV хороший, и вы вводите его, звоня EV::loop(). Но мы позволим AnyEvent принять решение о том, какой цикл событий использовать, написав AnyEvent->condvar->recv. Не волнуйтесь, что это делает; это идиома, которая означает «войти в цикл обработки событий и никогда не возвращаться». (Вы увидите много о условных переменных или condvars, когда будете читать о AnyEvent. Они хороши для примеров в документации и в модульных тестах, но вы действительно не хотите использовать их в своей программе. Если вы Вы используете их в файле .pm, вы делаете что-то очень неправильное. Поэтому просто притворитесь, что они пока не существуют, и вы с самого начала напишите чрезвычайно чистый код. многих авторов CPAN!)

Итак, просто для полноты:

AnyEvent->condvar->recv;

Если вы запустите эту программу, она будет выводить «Hello» каждые пять секунд, пока не закончится юниверс, или, что более вероятно, вы убьете ее с помощью элемента управления c. Что в этом хорошего, так это то, что вы можете делать другие вещи за эти пять секунд между выводом «Hello» и делать это просто добавляя больше наблюдателей.

Итак, теперь на чтение из труб. AnyEvent делает это очень легко благодаря модулю AnyEvent :: Handle. AnyEvent :: Handle может подключаться к сокетам или каналам и будет вызывать обратный вызов всякий раз, когда данные доступны для чтения из них. (Он также может выполнять неблокирующие записи, TLS и другие вещи. Но сейчас нас это не волнует.)

Сначала нам нужно открыть трубу:

use autodie 'open';
open my $fh, '<', '/path/to/pipe';

Затем мы оборачиваем его AnyEvent :: Handle.После создания объекта Handle мы будем использовать его для всех операций с этим каналом.Вы можете полностью забыть о $fh, AnyEvent :: Handle будет обрабатывать его непосредственное прикосновение.

my $h = AnyEvent::Handle->new( fh => $fh );

Теперь мы можем использовать $h для чтения строк из канала, когда они станут доступны:

$h->push_read( line => sub {
    my ($h, $line, $eol) = @_;
    say "Got a line: $line";
});

Это вызовет обратный вызов, который напечатает «Got a line», когда станет доступной следующая строка.Если вы хотите продолжить чтение строк, то вам нужно заставить функцию вернуться в очередь чтения, например:

my $handle_line; $handle_line = sub {
    my ($h, $line, $eol) = @_;
    say "Got a line: $line";
    $h->push_read( line => $handle_line );
};
$h->push_read( line => $handle_line );

Это будет читать строки и вызывать $handle_line->() для каждой строки, пока файл не будетзакрыто.Если вы хотите прекратить читать рано, это легко ... просто не повторяйте push_read в этом случае.(Вам не нужно читать на уровне строки; вы можете попросить, чтобы ваш обратный вызов вызывался всякий раз, когда становятся доступны любые байты. Но это сложнее и оставлено читателю в качестве упражнения.)

Так что теперь мыможет связать все это вместе в демон, который обрабатывает чтение каналов.Что мы хотим сделать: создать обработчик для линий, открыть каналы и обработать линии, и, наконец, настроить обработчик сигналов для чистого выхода из программы.Я рекомендую использовать ОО подход к этой проблеме;сделать каждое действие («обрабатывать строки из файла журнала доступа») классом с методами start и stop, создать экземпляр группы действий, настроить обработчик сигнала для чистой остановки действий, запустить все действия, а затемвойти в цикл событий.Это много кода, который не имеет отношения к этой проблеме, поэтому мы сделаем что-нибудь попроще.Но имейте это в виду при разработке вашей программы.

#!/usr/bin/env perl
use strict;
use warnings;
use AnyEvent;
use AnyEvent::Handle;
use EV;

use autodie 'open';
use 5.010;

my @handles;

my $abort; $abort = AnyEvent->signal( signal => 'INT', cb => sub {
    say "Exiting.";
    $_->destroy for @handles;
    undef $abort; 
    # all watchers destroyed, event loop will return
});

my $handler; $handler = sub {
    my ($h, $line, $eol) = @_;
    my $name = $h->{name};
    say "$name: $line";
    $h->push_read( line => $handler );
};

for my $file (@ARGV) {
    open my $fh, '<', $file;
    my $h = AnyEvent::Handle->new( fh => $fh );
    $h->{name} = $file;
    $h->push_read( line => $handler );
}

EV::loop;

Теперь у вас есть программа, которая читает строку из произвольного числа каналов, печатает каждую строку, полученную в любом канале (с префиксом пути ктруба), и выходит чисто, когда вы нажимаете Control-C!

2 голосов
/ 29 июля 2011

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

Учитывая это, как насчет простой передачи данных конфигурации (путь кименованный канал, адрес электронной почты для использования и т. д.) в командной строке, например:

the-daemon --pipe /path/to/named-pipe-A --mailto root@domainA.com
the-daemon --pipe /path/to/named-pipe-B --mailto root@domainB.com
...

Это работает для вас?

Чтобы убедиться, что демоны работают, имейтепосмотрите на пакет вроде daemontools DJ Bernstein или supervisord (задыхайтесь! пакет python).

Каждый из этих пакетов рассказывает, как настроить ваши rc-скрипты так,что они запускаются во время загрузки машины.

...