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

Perl AnyEvent文件写入加上logrotate会导致意外的文件大小,perl,anyevent,Perl,Anyevent,我有一个脚本,它经常使用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

我有一个脚本,它经常使用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时,文件大小与旋转后的日志文件相同。这里解释了这种行为和我的经历:

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

我不必通过logrotate实现日志旋转,但这是首选。如果在脚本中实现起来很简单,那么我可以做到,但是如果我可以使用logrotate使上面的示例很好地发挥作用,那就太好了。任何帮助或意见都将不胜感激。谢谢

编辑

根据下面的答案,我能够使用monkeypatch ikegami提供的工具,并按照Marc Lehmann的建议利用本地perl I/O。我的示例代码如下所示,运行良好。此外,这消除了logrotate中对copyruncate指令的要求

#!/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;

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

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

但您在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
我已经提交了一个bug报告,但是模块的作者不愿意修复这个bug,所以我不得不推荐一个相当糟糕的猴子补丁实践。将以下内容添加到您的程序中:

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的回答很容易引起误解-您的代码包含一个bug,即对文件I/O使用AnyEvent::Handle,这是未记录且不受支持的行为。ikegami感知到的“bug”是在非法文件句柄上使用AnyEvent::Handle造成的

虽然您可以尝试依赖未记录的行为和monkeypatch,并希望它能神奇地工作,但只要您将AnyEvent::Handle用于非流文件句柄,您可能会继续遇到问题,因此我建议修复实际的bug

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


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

这很好,再次感谢ikegami。我已经在我的程序中实现了你的补丁,它完全按照你所描述的那样工作,并获得了预期的结果。也感谢您提交错误报告。这真的让我摸不着头脑,所以我非常感谢你的帮助。请注意,这个答案是错误的,修补东西只会掩盖问题。请看我的回答以获得解释。@Marc Lehmann,1)AE肯定是错误地调用了
SET\u FL
。2) 我一直建议不要使用monkeypatching,但我的道德观受到了损害,因为我不想和你打交道,所以我不打算提交bug报告。听起来你无论如何都不打算修复这个bug?3) 即使AE::H使用非阻塞句柄和库级缓冲,它仍然可以处理OP的用例。它甚至可以处理并发(
my$guard=lock\u handle(push\u write($s);$hdl->on\u drain(sub{$lock->unlock()});
)。1)我们所拥有的只是你的断言,没有任何证据。断言事物并不能使它们成为事实。2) 我没有反驳你的道德观,我反驳了你的错误建议。尽管如此,提出不公平的问题绝对是不道德的。3) OP的代码有时是否能正常工作并不重要——bug随时都可能出现,而你的答案只是一个糟糕的建议。当然,这是他的选择,但我更喜欢编写正确的代码,而不是有时似乎有效的代码。当我建议别人时,我更喜欢教他们正确的解决方案,而不是黑客。我还认为,提醒别人你的错误答案是个好主意,简单地说,这样其他人就不会掉入陷阱,并认为使用AnyEvent::Handle处理文件在任何方面都是一种很好的编程技术。这只会引起悲伤。如果我理解正确,我应该删除使用AnyEvent引用文件I/O操作的代码部分,并用内置的perl I/O函数替换它们。简单地说,我会在计时器的回调中打开一个文件句柄,写入数据,然后关闭文件句柄。这样就消除了使用AnyEvent::Handle实现这一点的必要性。这在上面的示例代码中起作用,在我的生产程序中也起作用。这样做的一个副作用是能够在logrotate中放弃对copyruncate的要求。(请参阅上面的新代码片段以了解说明)。谢谢是-AnyEvent::Handle背后的思想是允许基于事件的非阻塞I/O,即某些外部源将数据推送到您的位置。这种模型对于磁盘(甚至网络文件系统)没有意义,因为在这些磁盘上,您必须显式地