如何解决在分离文件时遇到的打开文件数限制问题?

3
我经常需要处理大型文本文件(10-100GB解压缩),根据每行中的条形码进行解复用,实际上产生的单独文件(唯一条形码)的数量在1K到20K之间。我一直在使用awk来完成这个任务。然而,我注意到解复用更大的文件(与使用更多唯一条形码有关)的速度明显变慢(10-20倍)。检查ulimit -n显示每个进程的打开文件限制为4096个,因此我怀疑减速是由于强制awk在总解复用文件数超过4096时不断关闭和重新打开文件的开销引起的。
没有root访问权限(即限制是固定的),有什么办法可以规避这个瓶颈吗?
我确实有每个文件中存在的所有条形码列表,因此我考虑分叉多个awk进程,其中每个进程被分配一个互斥子集(< 4096)的条形码来搜索。但是,我担心必须检查每行的条形码是否属于集合会消耗关闭文件的好处所得到的好处。
有更好的策略吗?
我没有使用awk,因此其他脚本或编译语言的方法也可以。
具体示例
数据生成(带条形码的FASTQ)
以下生成类似于我正在处理的数据。每个条目由4行组成,其中条形码是使用非模糊DNA字母的18个字符单词。
1024个唯一条形码| 100万条读数。
cat /dev/urandom | tr -dc "ACGT" | fold -w 5 | \
awk '{ print "@batch."NR"_"$0"AAAAAAAAAAAAA_ACGTAC length=1\nA\n+\nI" }' | \
head -n 4000000 > cells.1K.fastq

16384个唯一条形码 | 100万次读取


cat /dev/urandom | tr -dc "ACGT" | fold -w 7 | \
awk '{ print "@batch."NR"_"$0"AAAAAAAAAAA_ACGTAC length=1\nA\n+\nI" }' | \
head -n 4000000 > cells.16K.fastq

awk脚本用于解复用

注意,在这种情况下,我为每个唯一的条形码写入2个文件。

demux.awk

#!/usr/bin/awk -f
BEGIN {
    if (length(outdir) == 0 || length(prefix) == 0) {
        print "Variables 'outdir' and 'prefix' must be defined!" > "/dev/stderr";
        exit 1;
    }
    print "[INFO] Initiating demuxing..." > "/dev/stderr";
}
{
    if (NR%4 == 1) {
        match($1, /.*_([ACGT]{18})_([ACGTN]{6}).*/, bx);
        print bx[2] >> outdir"/"prefix"."bx[1]".umi";
    }
    print >> outdir"/"prefix"."bx[1]".fastq";

    if (NR%40000 == 0) {
        printf("[INFO] %d reads processed\n", NR/4) > "/dev/stderr";
    }
}
END {
    printf("[INFO] %d total reads processed\n", NR/4) > "/dev/stderr";
}

使用方法

awk -v outdir="/tmp/demux1K" -v prefix="batch" -f demux.awk cells.1K.fastq

或者对于cells.16K.fastq也是类似的处理方式。假设您是唯一运行awk的人,您可以使用以下命令验证打开文件的大致数量。
lsof | grep "awk" | wc -l

观察到的行为

尽管两个文件大小相同,但具有16K个唯一条形码的文件比只有1K个唯一条形码的文件运行速度慢10倍至20倍。


3
先对文件进行排序可以排除问题(一次只会打开一个文件)。不确定排序的收益是否超过成本。另一种替代方案是某种类型的分区(比排序更便宜),使分区彼此互斥(例如C ++分区)。 - karakfa
发布一个包含简洁、可测试的样例输入和期望输出的 [mcve],你可能会得到一些答案。如果不清楚,请参考 [ask]。 - Ed Morton
2
这是适用的。我们不知道你的代码实际上在做什么,所以我们猜测可能是什么,而你在猜测问题的原因,我们在不知道你的输入长什么样子、需要什么输出等情况下猜测解决方案。只需要一个 [mcve] 就可以消除大部分猜测,并为我们提供一个起点。 - Ed Morton
安装适当的数据库管理系统。将数据加载到表中。使用SQL进行操作。 - Ben
1个回答

4

在没有看到任何示例输入/输出或您当前正在执行的脚本的情况下,这很难猜测。但是,如果您当前将条形码放在字段1中,并且正在执行以下操作(假设使用GNU awk,因此您没有自己的代码来管理打开的文件):

awk '{print > $1}' file

如果您正在处理打开的文件时遇到问题,可以尝试使用以下方法来获得显著的改善:
sort file | '$1!=f{close(f};f=$1} {print > f}'

当然,以上内容是基于假设这些条形码值是什么,哪个字段包含它们,分隔字段的方式,输出顺序是否必须与原始顺序匹配,以及您的代码在输入增长时会变慢等等。因为您还没有向我们展示任何这方面的信息。
如果这不是您所需要的全部,请编辑您的问题以包括缺失的MCVE。
根据您更新的问题和脚本信息,输入是由四行组成的块,我会在每个记录的开头添加“bx”键值,并使用NUL来分隔这些四行块,然后使用NUL作为排序的记录分隔符和后续的awk处理。
$ cat tst.sh
infile="$1"
outdir="${infile}_out"
prefix="foo"

mkdir -p "$outdir" || exit 1

awk -F'[_[:space:]]' -v OFS='\t' -v ORS= '
    NR%4 == 1 { print $2 OFS $3 OFS }
    { print $0 (NR%4 ? RS : "\0") }
' "$infile" |
sort -z |
awk -v RS='\0' -F'\t' -v outdir="$outdir" -v prefix="$prefix" '
BEGIN {
    if ( (outdir == "") || (prefix == "") ) {
        print "Variables \047outdir\047 and \047prefix\047 must be defined!" | "cat>&2"
        exit 1
    }
    print "[INFO] Initiating demuxing..." | "cat>&2"
    outBase = outdir "/" prefix "."
}
{
    bx1   = $1
    bx2   = $2
    fastq = $3

    if ( bx1 != prevBx1 ) {
        close(umiOut)
        close(fastqOut)
        umiOut   = outBase bx1 ".umi"
        fastqOut = outBase bx1 ".fastq"
        prevBx1  = bx1
    }

    print bx2   > umiOut
    print fastq > fastqOut

    if (NR%10000 == 0) {
        printf "[INFO] %d reads processed\n", NR | "cat>&2"
    }
}
END {
    printf "[INFO] %d total reads processed\n", NR | "cat>&2"
}
'

当针对您在问题中描述的生成的输入文件运行时:

$ wc -l cells.*.fastq
4000000 cells.16K.fastq
4000000 cells.1K.fastq

结果如下:

$ time ./tst.sh cells.1K.fastq 2>/dev/null

real    0m55.333s
user    0m56.750s
sys     0m1.277s

$ ls cells.1K.fastq_out | wc -l
2048

$ wc -l cells.1K.fastq_out/*.umi | tail -1
1000000 total

$ wc -l cells.1K.fastq_out/*.fastq | tail -1
4000000 total


$ time ./tst.sh cells.16K.fastq 2>/dev/null

real    1m6.815s
user    0m59.058s
sys     0m5.833s

$ ls cells.16K.fastq_out | wc -l
32768

$ wc -l cells.16K.fastq_out/*.umi | tail -1
1000000 total

$ wc -l cells.16K.fastq_out/*.fastq | tail -1
4000000 total

1
非常好!在输入文件为1K和输出文件为16K的测试中,这确实使得两者的执行时间可比较。对于其他人来说值得注意的是,仅排序并让操作系统(CentOS)负责关闭之前的文件仍会导致显著的减速 - 也就是说,必须像您所指示的那样显式地关闭文件。不幸的是,我拥有的数据大部分包含多行块(每个条形码一个块),因此这并不是一种简单的排序操作,但仍然可以完成。 - merv
1
当然。假设您对打开文件数量的问题正确,排序并不会帮助执行时间,它只是创建了一个输入顺序,以便我们在awk脚本中一次只能打开1个输出文件,并且这有助于执行时间。关于多行输入块 - 我添加了一个shell脚本到我的答案中,以展示如何处理它们。 - Ed Morton

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接