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.