Параллельное добавление в тот же файл с использованием Perl - PullRequest
12 голосов
/ 02 марта 2010

Мне нужно обновить CGI-скрипт Perl, где пользователи должны выполнить 3 шага.После того, как они заканчивают каждый шаг, сценарий регистрирует, какой шаг завершил пользователь.Наличие записи об этом важно, чтобы мы могли доказать пользователю, что он только закончил первый шаг и не выполнил все три шага, например.

В данный момент скрипт создает 1 файл журнала для каждогоэкземпляр скрипта CGI.Таким образом, если пользователь A выполняет шаг 1, то пользователь B выполняет шаг 1, затем шаг 2, затем шаг 3 - и затем пользователь A завершает шаг 2 и шаг 3, порядок файлов журнала будет следующим:

LogFile.UserA.Step1
LogFile.UserB.Step1
LogFile.UserB.Step2
LogFile.UserB.Step3
LogFile.UserA.Step2
LogFile.UserA.Step3

.Файлы журналов имеют текущую временную метку, случайное число и PID процесса.

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

По сути, журналфайл будет иметь текущую дату в имени файла, и каждый раз, когда сценарий CGI должен записывать в журнал, он будет добавляться к одному файлу журнала за этот день, независимо от пользователя или шага, на котором он находится.1011 * Ничего не нужно будет читать файл журнала - единственное, что с ним случится, - это добавление скриптом CGI.Ротация журнала будет выполняться для файлов журнала, которые старше 7 дней.

Мой вопрос: как лучше всего обрабатывать одновременные добавления в этот файл журнала?Нужно ли его заблокировать перед добавлением?Я обнаружил эту страницу на Perl Monks, которая, кажется, указывает на то, что «когда несколько процессов пишут в один и тот же файл, и все они открывают файл для добавления, данные не должны перезаписываться».

Я узнал, что только то, что это можно сделать, не означает, что я должен, но в этом случае, какой самый безопасный и лучший способ сделать это?

Резюме:

  • Параллельное добавление к одному и тому же файлу
  • Каждое добавление к файлу занимает всего одну строку, менее 50 символов
  • Порядок не имеет значения

Спасибо!

Ответы [ 6 ]

13 голосов
/ 02 марта 2010

Да, используйте flock.

Ниже приведен пример программы, начиная с типичного текста:

#! /usr/bin/perl

use warnings;
use strict;

use Fcntl qw/ :flock /;

Затем мы указываем путь к журналу и количество клиентовкоторый запустится:

my $log = "/tmp/my.log";
my $clients = 10;

Чтобы записать сообщение, откройте файл в режиме добавления, чтобы все записи автоматически шли в конце.Затем позвоните flock и дождитесь нашей очереди, чтобы получить эксклюзивный доступ к журналу.Как только мы закончим, напишите сообщение и close дескриптор, который автоматически снимает блокировку.

sub log_step {
  my($msg) = @_;

  open my $fh, ">>", $log or die  "$0 [$$]: open: $!";
  flock $fh, LOCK_EX      or die  "$0 [$$]: flock: $!";
  print $fh "$msg\n"      or die  "$0 [$$]: write: $!";
  close $fh               or warn "$0 [$$]: close: $!";
}

Now fork off $clients дочерние процессы, чтобы пройти все три шага со случайными интерваламимежду:

my %kids;
my $id = "A";
for (1 .. $clients) {
  my $pid = fork;
  die "$0: fork: $!" unless defined $pid;

  if ($pid) {
    ++$kids{$pid};
    print "$0: forked $pid\n";
  }
  else {
    my $user = "User" . $id;
    log_step "$user: Step 1";
    sleep rand 3;
    log_step "$user: Step 2";
    sleep rand 3;
    log_step "$user: Step 3";
    exit 0;
  }

  ++$id;
}

Не забудьте дождаться выхода всех детей:

print "$0: reaping children...\n";
while (keys %kids) {
  my $pid = waitpid -1, 0;
  last if $pid == -1;

  warn "$0: unexpected kid $pid" unless $kids{$pid};
  delete $kids{$pid};
}

warn "$0: still running: ", join(", " => keys %kids), "\n"
  if keys %kids;

print "$0: done!\n", `cat $log`;

Пример вывода:

[...]
./prog.pl: reaping children...
./prog.pl: done!
UserA: Step 1
UserB: Step 1
UserC: Step 1
UserC: Step 2
UserC: Step 3
UserD: Step 1
UserE: Step 1
UserF: Step 1
UserG: Step 1
UserH: Step 1
UserI: Step 1
UserJ: Step 1
UserD: Step 2
UserD: Step 3
UserF: Step 2
UserG: Step 2
UserH: Step 2
UserI: Step 2
UserI: Step 3
UserB: Step 2
UserA: Step 2
UserA: Step 3
UserE: Step 2
UserF: Step 3
UserG: Step 3
UserJ: Step 2
UserJ: Step 3
UserE: Step 3
UserH: Step 3
UserB: Step 3

Имейте в виду, чтопорядок будет отличаться от запуска к запуску.

2 голосов
/ 02 марта 2010

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

flock является надежным и достаточно простым решением этой проблемы. Я бы посоветовал вам просто использовать это.

1 голос
/ 03 марта 2010

Я бы призвал Log :: Log4Perl

0 голосов
/ 02 марта 2010

У вас есть несколько вариантов, в порядке возрастания сложности:

1) Просто отметка времени и даты каждой строки. Когда вам нужно проверить объединенный файл, вы чередуете все входные файлы.

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

3) Напишите скрипт, который принимает TCP-соединения. Если вы когда-нибудь окажетесь в ситуации, когда у регистраторов может быть открыто больше файлов журналов, чем процесс в вашей операционной системе может поддерживать одновременно, вы вернетесь к решению № 1. Честно говоря, перейдите к номеру 1.

0 голосов
/ 02 марта 2010

Я думаю, что я бы запустил отдельный процесс, например, используя Net :: Daemon или аналогичный, который обрабатывает запись записей журнала централизованно. Экземпляры сценария CGI будут передавать строки журнала этому демону через сокет.

0 голосов
/ 02 марта 2010

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

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

...