AnyEvent 文件写入加上 logrotate 导致意外的文件大小

AnyEvent file writes plus logrotate lead to unexpected file sizes

我有一个使用 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;

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

If the file was open(2)ed with O_APPEND, the file offset is first set to the end of the file before writing. The adjustment of the file offset and the write operation are performed as an atomic step.

但是您在 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

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

虽然您可以尝试依赖未记录的行为和 monkeypatch 东西并希望它会神奇地工作,但只要您将 AnyEvent::Handle 用于非流文件句柄,您可能会使 运行 陷入问题, 所以我会修复实际的错误。

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

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