我有一个 36GB 的文件,它是使用 gzip 压缩的 nginx 日志。我想将此文件拆分为较小的文件,这样我就可以使用这些小文件作为我编写的另一个脚本的输入,而不会耗尽内存。该文件的第一列包含 IP 地址,来自特定 IP 地址的所有请求在拆分后应位于同一个文件中。
我目前需要实现的目标如下:
#!/bin/bash
FILENAME=$1
NUM_FILES=$(($2))
PREFIX=$3
unpigz -c $FILENAME | awk -v NUM_FILES=$NUM_FILES -v PREFIX=$PREFIX \
'! ($1 in out) {out[$1] = (idx++ %NUM_FILES) } '\
'{ outfile= PREFIX"." out[$1] ".txt"; print | ("pigz >" outfile) ; next} '
虽然一切都按预期工作,但它使用了太多内存,而且我的计算机中只有 16GB 可用空间,因此我无法将其拆分为大于 x GB 的文件。
我想知道在这种情况下是否可以以某种方式浪费更少的内存。
答案1
这周我似乎一直在追逐独角兽和彩虹。我制定了三种策略的时间安排。对于所有计时,我将我的 2000 万行推断为您(估计的)80 亿行,但针对我的笔记本电脑和硬盘进行计时。为了计时目的,我压缩了我的测试文件,但只能用gunzip来解压它。
(A) 读取充满行的内存(可能是 10GB),然后将它们流式传输到文件(一次一个文件)。我误解了要求是将每个 IP 放入不同的文件中。这涉及到为大约每 10GB 的数据附加和关闭 400K 个文件。我的运行时间是120小时。这还导致您需要管理 400,000 个文件。这些都不好。
(B) 对 36GB 文件进行多次处理(360GB 已解压)。此时我误解了您在分离时没有足够的空间容纳整个 500GB 的数据。
事实上,多次通行证并不那么昂贵。这个想法是将 IP 分组到索引中。所以你可能会进行8遍,每遍写入100个文件,每个文件中有500个IP,每个文件大约有1000万行,800个文件。运行时间应该在33小时,多遍比多个文件附加快得多。还有其他优点:
.. 您可以使用更少的磁盘空间,因为您可以在开始下一组之前压缩一组中的所有文件。
.. 您可以安排每个组,例如在系统需求可能较低时过夜运行。
.. 每组都有重启点,以防出现故障。
.. 组文件是一个索引,用于定位包含特定IP 的文件。
(C)正如你所说,你有足够的磁盘空间来保存所有内容的一份解压副本,你的原始脚本是完全没问题的,进行了三处小修改。
.. 您无法压缩进程中的每个输出文件,但您可以写入所有文件并稍后压缩它们,也许要小心使用 GNU 并行。
.. 我在 awk 输出文件的数量上看到了我的时间安排的最佳点。根据经验,100 个输出文件(对于相同的总数据量)比 80 或 120 个输出文件要快。尝试超过 100 路的分割可能是不明智的,因此您最终可能会得到 100 个文件,每个文件包含大约 8000 万个文件行和 4000 个 IP。
.. 为每个 IP 编写一次文件名比每次写入都更清晰。
我对这种方法的估计是24小时。
所以你原来的现在看起来像:
time gunzip -c ../trial.data.gz | awk '
! ($1 in X) { X[$1] = sprintf ("Prefix_%.4d.dat", q++ % 100); }
{ print > X[$1]; }'
答案2
完成此类任务的一般方法是分割工作负载,分两步完成整个工作(您的硬盘会喜欢这样做)。
您不必为每个 IP 地址创建一个文件(以及内存中具有一个压缩器进程的一个管道),而是为最后一个 IP 八位字节中的每个值创建一个文件:
unpigz -c $FILENAME | awk -v NUM_FILES=$NUM_FILES -v PREFIX=$PREFIX \
'{ last_octet=$0; sub("^[0-9]+\\.[0-9]+\\.[0-9]+\\.","",last_octet); }; '\
! (last_octet in out) {out[last_octet] = 1 } '\
'{ outfile= PREFIX"." last_octet ".txt"; print | ("pigz >" outfile) ; next} '
之后,您让代码在每个文件上运行。考虑一下如何将生成的文件保留在内存中,直到它们达到一定大小(如果未存储在 SSD 上),这可能是有意义的。
答案3
我有一个 GNU/awk 脚本来执行此操作。然而,在性能、磁盘空间和文件管理方面存在一系列问题,此时可能值得考虑。
设计笔记——起源。
我找到了我的代码,并认为再次测试它会有所帮助。这最初是一个简单的论坛帖子:给定一个已知列中包含美国州代码的文件,如何在不知道存在哪些州的情况下将其分成不同的文件。很简单:数据到达时就会告诉您。我刚刚为每个新状态打开一个新文件。所以60多个文件是没有问题的(其中包括美国海外领土)。
我想知道 GNU/awk 能做到什么程度。它支持无限数量的输出文件,但是当它达到 ulimit (1024) 时,它会通过关闭并重新打开来伪造它。您可以增加 ulimit,但可能不会增加到 400,000。对于大量文件和随机输入序列,这近似于每行打开/关闭一个文件——这是一种病态的情况。我使用不同的关键字段(具有 25K 个不同值)运行,并尝试了使用多通道、级联和进程树的多种设计,然后找到了更快、更简单的解决方案。
基本的解决方案是获取尽可能多的行,并将它们填充到二维数组中。然后刷新它们,按文件和原始顺序排序。这避免了磁盘上的碎片,并且只需要一次打开一个输出文件。
测试数据。
我查看了 nginx 网站的日志格式,并且(除非您配置了每个可选字段)我假设您有一个 IP 地址,后面可能是 60 个字符的文本。所以我拼接了2000万行数据(1.5 GB):
.. 400,000 个 IP 地址扩展并随机化:{10..29}.{30..39}.{80..129}.{110..149}
.. 从哈佛 CS50 测试包中提取 490,306 条文本行,不包括长度不在 40 到 90 个字符之间的行。
这两个数据都被复制到 20M 行,然后连接起来。
评估。
性能——在笔记本电脑、4GB、HDD 上测量。
我的 20M 行 (1.5GB) 需要 15 分钟(来自未解压的纯文本)。我认为性能没有理由不达到 O(n),因此您的 480GB 原始数据将需要大约 80 小时。您的服务器可能比我的三星 RV515 快得多。
记忆
我的内存阵列使用了 600 万行,加上已知 IP 和文件名的增量阵列。这使用了我的 4MB 中的大约 2.7GB——超过这个数量会导致过多的交换和 REISUB。
输出文件
使用更多的空间应该是有益的。每次刷新平均每个输出文件追加 15 行(6M 行/400K 键)。使用 10 倍的内存可以避免 90% 的文件管理。
有多少个 IP 地址就有多少个文件。一个目录下有 40 万个文件,处理起来非常麻烦。
输入中特定 IP 的分布可能很重要。我的数据是随机分布的,这可能是最坏的情况。如果使用模式是 IP 进行局部突发访问,然后就不再出现,那么每次刷新的文件将会更少、更大,因此效率更高。
我们可以追加 400,000 个文件(一次一个),但我们无法追加到压缩文件,因为大部分上下文都存储在进程中,而我们无法运行那么多进程。因此,所有数据暂时需要是纯文本,并在提取后单独压缩。因此,您暂时需要 500GB 的临时磁盘空间。
欢迎您使用该代码,以及定制该代码所需的任何帮助。然而,我不禁觉得以前一定已经使用数据库和适当的工具对此类数据进行了分析,也许应该考虑这一点,特别是如果这不仅仅是一次性的练习。