使用 Perl 并发追加到同一个文件

Posted

技术标签:

【中文标题】使用 Perl 并发追加到同一个文件【英文标题】:Concurrent appends to the same file using Perl 【发布时间】:2010-03-02 18:28:44 【问题描述】:

我需要升级一个 Perl CGI 脚本,用户必须完成 3 个步骤。在他们完成每个步骤后,脚本会记录用户完成的步骤。记录这一点很重要,这样我们就可以向用户证明他们只完成了第一步,并没有完成所有三个步骤。

现在,该脚本正在为 CGI 脚本的每个实例创建 1 个日志文件。因此,如果 UserA 执行第 1 步,然后 UserB 执行第 1 步,然后执行第 2 步,然后第 3 步 - 然后 UserA 完成第 2 步和第 3 步,则日志文件的顺序将是。

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

日志文件以当前时间戳、随机数和进程 PID 命名。

这可以很好地防止同一个文件被多次写入,但目录很快就会获得数千个文件(每个文件只包含几个字节)。有一个过程可以轮换和压缩这些日志,但我不得不这样做,因此脚本每天只记录一个文件,以减少创建的日志文件的数量。

基本上,日志文件的文件名中会包含当前日期,并且每当 CGI 脚本需要写入日志时,它都会附加到当天的一个日志文件中,而不管用户或他们执行的步骤如何正在开启。

不需要读取日志文件 - 唯一会发生的事情是 CGI 脚本的追加。日志轮换将在 7 天或更早的日志文件上运行。

我的问题是,处理并发追加到此日志文件的最佳方法是什么?我需要在追加之前锁定它吗?我在 Perl Monks 上发现 this page 似乎表明“当多个进程正在写入同一个文件,并且所有进程都打开文件进行追加时,不应覆盖数据。”

我了解到,仅仅因为可以做到并不意味着我应该这样做,但在这种情况下,最安全、最佳实践的方法是什么?

总结:

并发追加到同一个文件 对文件的每次追加只有一行,少于 50 个字符 顺序无关紧要

谢谢!

【问题讨论】:

【参考方案1】:

是的,使用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: $!";

现在fork 关闭$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:收割孩子... ./prog.pl:完成! 用户A:步骤1 用户 B:步骤 1 用户 C:步骤 1 用户 C:第 2 步 用户 C:步骤 3 用户 D:步骤 1 用户 E:第 1 步 用户F:步骤1 用户 G:步骤 1 用户 H:第 1 步 用户 I:步骤 1 用户J:第1步 用户 D:步骤 2 用户 D:第 3 步 用户F:步骤2 用户 G:第 2 步 用户 H:第 2 步 用户 I:步骤 2 用户 I:步骤 3 用户 B:步骤 2 用户A:步骤2 用户A:步骤3 用户E:步骤2 用户 F:第 3 步 用户 G:步骤 3 用户J:第2步 用户J:第3步 用户 E:第 3 步 用户 H:步骤 3 用户 B:第 3 步

请记住,每次运行的顺序会有所不同。

【讨论】:

gbacon 这样做是正确的,但在调整他的代码时要记住一些重要的事情:你不要解锁 (LOCK_UN) 文件——你关闭它。这将确保数据被刷新并然后解锁它。 感谢 gbacon。顺序并不重要,所以这不是问题。我不完全确定我是否需要分叉。由于这是一个 CGI 脚本(不是快速 CGI - 它不会保持活动状态),因此用户在脚本的生命周期内只能执行 1 个步骤 - 一旦他完成一个步骤,脚本将退出。然后,在网络上,他正在执行第 2 步,点击提交,第 2 步将被记录,脚本退出。 @BrianH 我不清楚:分叉的孩子模拟多个并发客户端。在您的 CGI 程序中,从我的回答中调用类似于 log_step 的子程序,以记录真实用户完成的步骤。 @hobbs 是的,用close 解锁是最好的,但是Perl 也试图通过在锁定或解锁之前自动刷新来提供帮助。这种行为自 5.004 以来一直存在:perldoc.perl.org/perl5004delta.html#flock @gbacon - 哦,好吧 - 我明白你现在在做什么。好吧,感谢您付出额外的努力来演示并发日志记录的工作原理! :)【参考方案2】:

“当多个进程正在写入同一个文件,并且所有进程都打开文件进行追加时,数据不应被覆盖”可能是真的,但这并不意味着你的数据不会被破坏(一个条目内另一个条目)。对于少量数据,这种情况不太可能发生,但可能发生。

flock 是解决该问题的可靠且相当简单的解决方案。我建议你简单地使用它。

【讨论】:

【参考方案3】:

我会敦促 Log::Log4Perl

【讨论】:

详细说明您的建议【参考方案4】:

您可以尝试使用文件锁定,但这会很快将您带入伤害之地。更简单的方法是使用一个小的持久性进程或一个 cron 作业来扫描您的日志文件目录并将事件一次附加到日志文件中。

为了更加安全,您可以让您的日志记录脚本每隔一段时间(比如 5 分钟)创建新的日志文件,并让您的守护进程忽略小于 5 分钟的文件。

【讨论】:

【参考方案5】:

我认为我会运行一个单独的进程,例如使用 Net::Daemon 或类似的,它以集中方式处理日志条目的写入。 CGI 脚本实例会通过套接字将日志字符串传递给该守护进程。

【讨论】:

【参考方案6】:

您有几个选项,按复杂度递增的顺序排列:

1) 每行只有时间和日期戳。当您需要检查合并后的文件时,您将交错所有输入文件。

2) 编写一个一直运行的脚本,保持所有文件句柄处于打开状态,并使用 select() 查找具有新数据的文件,并按照接收到的顺序将其转储到输出中。此方法可能会占用资源,因为它会不断调用 select,然后查找新文件,然后打开新文件,然后再次调用 select。

3) 编写一个接受 TCP 连接的脚本。如果您最终遇到记录器可以打开的日志文件比操作系统中的进程一次支持的更多的情况,那么您将回到解决方案 1。老实说,选择 1。

【讨论】:

以上是关于使用 Perl 并发追加到同一个文件的主要内容,如果未能解决你的问题,请参考以下文章

并发追加到文件:写入丢失

Perl Inotify 不响应追加(即 echo 'test' >> 文件)

使用 CancellationToken.None 使用 ConcurrentAppendAsync 并发追加内容时的 TaskCanceledException

使用 R 将行追加到 .csv 文件

管道和追加的基本使用

perl语言