누가 내 종족을 죽였나요? 또는 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

그리고 해당 셸에서(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

아주 아주 빠르게 진행되며 tqdm30분 미만이 될 것으로 예상되지만 99%에 진입하면 점점 느려집니다. 이 프로세스는 약 1.7GIB 정도의 많은 RAM을 사용합니다. 제가 이 데이터를 처리하고 있는 머신, 즉 충분한 스토리지가 있는 머신은 RAM이 2GiB이고 스토리지가 ~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 스왑 파티션을 활성화하고 다시 실행해 보는 것이었습니다. 또한 어느 시점에서 10GiB RAM을 사용하는 tqdm을 본 것 같습니다. 그러나 오류가 표시되거나 출력이 10MiB만 표시하므로 출력이 혼란스럽다고 확신합니다 btop. tqdm이 new 를 읽는 중이므로 그렇게 많은 메모리를 사용하고 있다고 생각하지 마세요 \n.

이 문제에 대한 Stéphane Chazelas의 의견에서 그들은 다음과 같이 말합니다.

시스템 로그를 통해 알 수 있습니다.

더 알고 싶은데, Journalctl에서 뭔가를 찾아봐야 할까요? 그렇다면 어떻게 해야 합니까?

아무튼 다음과 같이마커스 뮬러 답변즉, CSV를 Sqlite에 로드하는 것이 아마도 지금까지 가장 현명한 솔루션일 것입니다. 이를 통해 데이터를 여러 방법으로 조작할 수 있고 메모리 부족 없이 이 데이터를 가져올 수 있는 몇 가지 현명한 방법이 있을 수 있기 때문입니다.

하지만 이제 프로세스가 종료된 이유를 알아내는 방법이 궁금합니다. 왜냐하면 나는 내 프로세스를 이해하고 싶기 때문입니다. sort -T. -S1G이제 내 프로세스를 이해하고 싶습니다 ./distinct. 마지막 것은 거의 확실하게 메모리와 관련이 있다는 것입니다. 그렇다면 로그를 확인하여 이러한 프로세스가 종료된 이유를 확인하려면 어떻게 해야 합니까?

업데이트 2

그래서 SWAP 파티션을 활성화하고마커스 뮐러이 질문에 대한 의견은 다음과 같습니다. Python collection.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을 사용하십시오. 또한 이 스크립트를 더 빠르게 만들 수 있는 방법이 있는지 알고 싶습니다. 스레드 풀을 생성하지만 Python의 dict 동작을 확인해야 할 수도 있고, 내가 계산하는 열이 고정 너비 문자열이므로 다른 데이터 구조도 고려해야 하며, 목록을 사용하여 각각의 다른 user_hash의 빈도를 저장할 수도 있습니다. 나는 또한 이전 구현과 거의 동일한 사전인 Counter의 Python 구현을 읽었지만 justused 대신 dict.setdefault이 경우 실제로 dict[key] = dict.get(key, 0) + 1오용할 필요가 없습니다.setdefault

업데이트 3

그래서 나는 토끼굴에 빠졌고 목표에 대한 집중력을 완전히 잃었습니다. C나 Rust를 작성하는 등 더 빠른 정렬을 찾기 시작했지만 처리하려는 데이터가 이미 처리되었다는 것을 깨달았습니다. 여기에서는 dmesg 출력과 Python 스크립트에 대한 마지막 팁을 보여드리겠습니다. 힌트: 출력을 정렬하기 위해 gnu 정렬 도구를 사용하는 것보다 계산을 위해 dict 또는 Counter를 사용하는 것이 더 나을 수 있습니다. 정렬은 Python 정렬 기능보다 빠를 수 있습니다.

dmesg와 관련하여 메모리를 찾는 것은 매우 간단합니다. 문자열을 검색하는 대신 뒤로가 아니라 끝까지 sudo dmesg | less누르기 만 하면 됩니다. 그 중 두 개를 찾았습니다. 하나는 내 Python 스크립트용이고 다른 하나는 내 종류용인데, 이 문제를 일으키는 것입니다. 출력은 다음과 같습니다.G?Out

[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

핵심요약: 임시 파일에 대한 메모리 부족 킬러 또는 디스크 공간 부족 킬러 sort. 권장사항: 다양한 도구를 사용하세요.


sort.c이제 GNU coreutils'를 탐색했습니다. 이는 -S 1G단순히 sort프로세스가 1GB 메모리 블록을 할당하려고 시도하고 이것이 불가능할 경우 점점 더 작은 크기로 폴백한다는 의미입니다.

해당 버퍼가 소진되면 정렬된 라인²을 저장하기 위한 임시 파일을 생성하고 메모리의 다음 입력 블록을 정렬합니다.

모든 입력이 소비된 후 sort두 개의 임시 파일이 하나의 임시 파일로 병합/정렬되고(mergesort 스타일) 모든 임시 파일은 병합으로 인해 전체 정렬된 출력이 생성될 때까지 계속 병합되어 stdout.

이는 사용 가능한 메모리보다 더 큰 입력을 정렬할 수 있다는 의미이므로 영리합니다.

/tmp/또는 이러한 임시 파일 자체가 RAM( 일반적으로 RAM 전용 파일 시스템) 에 보관되지 않는 시스템에서는 tmpfs이것이 현명합니다 . 따라서 이러한 임시 파일을 작성하는 것은 저장하려는 RAM을 차지하며 RAM이 부족해집니다. 파일에는 1억 6천만 개의 행이 있으며 Google에서 검색한 결과 11GB의 비압축 데이터가 표시됩니다.

sort사용하는 임시 디렉토리를 변경하여 이 문제를 해결하는 데 "도움"을 줄 수 있습니다. 이미 이 작업을 수행하여 -T.임시 파일을 현재 디렉터리에 배치했습니다. 공간이 부족하지 않을까요? 아니면 현재 디렉토리가 tmpfs비슷합니까?

적당한 양의 데이터가 포함된 CSV 파일이 있습니다(1억 6천만 행은저것최신 PC는 데이터 용량이 큽니다. 대량의 데이터를 처리하도록 설계된 시스템에 넣는 대신 16MB RAM이 꽤 넉넉해 보였던 1990년대의 도구를 사용하여 sort작업하려고 합니다(예, 방금 git 기록을 읽었습니다).

CSV는 단지데이터 형식 오류많은 양의 데이터 작업에 대해 귀하의 예는 이를 완벽하게 보여줍니다. 비효율적인 도구는 목표를 달성하기 위해 비효율적인 데이터 구조(줄이 있는 텍스트 파일)를 비효율적인 방식으로 처리합니다.

각 값이 두 번째 열에 얼마나 자주 나타나는지 알고 싶을 뿐입니다. 이전 정렬은 도구( uniq -c)가 형편없고 이전에 행을 정렬해야 하기 때문입니다(실제로 타당한 이유는 없습니다. 값과 해당 빈도의 맵을 보유하고 증가할 수 있다는 점은 구현되지 않았습니다. 발생) 나타남).


따라서 이제는 CSV 기반 데이터 스트림을 사용하지 마십시오. 간단한

sqlite3 place.sqlite

그리고 해당 셸에서(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에게 감사드립니다!

² 다음을 시도해 볼 수 있습니다. 예를 들어 5577줄과 같이 너무 크지 않은 파일을 정렬하고 ls.c열린 파일 수를 기록합니다.

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(고유 키 수), 또는 40MB 미만에 약간의 오버헤드를 더한 값입니다.

관련 정보