【问题标题】:How can I keep memory from exploding when child processes touch variable metadata?当子进程接触变量元数据时,如何防止内存爆炸?
【发布时间】:2010-06-04 15:07:35
【问题描述】:

Linux 使用COW 在fork 后保持低内存使用率,但Perl 5 变量在perl 中的工作方式似乎破坏了这种优化。例如,对于变量:

my $s = "1";

perl 真的在存储:

SV = PV(0x100801068) at 0x1008272e8
  REFCNT = 1
  FLAGS = (POK,pPOK)
  PV = 0x100201d50 "1"\0
  CUR = 1
  LEN = 16

当您在数字上下文中使用该字符串时,它会修改表示数据的 C struct

SV = PVIV(0x100821610) at 0x1008272e8
  REFCNT = 1
  FLAGS = (IOK,POK,pIOK,pPOK)
  IV = 1
  PV = 0x100201d50 "1"\0
  CUR = 1
  LEN = 16

字符串指针本身并没有改变(它仍然是0x100201d50),但现在它位于不同的C 中structPVIV 而不是PV)。我根本没有修改值,但突然间我付出了 COW 成本。有什么方法可以锁定 Perl 5 变量的 perl 表示,以便节省时间(perl 不必将 "0" 转换为 0 第二次)hack 不会伤害我的内存使用?

注意,上面的表示是从这段代码生成的:

perl -MDevel::Peek -e '$s = "1"; Dump $s; $s + 0; Dump $s'

【问题讨论】:

    标签: linux performance perl memory-management


    【解决方案1】:

    到目前为止,我找到的唯一解决方案是确保我强制 perl 执行我期望在父进程中进行的所有转换。你可以从下面的代码中看到,即使这样也只是有点帮助。

    结果:

    Useless use of addition (+) in void context at z.pl line 34.
    Useless use of addition (+) in void context at z.pl line 45.
    Useless use of addition (+) in void context at z.pl line 51.
    before eating memory
    used memory: 71
    after eating memory
    used memory: 119
    after 100 forks that don't reference variable
    used memory: 144
    after children are reaped
    used memory: 93
    after 100 forks that touch the variables metadata
    used memory: 707
    after children are reaped
    used memory: 93
    after parent has updated the metadata
    used memory: 109
    after 100 forks that touch the variables metadata
    used memory: 443
    after children are reaped
    used memory: 109
    

    代码:

    #!/usr/bin/perl
    
    use strict;
    use warnings;
    
    use Parallel::ForkManager;
    
    sub print_mem {
        print @_, "used memory: ", `free -m` =~ m{cache:\s+([0-9]+)}s, "\n";
    }
    
    print_mem("before eating memory\n");
    
    my @big = ("1") x (1_024 * 1024);
    
    my $pm = Parallel::ForkManager->new(100);
    
    print_mem("after eating memory\n");
    
    for (1 .. 100) {
        next if $pm->start;
        sleep 2;
        $pm->finish;
    }
    
    print_mem("after 100 forks that don't reference variable\n");
    
    $pm->wait_all_children;
    
    print_mem("after children are reaped\n");
    
    for (1 .. 100) {
        next if $pm->start;
        $_ + 0 for @big; #force an update to the metadata
        sleep 2;
        $pm->finish;
    }
    
    print_mem("after 100 forks that touch the variables metadata\n");
    
    $pm->wait_all_children;
    
    print_mem("after children are reaped\n");
    
    $_ + 0 for @big; #force an update to the metadata
    
    print_mem("after parent has updated the metadata\n");
    
    for (1 .. 100) {
        next if $pm->start;
        $_ + 0 for @big; #force an update to the metadata
        sleep 2;
        $pm->finish;
    }
    
    print_mem("after 100 forks that touch the variables metadata\n");
    
    $pm->wait_all_children;
    
    print_mem("after children are reaped\n");
    

    【讨论】:

    • 很好,但是你知道当你分配几百 MB 的数据,fork 几个孩子并且孩子会结束时会发生什么吗? GC无论如何都会杀了你。这是一个可悲的故事,但 Perl 只是这种工作的错误工具。我们使用 END {kill 9 $$} 方法部分解决了它,但此时您应该寻找更好的工具;-)
    • GC 不打扰我,真正的代码是基于mod_perl 的,每个孩子都被重复使用了很多次。问题是加载到父级中的配置数据被复制到可能数百个子级中的每一个中,即使子级从未修改它(从 Perl 5 的角度来看,perl 正在处理元数据)。我考虑过的另一个解决方案是将配置数据移到一个单独的进程中,让孩子们通过域套接字与之交谈。
    • 你也可以使用共享内存,速度会更快。
    • @Hynek 如果全局破坏阶段困扰着你,你最好打电话给exec '/bin/true/ 或使用POSIX::_exit()。两者都是跳过对象破坏的记录方法。
    • @Chas in perlfunc under exec "请注意,exec 不会调用您的 END 块,也不会调用您对象中的任何 DESTROY 方法。"在 POSIX 中“它立即退出程序,这意味着缓冲的 I/O 不会被刷新。”并在 perlfunc 下 exit "The exit() 函数并不总是立即退出。它首先调用任何已定义的 END 例程,但这些 END 例程本身可能不会中止退出。同样,任何需要调用的对象析构函数都会在实际调用之前调用退出。如果这是一个问题,你可以调用 POSIX:_exit($status) 来避免 END 和析构函数处理"
    【解决方案2】:

    无论如何,如果您在启动和运行期间避免使用 COW,则不应忘记生命周期的 END 阶段。在关机时有两个 GC 阶段,首先有 ref 计数更新,所以它可以很好地杀死你。你可以在丑陋的解决它:

    END { kill 9, $$ }
    

    【讨论】:

    • 你的回答没有多大意义。
    • @Ether 在程序结束时,所有变量的 REFCNT 字段将被 GC 递减。这将导致内存使用量突然激增,因为所有变量都被拉入子进程。他建议的代码将导致孩子在垃圾收集阶段开始之前死亡。
    【解决方案3】:

    这是不言而喻的,但 COW 不是在每个结构的基础上发生的,而是在内存页的基础上发生的。所以这样修改整个内存页中的一个东西就够你支付复制成本了。

    在 Linux 上,您可以像这样查询页面大小:

    getconf PAGESIZE
    

    在我的系统上是 4096 字节。您可以在该空间中放置许多 Perl 标量结构。如果其中一项被修改,Linux 将不得不复制整个内容。

    这就是为什么使用内存区域通常是一个好主意。您应该将可变数据和不可变数据分开,这样您就不必因为不可变数据恰好与可变数据位于同一内存页中而为不可变数据支付 COW 成本。

    【讨论】:

    • 问题是 perl 更新了 struct 即使我没有更改我关心的任何数据,所以我无法分离我的不可变数据(因为没有不可变数据)。
    • 此外,没有一种简单的方法可以将可变数据与不可变数据分开(即使有)。
    猜你喜欢
    • 2021-05-21
    • 2020-04-12
    • 1970-01-01
    • 2015-06-11
    • 1970-01-01
    • 2020-05-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多