谁杀了我的同类?或如何有效地计算 csv 列中的不同值

谁杀了我的同类?或如何有效地计算 csv 列中的不同值

我正在做一些处理,试图获取包含 160,353,104 行的文件中有多少不同的行。这是我的管道和 stderr 输出。

$ tail -n+2 2022_place_canvas_history.csv | cut -d, -f2 | tqdm --total=160353104 |\
  sort -T. -S1G | tqdm --total=160353104 | uniq -c | sort -hr > users

100%|████████████████████████████| 160353104/160353104 [0:15:00<00:00, 178051.54it/s]
 79%|██████████████████████      | 126822838/160353104 [1:16:28<20:13, 027636.40it/s]

zsh: done tail -n+2 2022_place_canvas_history.csv | cut -d, -f2 | tqdm --total=160353104 | 
zsh: killed sort -T. -S1G | 
zsh: done tqdm --total=160353104 | uniq -c | sort -hr > users

我的命令行 PS1 或 PS2 打印了管道所有进程的返回码。✔ 0|0|0|KILL|0|0|0第一个字符是绿色复选标记,表示最后一个进程返回 0(成功)。其他数字是每个管道进程的返回代码,顺序相同。所以我注意到我的第四个命令获得了KILL状态,这是我的排序命令sort -T. -S1G,将本地目录设置为临时存储并缓冲高达 1GiB。

问题是,为什么它返回了KILL,是否意味着KILL SIGN向它发送了一些东西?有没有办法知道“谁杀了”它?

更新

看完之后马库斯·穆勒 答案,首先我尝试将数据加载到 Sqlite 中。

因此,也许现在是告诉您的好时机,不,不要使用基于 CSV 的数据流。一个简单的

sqlite3 place.sqlite

并在该 shell 中(假设您的 CSV 有一个标题行,SQLite 可以使用该标题行来确定列)(当然,将 $second_column_name 替换为该列的名称)

.import 022_place_canvas_history.csv canvas_history --csv
SELECT $second_column_name, count($second_column_name)   FROM canvas_history 
GROUP BY $second_column_name;

这花了很多时间,所以我把它留在处理中并去做其他事情。虽然我更多地思考了另一段马库斯·穆勒 答案

您只想知道每个值在第二列中出现的频率。之前的排序只是因为你的工具(uniq -c)很糟糕,并且需要之前对行进行排序(实际上没有充分的理由。它只是没有实现它可以保存值及其频率的映射,并随着它们的出现而增加)出现)。

所以我想,我可以实现它。当我回到计算机时,我的 Sqlite 导入过程因 SSH Broken Pip 而停止,认为它很长时间没有传输数据,因此关闭了连接。好吧,这是使用 dict/map/hashtable 实现计数器的好机会。所以我写了以下distinct文件:

#!/usr/bin/env python3
import sys

conter = dict()

# Create a key for each distinct line and increment according it shows up. 
for l in sys.stdin:
    conter[l] = conter.setdefault(l, 0) + 1 # After Update2 note: don't do this, do just `couter[l] = conter.get(l, 0) + 1`

# Print entries sorting by tuple second item ( value ), in reverse order
for e in sorted(conter.items(), key=lambda i: i[1], reverse=True):
    k, v = e
    print(f'{v}\t{k}')

所以我通过以下命令管道使用了它。

tail -n+1 2022_place_canvas_history.csv | cut -d, -f2 | tqdm --total=160353104 | ./distinct > users2

它进展得非常非常快,预计tqdm不到 30 分钟,但当进入 99% 时,它变得越来越慢。此过程使用了大量 RAM,大约 1.7GIB。我正在处理这些数据的机器,我有足够存储空间的机器,是一个只有 2GiB RAM 和 ~1TiB 存储空间的 VPS。我认为它可能会变得如此缓慢,因为必须处理这些巨大的内存,也许要做一些交换或其他事情。无论如何,我一直在等待,当它最终在 tqdm 中达到 100% 时,所有数据都被发送到./distinct进程中,几秒钟后得到以下输出:

160353105it [30:21, 88056.97it/s]                                                                                            
zsh: done       tail -n+1 2022_place_canvas_history.csv | cut -d, -f2 | tqdm --total=160353104 | 
zsh: killed     ./distinct > users2

这次基本上可以肯定是由内存不足的杀手造成的,如在马库斯·穆勒 答案TLDR 部分。

所以我刚刚检查过,我在这台机器上没有启用交换。使用 dmcrypt 和 LVM 完成设置后禁用它,因为您可能会在以下位置获得更多信息我的这个答案

所以我的想法是启用我的 LVM 交换分区并尝试再次运行它。另外,在某个时刻,我认为我见过 tqdm 使用 10GiB RAM。但我很确定我看到了错误或btop输出混淆了,因为后者仅显示 10MiB,不认为 tqdm 会使用太多内存,因为它只是在读取新的\n.

在 Stéphane Chazelas 对这个问题的评论中,他们说:

系统日志可能会告诉你。

我想了解更多,我应该在journalctl中找到一些东西吗?如果可以的话,该怎么办呢?

无论如何,作为马库斯·穆勒 答案说,将 csv 加载到 Sqlite 可能是迄今为止最智能的解决方案,因为它将允许以多种方式操作数据,并且可能有一些智能的方式来导入这些数据而不会出现内存不足。

但现在我对如何找出进程被杀死的原因感到好奇,因为我想了解我的进程,sort -T. -S1G现在了解我的进程./distinct,最后一个几乎可以肯定它与内存有关。那么如何检查日志来说明为什么这些进程被杀死呢?

更新2

所以我启用了我的 SWAP 分区并采取了马库斯·穆勒这个问题评论的建议。使用 python 集合.Counter。所以我的新代码 ( distinct2) 如下所示:

#!/usr/bin/env python3
from collections import Counter
import sys

print(Counter(sys.stdin).most_common())

所以我跑了Gnu 屏幕即使我再次遇到管道损坏,我也可以恢复会话,而不是在以下管道中运行它:

tail -n+1 2022_place_canvas_history.csv | cut -d, -f2 | tqdm --total=160353104 --unit-scale=1 | ./distinct2 | tqdm --unit-scale=1 > users5

这让我得到了以下输出:

160Mit [1:07:24, 39.6kit/s]
1.00it [7:08:56, 25.7ks/it]

正如您所看到的,对数据进行排序比对数据进行计数花费了更多的时间。您可能会注意到的另一件事是 tqdm 第二行输出仅显示 1.00it,这意味着它只有一行。所以我使用 head 检查了 user5 文件:

head -c 150 users5 
[('kgZoJz//JpfXgowLxOhcQlFYOCm8m6upa6Rpltcc63K6Cz0vEWJF/RYmlsaXsIQEbXrwz+Il3BkD8XZVx7YMLQ==\n', 795), ('JMlte6XKe+nnFvxcjT0hHDYYNgiDXZVOkhr6KT60EtJAGa

正如您所看到的,它在一行中打印了整个元组列表。为了解决这个问题,我使用了旧的 sed ,如下所示sed 's/),/)\n/g' users5 > users6。之后,我使用 head 检查了 users6 内容,其输出如下:

$ head users6
[('kgZoJz/...c63K6Cz0vEWJF/RYmlsaXsIQEbXrwz+Il3BkD8XZVx7YMLQ==\n', 795)
 ('JMlte6X...0EtJAGaezxc4e/eah6JzTReWNdTH4fLueQ20A4drmfqbqsw==\n', 781)
 ('LNbGhj4...apR9YeabE3sAd3Rz1MbLFT5k14j0+grrVgqYO1/6BA/jBfQ==\n', 777)
 ('K54RRTU...NlENRfUyJTPJKBC47N/s2eh4iNdAKMKxa3gvL2XFqCc9AqQ==\n', 767)
 ('8USqGo1...1QSbQHE5GFdC2mIK/pMEC/qF1FQH912SDim3ptEFkYPrYMQ==\n', 767)
 ('DspItMb...abcd8Z1nYWWzGaFSj7UtRC0W75P7JfJ3W+4ne36EiBuo2YQ==\n', 766)
 ('6QK00ig...abcfLKMUNur4cedRmY9wX4vL6bBoV/JW/Gn6TRRZAJimeLw==\n', 765)
 ('VenbgVz...khkTwy/w5C6jodImdPn6bM8izTHI66HK17D4Bom33ZrwuGQ==\n', 758)
 ('jjtKU98...Ias+PeaHE9vWC4g7p2KJKLBdjKvo+699EgRouCbeFjWsjKA==\n', 730)
 ('VHg2OiSk...3c3cr2K8+0RW4ILyT1Bmot0bU3bOJyHRPW/w60Y5so4F1g==\n', 713)

足够好,可以稍后工作。现在我想我应该在尝试检查后添加更新谁杀了我的同类使用dmesg或journalctl。我还想知道是否有办法让这个脚本更快。也许创建一个线程池,但必须检查pythons dict行为,还考虑其他数据结构,因为我正在计算的列是固定宽度字符串,也许使用列表来存储每个不同user_hash的频率。我还阅读了 Counter 的 python 实现,它只是一个字典,与我之前的实现几乎相同,但不是使用dict.setdefaultjustused ,而是在这种情况下没有真正需要dict[key] = dict.get(key, 0) + 1的误用。setdefault

更新3

所以我已经陷入了兔子洞的深渊,完全失去了目标的焦点。我开始寻找更快的排序,也许写一些 C 或 Rust,但意识到已经处理了我要处理的数据。所以我在这里展示 dmesg 输出以及有关 python 脚本的最后一个技巧。提示是:使用 dict 或 Counter 进行计数可能比使用 gnu 排序工具对其输出进行排序更好。排序可能比 python 排序 buitin 函数更快。

关于 dmesg,查找内存不足非常简单,只需sudo dmesg | less按一下G即可一直向下,而不是?向后搜索,而不是搜索Out字符串。找到了其中两个,一个用于我的 python 脚本,另一个用于我的排序,即引发此问题的那个。这是这些输出:

[1306799.058724] Out of memory: Killed process 1611241 (sort) total-vm:1131024kB, anon-rss:1049016kB, file-rss:0kB, shmem-rss:0kB, UID:1000 pgtables:2120kB oom_score_adj:0
[1306799.126218] oom_reaper: reaped process 1611241 (sort), now anon-rss:0kB, file-rss:0kB, shmem-rss:0kB
[1365682.908896] Out of memory: Killed process 1611945 (python3) total-vm:1965788kB, anon-rss:1859264kB, file-rss:0kB, shmem-rss:0kB, UID:1000 pgtables:3748kB oom_score_adj:0
[1365683.113366] oom_reaper: reaped process 1611945 (python3), now anon-rss:0kB, file-rss:0kB, shmem-rss:0kB

就是这样,非常感谢您到目前为止的帮助,希望它也能帮助其他人。

答案1

TL;DR:内存不足杀手或磁盘空间不足用于临时文件杀死sort。建议:使用不同的工具。


sort.c我现在已经浏览了 GNU coreutils' 。您的-S 1G意思只是意味着该sort进程尝试分配 1GB 的内存块,如果不可能的话,将回退到越来越小的大小。

耗尽该缓冲区后,它将创建一个临时文件来存储已排序的行²,并对内存中的下一个输入块进行排序。

消耗完所有输入后,sort会将两个临时文件合并/排序为一个临时文件(mergesort-style),并连续合并所有临时文件,直到合并产生总排序输出,然后将其输出到stdout.

这很聪明,因为这意味着您可以对大于可用内存的输入进行排序。

或者,在这些临时文件本身不保存在 RAM 中的系统上(/tmp/通常是 a tmpfs,仅 RAM 文件系统),这很聪明。因此,写入这些临时文件会占用您想要保存的 RAM,并且您的 RAM 即将耗尽:您的文件有 1.6 亿行,快速谷歌一下就会发现它是 11GB 的未压缩数据。

sort您可以通过更改它使用的临时目录来“帮助”解决这个问题。您已经这样做了,-T.将临时文件放置在当前目录中。可能你那里的空间不够了?或者当前目录是否在tmpfs或类似?

您有一个包含中等数据量的 CSV 文件(1.6 亿行不是现代 PC 的数据量很大)。您不是将其放入旨在处理大量数据的系统中,而是尝试使用 20 世纪 90 年代的工具(是的,我刚刚读过sortgit 历史)对其进行操作,当时 16 MB RAM 似乎相当慷慨。

CSV 只是数据格式错误用于处理任何大量数据,您的示例完美地说明了这一点。低效的工具以低效的方式处理低效的数据结构(带有行的文本文件),以低效的方式实现目标:

您只想知道每个值在第二列中出现的频率。之前的排序只是因为你的工具(uniq -c)很糟糕,并且需要之前对行进行排序(实际上没有充分的理由。它只是没有实现它可以保存值及其频率的映射,并随着它们的出现而增加)出现)。


因此,也许现在是告诉您的好时机,不,不要使用基于 CSV 的数据流。一个简单的

sqlite3 place.sqlite

并在该 shell 中(假设您的 CSV 有一个标题行,SQLite 可以使用该标题行来确定列)(当然,替换$second_column_name为该列的名称)

.import 022_place_canvas_history.csv canvas_history --csv
SELECT $second_column_name, count($second_column_name)
  FROM canvas_history
  GROUP BY $second_column_name;

可能会一样快,而且额外的好处是,您会得到一个实际的数据库文件place.sqlite。您可以更灵活地进行操作 - 例如,创建一个表,在其中提取坐标,并将时间转换为数字时间戳,然后通过您的分析变得更快、更灵活。


1 全局变量,以及何时使用的不一致。他们受伤了。对于 C 语言作者来说,这是一个不同的时代。它绝对不是糟糕的 C,只是……不是您所习惯的更现代的代码库。感谢 Jim Meyering 和 Paul Eggert 编写和维护此代码库!

² 你可以尝试执行以下操作:对一个不太大的文件进行排序,比如说ls.c有 5577 行,并记录打开的文件数:

strace -o /tmp/no-size.strace -e openat sort ls.c
strace -o /tmp/s1kB-size.strace -e openat sort -S 1 ls.c
strace -o /tmp/s100kB-size.strace -e openat sort -S 100 ls.c
wc -l /tmp/*-size.strace

答案2

来自@MarcusMüller的回答关于“谁杀了我的同类?”已经很清楚了。并且你已经确认了这个问题。

然而,第二部分还没有得到太多讨论:或如何有效地计算 csv 列中的不同值。除了尝试找到更好/更快的排序方法之外。

那是因为你的管道(全部)都是基于使用uniq.并且uniq需要排序的数据。

还有其他解决办法吗?

是的。以第 2 列数据作为键构建一个数组,并在每次找到此类值时加 1。这是 awk 处理数据的常用方式:

$ awk -F, '{count[$2]++}END{for (i in count) {print i,count[i]}}'

这不需要像排序那样将整个文件保留在内存中。但只有键列表(如'kgZoJz//JpfXgowLxOhcQlFYOCm8m6upa6Rpltcc63K6Cz0vEWJF/RYmlsaXsIQEbXrwz+Il3BkD8XZVx7YMLQ==\n'您显示的键和用于计数的浮点数)。

这将按文件出现的顺序处理文件的每一行一次,无需排序即可计算唯一用户数。但是,是的,需要进行最终排序来对计数进行排序。

因此,处理文件的时间将与n排序时间成正比n*log(n),并且内存使用将与用户数“m”(第二个字段 uniq 键)成正比。

如果每个用户的平均计数为 350(假设最大为 ~795,最小为 1,并且计数在两个计数之间呈线性变化),则使用的内存大小应与 88(键的大小)成正比。 )乘以 160353104/350(不同键的数量),或者小于 40 兆字节加上一些开销。

相关内容