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

时间:2015-06-26 19:25:19

标签: 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"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;

2 个答案:

答案 0 :(得分: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

答案 1 :(得分:2)

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

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

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

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