4

我有一个使用 AnyEvent 频繁写入文件的脚本。我编写了以下示例来说明我面临的问题。

#!/usr/bin/perl

use strict;
use warnings;

use AnyEvent;
use AnyEvent::Handle;

my $outputFile = 'out_test.log';
open my $out, ">>", $outputFile or die "Can't open output\n";

my $data = "test string"x50000 . "\n";

my $out_ready = AnyEvent->condvar;
my $out_hdl; $out_hdl = AnyEvent::Handle->new(
    fh => $out,
    on_error => sub {
        my ($hdl, $fatal, $msg) = @_;
        AE::log error => $msg;
        $hdl->destroy;
        $out_ready->send;
    }
);

my $timer = AnyEvent->timer(
    after => 0,
    interval => 5,
    cb => sub {
        $out_hdl->push_write($data);
    }
);

$out_ready->recv;

这很好用,但一段时间后文件大小会变得很大。我们使用 logrotate 来解决此类问题,因此我创建了以下 logrotate 配置文件。

/path/to/out_test.log {
        size 2M
        copytruncate
        rotate 4
}

这也很有效,只要上述输出文件超过 2M,它就会旋转到 out_test.log.1。但是,当轮换后立即写入 out_test.log 时,文件大小与轮换后的日志文件相同。这里解释了这种行为和我遇到的情况:https ://serverfault.com/a/221343

虽然我理解这个问题,但我不知道如何解决我提供的示例 Perl 代码中的问题。

我不必通过 logrotate 实现日志轮换,但它会是首选。如果在脚本中实现起来很简单,我可以这样做,但如果我可以让上面的示例与 logrotate 一起玩就好了。任何帮助或意见表示赞赏。谢谢!

编辑

根据下面的答案,我能够使用提供的 monkeypatch ikegami 以及按照 Marc Lehmann 的建议利用本机 perl I/O。我的示例代码看起来像这样并且运行良好。此外,这消除了 logrotate 中对 copytruncate 指令的要求。

#!/usr/bin/perl

use strict;
use warnings;

use AnyEvent;
use AnyEvent::Handle;

my $outputFile = 'out_test.log';
open my $out, ">>", $outputFile or die "Can't open output\n";

my $data = "test string"x50000 . "\n";

my $cv = AnyEvent::condvar();
my $timer = AnyEvent->timer(
    after => 0,
    interval => 5,
    cb => sub {
        open my $out, ">>", $outputFile or die "Can't open output\n";
        print $out $data;
        close $out; 
    }
);

$cv->recv;
4

2 回答 2

2

通常,写入为追加句柄打开的句柄首先查找文件末尾。

如果文件是open(2)ed with O_APPEND,则文件偏移量在写入之前首先设置为文件末尾。文件偏移量的调整和写入操作作为原子步骤执行。

但是您在 AnyEvent::Handle 中看不到这一点。下面演示了这个问题:

$ perl -e'
   use strict;
   use warnings;

   use AE               qw( );
   use AnyEvent::Handle qw( );

   sub wait_for_drain {
      my ($hdl) = @_;
      my $drained = AE::cv();
      $hdl->on_drain($drained);
      $drained->recv();
   }


   my $qfn = "log";
   unlink($qfn);

   open(my $fh, ">>", $qfn) or die $!;
   $fh->autoflush(1);

   my $hdl = AnyEvent::Handle->new(
      fh => $fh,
      on_error => sub {
         my ($hdl, $fatal, $msg) = @_;
         if ($fatal) { die($msg); } else { warn($msg); }
      },
   );

   $hdl->push_write("abc\n");
   $hdl->push_write("def\n");
   wait_for_drain($hdl);
   print(-s $qfn, "\n");

   truncate($qfn, 0);
   print(-s $qfn, "\n");

   $hdl->push_write("ghi\n");
   wait_for_drain($hdl);
   print(-s $qfn, "\n");
'
8
0
12

虽然以下说明了您应该看到的行为:

$ perl -e'
   use strict;
   use warnings;

   my $qfn = "log";
   unlink($qfn);

   open(my $fh, ">>", $qfn) or die $!;
   $fh->autoflush(1);

   print($fh "abc\n");
   print($fh "def\n");
   print(-s $qfn, "\n");

   truncate($qfn, 0);
   print(-s $qfn, "\n");

   print($fh "ghi\n");
   print(-s $qfn, "\n");
'
8
0
4

问题是 AnyEvent::Handle 破坏了一些句柄的标志。上面的 AnyEvent 代码归结为以下内容:

$ perl -e'
   use strict;
   use warnings;

   use Fcntl qw( F_SETFL O_NONBLOCK );

   my $qfn = "log";
   unlink($qfn);

   open(my $fh, ">>", $qfn) or die $!;
   $fh->autoflush(1);

   fcntl($fh, F_SETFL, O_NONBLOCK);

   print($fh "abc\n");
   print($fh "def\n");
   print(-s $qfn, "\n");

   truncate($qfn, 0);
   print(-s $qfn, "\n");

   print($fh "ghi\n");
   print(-s $qfn, "\n");
'
8
0
12

以下是 AnyEvent::Handle 应该做的事情:

$ perl -e'
   use strict;
   use warnings;

   use Fcntl qw( F_GETFL F_SETFL O_NONBLOCK );

   my $qfn = "log";
   unlink($qfn);

   open(my $fh, ">>", $qfn) or die $!;
   $fh->autoflush(1);

   my $flags = fcntl($fh, F_GETFL, 0)
      or die($!);

   fcntl($fh, F_SETFL, $flags | O_NONBLOCK)
      or die($!);

   print($fh "abc\n");
   print($fh "def\n");
   print(-s $qfn, "\n");

   truncate($qfn, 0);
   print(-s $qfn, "\n");

   print($fh "ghi\n");
   print(-s $qfn, "\n");
'
8
0
4

我已经提交了一个错误报告,但是模块的作者不愿意修复这个错误,所以我不得不推荐猴子补丁这种相当糟糕的做法。将以下内容添加到您的程序中:

use AnyEvent       qw( );
use AnyEvent::Util qw( );
use Fcntl          qw( );

BEGIN {
   if (!AnyEvent::WIN32) {
      my $fixed_fh_nonblocking = sub($$) {
         my $flags = fcntl($_[0], Fcntl::F_GETFL, 0)
             or return;

         $flags = $_[1]
            ? $flags | AnyEvent::O_NONBLOCK
            : $flags & ~AnyEvent::O_NONBLOCK;

         fcntl($_[0], AnyEvent::F_SETFL, $flags);
      };

      no warnings "redefine";
      *AnyEvent::Util::fh_nonblocking = $fixed_fh_nonblocking;
   }
}

使用此修复程序,您的程序将正常运行

$ perl -e'
   use strict;
   use warnings;

   use AE               qw( );
   use AnyEvent         qw( );
   use AnyEvent::Handle qw( );
   use AnyEvent::Util   qw( );
   use Fcntl            qw( );

   BEGIN {
      if (!AnyEvent::WIN32) {
         my $fixed_fh_nonblocking = sub($$) {
            my $flags = fcntl($_[0], Fcntl::F_GETFL, 0)
                or return;

            $flags = $_[1]
               ? $flags | AnyEvent::O_NONBLOCK
               : $flags & ~AnyEvent::O_NONBLOCK;

            fcntl($_[0], AnyEvent::F_SETFL, $flags);
         };

         no warnings "redefine";
         *AnyEvent::Util::fh_nonblocking = $fixed_fh_nonblocking;
      }
   }

   sub wait_for_drain {
      my ($hdl) = @_;
      my $drained = AE::cv();
      $hdl->on_drain($drained);
      $drained->recv();
   }


   my $qfn = "log";
   unlink($qfn);

   open(my $fh, ">>", $qfn) or die $!;
   $fh->autoflush(1);

   my $hdl = AnyEvent::Handle->new(
      fh => $fh,
      on_error => sub {
         my ($hdl, $fatal, $msg) = @_;
         if ($fatal) { die($msg); } else { warn($msg); }
      },
   );

   $hdl->push_write("abc\n");
   $hdl->push_write("def\n");
   wait_for_drain($hdl);
   print(-s $qfn, "\n");

   truncate($qfn, 0);
   print(-s $qfn, "\n");

   $hdl->push_write("ghi\n");
   wait_for_drain($hdl);
   print(-s $qfn, "\n");
'
8
0
4
于 2015-06-26T21:26:57.527 回答
2

ikegamis 的回答非常具有误导性 - 您的代码包含一个错误,即使用 AnyEvent::Handle 进行文件 I/O,这是未记录且不受支持的行为。ikegami 感知到的“错误”是在非法文件句柄上使用 AnyEvent::Handle 造成的。

虽然您可以尝试依赖未记录的行为和猴子补丁,并希望它能够神奇地工作,但只要您将 AnyEvent::Handle 用于非流文件句柄,您可能会一直遇到问题,所以我会建议修复实际的错误。

如果您想做基于事件的文件 I/O,那么您应该查看 AnyEvent::IO(并安装合适的后端,例如 IO::AIO)。否则,您应该使用普通的 perl I/O 函数(内置、IO:: 类等)来访问文件。

更新:AnyEvent::Handle 对文件不起作用的更深层原因是它最终没有意义,因为非阻塞 I/O 的概念不适用于文件,因此使用 AnyEvent::处理只会增加开销。

于 2015-06-28T09:26:59.460 回答