【问题标题】:AnyEvent file writes plus logrotate lead to unexpected file sizesAnyEvent 文件写入加上 logrotate 导致意外的文件大小
【发布时间】:2015-06-26 19:25:19
【问题描述】:

我有一个使用 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 一起玩,那就太好了。任何帮助或 cmets 表示赞赏。谢谢!

编辑

根据以下答案,我能够使用提供的 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;

【问题讨论】:

    标签: perl anyevent


    【解决方案1】:

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

    如果文件是open(2)ed 和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
    

    【讨论】:

    • 这太好了,再次感谢 ikegami。我已经在我的程序中实现了你的补丁,它的工作原理和你描述的一样,达到了预期的效果。也感谢您提交错误报告。这真的让我摸不着头脑,非常感谢您的帮助。
    • 请注意,这个答案是错误的,monkeypatching 东西只会隐藏问题。请参阅我的答案以获得解释。
    • @Marc Lehmann,1) AE 肯定错误地调用了SET_FL。 2)我总是建议不要使用猴子补丁,但我不打算提交错误报告,因为我不想和你打交道,所以我损害了我的道德。听起来您无论如何都不打算修复错误? 3) 即使 AE::H 使用非阻塞句柄和库级缓冲,它仍然可以处理 OP 的用例。它甚至可以处理并发 (my $guard = lock_handle($hdl, LOCK_EX, sub { my ($lock, $hdl) = @_; $hdl->push_write($s); $hdl->on_drain(sub{ $lock->unlock(); }); }); )。
    • 1) 我们只有您的断言,没有任何证据。断言事物并不能使它们成为真实。 2)我没有争论你的道德,我反对你的坏建议。不过,问不公平的问题绝对是不道德的。 3) OP 的代码 sometimes 是否会起作用并不重要 - 该错误可能随时会咬人,而您的回答只是糟糕的建议。当然,这是他的选择,但我更喜欢编写正确的代码而不是有时似乎有效的代码,而且当我向他人提供建议时,我更愿意教他们正确的解决方案而不是 hack。
    • 我也认为警告其他人你的错误答案是个好主意,这样其他人就不会落入陷阱并假设使用 AnyEvent::Handle with files 无论如何都是良好的编程技术。它只会引起悲伤。这是负责任的事情,真的。
    【解决方案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::Handle 只会增加开销。

    【讨论】:

    • 如果我理解正确,我应该删除使用 AnyEvent 引用文件 I/O 操作的代码部分,并用内置的 perl I/O 函数替换它们。简单地说,我会在计时器的回调中打开一个文件句柄,写入我的数据,然后关闭文件句柄。因此消除了为此使用 AnyEvent::Handle 的感知需要。这适用于我上面的示例代码,也适用于我的生产程序。这样做的一个副作用是能够放弃 logrotate 中对 copytruncate 的要求。 (有关说明,请参见上面的新代码 sn-p)。谢谢!
    • 是的 - AnyEvent::Handle 背后的想法是允许基于事件的非阻塞 I/O,其中一些外部源将数据推送给您。这个模型对磁盘(甚至网络文件系统)没有意义,你必须明确地请求数据。磁盘通常也有相当有限的 I/O 时间,因此通常不需要通过事件处理磁盘 I/O - 磁盘通常在几十毫秒内交付,网络可能需要数小时或更长时间来处理新数据。如果磁盘 I/O 是一个问题并减慢您的程序,AnyEvent::IO (+IO::AIO) 是正确的模块,或者直接使用 IO::AIO。
    • 否则,内置的 perl I/O 是用于文件的正确方法。在某些情况下,使用 AnyEvent::Handle 会很方便(例如,在处理 STDIN/STDOUT 时,可能是管道、网络套接字或文件,而您不知道是哪一个),这就是为什么AnyEvent::Handle 不会拒绝使用这些文件句柄,但通常,您应该只在句柄可能是套接字、管道或其他流式源的情况下使用 AnyEvent::Handle。在这些情况下,它也可以正常处理文件。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-07-31
    • 2014-02-25
    • 1970-01-01
    • 2018-02-25
    • 1970-01-01
    • 1970-01-01
    • 2015-04-09
    相关资源
    最近更新 更多