Estou fazendo algum processamento tentando obter quantas linhas diferentes em um arquivo contendo 160.353.104 linhas. Aqui está minha saída de pipeline e 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
Minha linha de comando PS1 ou PS2 imprimiu os códigos de retorno de todos os processos do pipeline. ✔ 0|0|0|KILL|0|0|0
O primeiro caractere é uma marca de seleção verde que significa que o último processo retornou 0 (sucesso). Outros números são códigos de retorno para cada um dos processos em pipeline, na mesma ordem. Então, notei que meu quarto comando obteve KILL
status, este é meu comando de classificação sort -T. -S1G
definindo o diretório local para armazenamento temporário e buffer de até 1GiB.
A questão é, por que ele retornou KILL, isso significa que algo enviado KILL SIGN
a ele? Existe uma maneira de saber "quem matou"?
Atualizações
Depois de ler Marcus Müller Answer , primeiro tentei carregar os dados no Sqlite.
Então, talvez este seja um bom momento para lhe dizer que, não, não use um fluxo de dados baseado em CSV. Um simples
sqlite3 place.sqlite
e nesse shell (supondo que seu CSV tenha uma linha de título que o SQLite possa usar para determinar as colunas) (é claro, substitua $second_column_name pelo nome dessa coluna)
.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;
Isso estava demorando muito, então deixei processando e fui fazer outras coisas. Enquanto eu pensava mais sobre este outro parágrafo de Marcus Müller Resposta :
Você só quer saber com que frequência cada valor apareceu na segunda coluna. Classificar isso antes só acontece porque sua ferramenta (
uniq -c
) é ruim e precisa que as linhas sejam classificadas antes (literalmente não há uma boa razão para isso. Apenas não está implementado que ela possa conter um mapa de valores e sua frequência e aumentar isso à medida que eles aparecer).
Então eu pensei, eu posso implementar isso. Quando voltei ao computador, meu processo de importação do Sqlite parou por causa de um SSH Broken Pip, acho que ele não transmitiu dados por um longo tempo, fechou a conexão. Ok, que boa oportunidade para implementar um contador usando um dict/map/hashtable. Então eu escrevi o seguinte distinct
arquivo:
#!/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}')
Então eu usei pelo pipeline de comando follow.
tail -n+1 2022_place_canvas_history.csv | cut -d, -f2 | tqdm --total=160353104 | ./distinct > users2
Estava indo muito rápido, projeção de tqdm
menos de 30 minutos, mas quando chegou em 99% estava ficando cada vez mais lento. Este processo estava usando muita RAM, cerca de 1,7 GB. Máquina que estou trabalhando com esses dados, a máquina que tenho armazenamento suficiente, é um VPS com apenas 2GiB de RAM e ~1TiB de armazenamento. Pensei que poderia estar ficando tão lento porque o SO estava tendo que lidar com essa memória enorme, talvez fazendo alguma troca ou outras coisas. Eu esperei de qualquer maneira, quando finalmente chegou em 100% no tqdm, todos os dados foram enviados para o ./distinct
processo, depois de alguns segundos obtive a seguinte saída:
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
Desta vez, principalmente por causa do assassino sem memória, como visto na seção Marcus Müller Answer TLDR.
Então, acabei de verificar e não tenho swap ativado nesta máquina. Desativei depois de concluir sua configuração com dmcrypt e LVM, pois você pode obter mais informações nestas minhas respostas .
Então, o que estou pensando é habilitar minha partição de troca LVM e tentar executá-la novamente. Também em algum momento acho que vi tqdm usando 10GiB de RAM. Mas tenho certeza de que vi erroneamente ou btop
saída confusa, pois depois mostrou apenas 10MiB, não acho que o tqdm usaria muita memória, pois apenas conta e atualiza algumas estatísticas ao ler um novo arquivo \n
.
No comentário de Stéphane Chazelas a esta questão dizem:
Os logs do sistema possivelmente lhe dirão.
Gostaria de saber mais sobre isso, devo encontrar algo no journalctl? Se for o caso, como fazer?
De qualquer forma, como Marcus Müller Answer diz, carregar o csv no Sqlite pode ser de longe a solução mais inteligente, pois permitirá operar em dados de várias maneiras e provavelmente tem alguma maneira inteligente de importar esses dados sem falta de memória.
Mas agora estou duas vezes curioso sobre como descobrir por que o processo foi morto, como quero saber sobre o meu sort -T. -S1G
e agora sobre o meu ./distinct
, o último quase certo que era sobre memória. Então, como verificar os logs que dizem por que esses processos foram eliminados?
Atualização2
Então, habilitei minha partição SWAP e aceitei a sugestão de Marcus Müller deste comentário de pergunta. Usando coleções de pythons.Counter. Então meu novo código ( distinct2
) fica assim:
#!/usr/bin/env python3
from collections import Counter
import sys
print(Counter(sys.stdin).most_common())
Então, eu executei o Gnu Screen para que, mesmo se eu obtivesse um pipe quebrado novamente, eu pudesse simplesmente retomar a sessão e executá-lo no seguinte pipeline:
tail -n+1 2022_place_canvas_history.csv | cut -d, -f2 | tqdm --total=160353104 --unit-scale=1 | ./distinct2 | tqdm --unit-scale=1 > users5
Isso me deu a seguinte saída:
160Mit [1:07:24, 39.6kit/s]
1.00it [7:08:56, 25.7ks/it]
Como você pode ver, levou muito mais tempo para classificar os dados do que contá-los. Uma outra coisa que você pode notar é que a saída da segunda linha do tqdm mostra apenas 1.00it, significa que tem apenas uma única linha. Então eu verifiquei o arquivo user5 usando head:
head -c 150 users5
[('kgZoJz//JpfXgowLxOhcQlFYOCm8m6upa6Rpltcc63K6Cz0vEWJF/RYmlsaXsIQEbXrwz+Il3BkD8XZVx7YMLQ==\n', 795), ('JMlte6XKe+nnFvxcjT0hHDYYNgiDXZVOkhr6KT60EtJAGa
Como você pode ver, ele imprimiu toda a lista de tuplas em uma única linha. Para resolver isso eu usei o bom e velho sed como segue sed 's/),/)\n/g' users5 > users6
. Depois disso, verifiquei o conteúdo de users6 usando head, como segue com sua saída:
$ 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)
Bom o suficiente para trabalhar mais tarde. Agora acho que devo adicionar uma atualização depois de tentar verificar quem matou meu tipo usando dmesg ou journalctl. Eu também estou querendo saber se existe uma maneira de tornar este script mais rápido. Talvez criando um threadpool, mas tenho que verificar o comportamento do dict de pythons, também pensei em outras estruturas de dados, pois a coluna que estou contando é uma string de largura fixa, talvez usando uma lista para armazenar a frequência de cada user_hash diferente. Também li a implementação python do Counter, é apenas um dict, praticamente a mesma implementação que eu tinha antes, mas em vez de usar dict.setdefault
apenas used dict[key] = dict.get(key, 0) + 1
, foi um uso incorreto de setdefault
nenhuma necessidade real para esse cenário.
Atualização3
Então eu estou ficando tão fundo na toca do coelho, totalmente perdido o foco do meu objetivo. Comecei a procurar por ordenação mais rápida, talvez escrever algum C ou Rust, mas percebi que já tenho os dados que vim buscar processados. Então, estou aqui para mostrar a saída do dmesg e uma dica final sobre o script python. A dica é: pode ser melhor apenas contar usando dict ou Counter, do que classificar sua saída usando a ferramenta de classificação gnu. Provavelmente classificar classifica mais rápido que a função buitin classificada em python.
Sobre o dmesg, foi bem simples encontrar memória insuficiente, basta sudo dmesg | less
pressionar G
para ir até o fim, do ?
que pesquisar de volta, do que pesquisar por Out
string. Encontrei dois deles, um para o meu script python e outro para o meu tipo, aquele que iniciou esta pergunta. Aqui estão essas saídas:
[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
É isso, muito obrigado por ajudar até agora, espero que ajude os outros também.
TL;DR: falta de memória-killer ou falta de espaço em disco para arquivos temporários kills
sort
. Recomendação: Use uma ferramenta diferente.Eu dei uma olhada no GNU coreutils
sort.c
agora¹. Seu-S 1G
apenas significa que osort
processo tenta alocar memória em um pedaço de 1 GB e voltará para tamanhos cada vez menores se isso não for possível.Depois de esgotar esse buffer, ele criará um arquivo temporário para armazenar as linhas já classificadas² e classificará o próximo bloco de entrada na memória.
Depois que toda a entrada for consumida,
sort
mesclará/classificará dois dos arquivos temporários em um arquivo temporário (estilo mergesort) e mesclará sucessivamente todos os temporários até que a mesclagem produza a saída total classificada, que é então enviada parastdout
.Isso é inteligente, porque significa que você pode classificar a entrada maior do que a memória disponível.
Ou é inteligente em sistemas em que esses arquivos temporários não são mantidos na RAM, o que normalmente são atualmente (
/tmp/
normalmente é umtmpfs
, que é um sistema de arquivos somente RAM). Então, escrever esses arquivos temporários consome exatamente a RAM que você está tentando salvar e está ficando sem RAM: seu arquivo tem 160 milhões de linhas, e um rápido google sugere que são 11 GB de dados não compactados.Você pode "ajudar"
sort
com isso alterando o diretório temporário que ele usa. Você já está fazendo isso,-T.
, colocando os arquivos temporários em seu diretório atual. Pode ser que você esteja ficando sem espaço lá? Ou é esse diretório atualtmpfs
ou similar?Você tem um arquivo CSV com uma quantidade média de dados (160 milhões de linhas não são tantos dados para um PC moderno). Em vez de colocar isso em um sistema destinado a lidar com tantos dados, você está tentando operá-lo com ferramentas da década de 1990 (sim, acabei de ler o
sort
histórico do git), quando 16 MB de RAM pareciam bastante generosos.CSV é apenas o formato de dados errado para processar qualquer quantidade significativa de dados, e seu exemplo é a ilustração perfeita disso. Ferramentas ineficientes trabalhando em uma estrutura de dados ineficiente (um arquivo de texto com linhas) de maneiras ineficientes para atingir uma meta com uma abordagem ineficiente:
Você só quer saber com que frequência cada valor apareceu na segunda coluna. Classificar isso antes só acontece porque sua ferramenta (
uniq -c
) é ruim e precisa que as linhas sejam classificadas antes (literalmente não há uma boa razão para isso. Apenas não está implementado que ela possa conter um mapa de valores e sua frequência e aumentar isso à medida que eles aparecer).Então, talvez este seja um bom momento para lhe dizer que, não, não use um fluxo de dados baseado em CSV. Um simples
e nesse shell (supondo que seu CSV tenha uma linha de título que o SQLite possa usar para determinar as colunas) (é claro, substitua
$second_column_name
pelo nome dessa coluna)é provável que seja tão rápido, e bônus, você obtém um arquivo de banco de dados real
place.sqlite
. Você pode brincar com isso de forma muito mais flexível – por exemplo, crie uma tabela onde você extraia coordenadas e converta os tempos em registros de data e hora numéricos e, em seguida, seja muito mais rápido e flexível com o que você analisa.¹ Os globais e a inconsistência sobre o que é usado quando. Eles magoam. Foi uma época diferente para os autores C. E definitivamente não é C ruim, apenas... não é o que você está acostumado em bases de código mais modernas. Obrigado a Jim Meyering e Paul Eggert por escrever e manter esta base de código!
² você pode tentar fazer o seguinte: ordenar um arquivo que não seja muito grande, digamos,
ls.c
com 5577 linhas, e registre o número de arquivos abertos:A resposta de @MarcusMüller é clara o suficiente sobre "Quem matou meu tipo?". E você confirmou o problema.
No entanto, a segunda parte não foi muito abordada: ou Como contar valores distintos de forma eficiente de uma coluna csv . Além de tentar encontrar maneiras melhores/mais rápidas de classificar.
Isso porque seus pipes (todos) foram baseados no uso do
uniq
. Euniq
requer dados classificados.existe alguma outra solução?
Sim. Construa uma matriz com os dados da coluna 2 como chave e adicione 1 cada vez que esse valor for encontrado. Essa é a maneira usual em que o awk processa dados:
Isso não precisa reter todo o arquivo na memória como o sort. Mas apenas a lista de chaves (como a
'kgZoJz//JpfXgowLxOhcQlFYOCm8m6upa6Rpltcc63K6Cz0vEWJF/RYmlsaXsIQEbXrwz+Il3BkD8XZVx7YMLQ==\n'
que você mostrou e um float para a contagem).Isso processará cada linha do arquivo uma vez e na ordem em que aparecem, sem necessidade de classificação para contar usuários únicos. Mas sim, é necessária uma classificação final para classificar as contagens.
Assim, o tempo para processar o arquivo será proporcional ao
n
invés do tempo para ordenarn*log(n)
e o uso de memória será proporcional ao número de usuários 'm' (segundo campo uniq keys).Se a contagem média de cada usuário for 350 (assumindo que ~795 é o máximo, 1 é o mínimo e a contagem vai linearmente entre as duas contagens), então o tamanho da memória usada deve ser proporcional a 88 (tamanho de uma chave ) vezes 160353104/350 (número de chaves distintas), ou menos de 40 megabytes mais alguma sobrecarga.