Perl forking和IPC :: Open2 exec管道

时间:2014-11-19 13:14:33

标签: multithreading perl fork

我每隔5分钟就通过cron运行一个脚本。此脚本从我的环境中收集大量性能指标,并使用它们使用rrdtool更新循环数据库。

目前,我是通过threadsThread::Queue这样做的。我有'收集器'线程和'更新程序'线程:

#!/usr/bin/perl

use strict;
use warnings;

use IPC::Open2;
use Thread::Queue;
use English;

my $update_q = Thread::Queue->new();

sub updater {
    open2( my $rrdtool_response, my $rrdtool, "/usr/bin/rrdtool -" )
        or warn $OS_ERROR;
    while ( my $item = $update_q->dequeue ) {
        my ( $rrd, $data ) = split( /,/, $item );
        print {$rrdtool}
            "update --daemon /tmp/rrdcached.sock $rrd $data\n";
        my $result = <$rrdtool_response>;
        if ( not $result =~ m/^OK/ ) {
            print "$rrd $data $result\n";
            close($rrdtool_response);
            close($rrdtool);
            open2(
                my $rrdtool_response,
                my $rrdtool,
                "/usr/bin/rrdtool -"
            ) or warn $OS_ERROR;
        }
    }
    close($rrdtool_response);
    close($rrdtool);
}


for ( 1 .. $update_threads ) {
    my $thr = threads->create( \&updater );
}

这个更新程序将获得一整套以下类型的字符串:

"/path/to/data/file.rrd,N:1:4:3:2:234:3"; 

(这是rrdtool的更新格式 - N为'now`,冒号分隔的值是要更新的内容。

因为我使用队列,所以它是序列化的,我可以确保运行适当数量的rrdtool实例。

我每隔5分钟就会以这种方式收集大约20,000个指标,而且它的工作原理还可以。

我正在进行一些重写,看看我是否无法使用分叉功能。我本来打算生成多个'rrdtool'更新实例,但我偶然意外地在分叉代码之外做了open2

E.g:

    open2( my $rrdtool_response, my $rrdtool, "/usr/bin/rrdtool -" )
        or warn $OS_ERROR;

    $manager -> start and next 
    while ( my $item = $update_q->dequeue ) {
        my ( $rrd, $data ) = split( /,/, $item );
        print {$rrdtool}
            "update --daemon /tmp/rrdcached.sock $rrd $data\n";
        my $result = <$rrdtool_response>;
        if ( not $result =~ m/^OK/ ) {
            print "$rrd $data $result\n";
            close($rrdtool_response);
            close($rrdtool);
            open2(
                my $rrdtool_response,
                my $rrdtool,
                "/usr/bin/rrdtool -"
            ) or warn $OS_ERROR;
        }
    }


    close($rrdtool_response);
    close($rrdtool);

因为孩子们继承了文件句柄,所以几乎工作了。打印到{$rrdtool}文件句柄有效。在错误时重新打开它也有效。 (你最终得到了额外的rrdtool实例,但是基于错误率的'少数',而不是我试图避免的20,000。

#!/usr/bin/perl

use strict;
use warnings;

use IPC::Open2;
use Parallel::ForkManager;
use English;

my $max_concurrency = 100;

my %metric_subs = (
    "fetch_iops" => \&fetch_io_operations_on,

    #... more here;
);

sub fetch_io_operations_on {
    my ($hostname) = @_;
    my $rrd = "/path/to/data/$hostname/iops.rrd";

    #do some stuff to fetch data
    update_rrd( $rrd, $data );
}

{
    my $rrdtool;
    my $rrd_response;

    sub start_updates {
        open2( my $rrdtool_response, my $rrdtool, "/usr/bin/rrdtool -" )
            or warn $OS_ERROR;
    }

    sub update_rrd {
        my ( $rrd, $data ) = @ARG;
        print {$rrdtool}
            "update --daemon /tmp/rrdcached.sock $rrd $data\n";
        my $result = <$rrdtool_response>;
        if ( not $result =~ m/^OK/ ) {
            print "$rrd $data $result\n";
            close($rrdtool_response);
            close($rrdtool);
            open2(
                my $rrdtool_response,
                my $rrdtool,
                "/usr/bin/rrdtool -"
            ) or warn $OS_ERROR;
        }
    }

    sub end_updates {
        print {$rrdtool} "quit\n";
        close($rrdtool);
        close($rrdtool_response);
    }
}

##main

my $manager = Parallel::ForkManager->new($max_concurrency);

start_updates();
foreach my $host (@list_of_hosts) {
    foreach my $metric ( keys %metric_subs ) {
        $manager->start and next;
        ##parallel bit
        print "Fetching $metric on $host\n";
        &{ $metric_subs{$metric} }($host);
        $manager->finish();
    }
}
end_updates();


$manager->wait_all_children();

这是非常令人满意的 - 因为重启是在一个分叉子项中,如果出现错误,你最终会得到多个rrdtool实例,但它们都是通过rrdcached进行更新,所以这是可以接受的。 ForkManager限制将确保我没有得到荒谬的数字

然而,我开始得到一些“响应”查询被阻止的案例,我意识到我对该过程的IO有效地遭受了竞争条件的影响。我开始尝试通过调试打印来匹配'更新'和'响应',但是......好吧,因为我确信你会意识到因为缓冲,所以没有任何方法可以确保分叉读取响应文件句柄的人也是将匹配更新打印到更新文件句柄的人。

所以 - 如果我想以一种不那么粗制滥造的方式做到这一点 - 在perl中处理exec管道的正确方法是什么,这样我就不会在我的代码中构建竞争条件了?

1 个答案:

答案 0 :(得分:2)

  

在perl中处理exec管道的正确方法是什么,这样我就不会在我的代码中构建竞争条件了?

确保一次只执行一个子项非常容易,但一次只能执行一个子项。这会违反你的多任务处理目的。


您在线程模型中重用了worker,但是P :: FM为每个作业使用了一个新的worker。

因此,您需要将循环外部的初始化代码移动到循环中。

for my $item (...) {
    $pm->start and next;
    open2( my $rrdtool_response, my $rrdtool, "/usr/bin/rrdtool -" )
        or warn $OS_ERROR;
    my ( $rrd, $data ) = split( /,/, $item );
    print { $rrdtool } "update --daemon /tmp/rrdcached.sock $rrd $data\n";
    my $result = <$rrdtool_response>;
    if ( not $result =~ m/^OK/ ) {
        print "$rrd $data $result\n";
        close($rrdtool_response);
        close($rrdtool);
        open2(
            my $rrdtool_response,
            my $rrdtool,
            "/usr/bin/rrdtool -"
        ) or warn $OS_ERROR;
    }

    close($rrdtool_response);
    close($rrdtool)
    $pm->finish();
}

这需要一些清理。

  • rrdtool的整个重新启动不再有用。
  • open2在提供命令时永远不会返回false。
  • 你没有收获你的rrdtool孩子。您需要致电waitpid

for my $item (...) {
    $pm->start and next;
    my $pid = open2( my $rrdtool_response, my $rrdtool, "/usr/bin/rrdtool -" );
    my ( $rrd, $data ) = split( /,/, $item );
    print { $rrdtool } "update --daemon /tmp/rrdcached.sock $rrd $data\n";
    my $result = <$rrdtool_response>;
    print "$rrd $data $result\n"
        if $result !~ /^OK/;

    close($rrdtool_response);
    close($rrdtool)
    waitpid($pid, 0);
    $pm->finish();
}