고객 데이터 다루기 · 다 못 올리는 파일 · 실습
노트북에서는 됐는데 파드에서 죽는다
목표
큰 파일을 메모리에 올리지 않고 다루는 도구 stream.py 를 만들고, 메모리 상한을 걸어 그것이 정말로 스트리밍인지 종료 코드로 증명한다. 집계·상위 N·고유값 세기·외부 정렬·검증을 모두 손에 쥐는 것의 개수로 설계한다.
왜 중요한가
파일을 통째로 읽는 코드는 작은 파일에서 더 짧고 더 빠르다. 그래서 처음 쓸 때는 의심할 이유가 없고, 파일이 커지는 것은 우리가 아니라 고객이 결정한다. 컨테이너가 메모리 한도를 넘으면 커널이 프로세스를 죽이는데, 그 죽음은 파이썬 예외로 잡히지 않고 표준출력에도 아무것도 남지 않는다.
설계의 기준은 파일 크기가 아니라 어느 시점에도 손에 쥐고 있는 것의 개수다. 집계는 한 줄, 상위 N 건은 N 개면 된다. 그런데 고유값 세기는 스트리밍이어도 메모리가 값의 가짓수에 비례한다 — 이 차이를 모르면 "스트리밍으로 짰는데 왜 죽죠" 가 된다.
그리고 "돌려 봤더니 되던데요" 는 증명이 아니다. 그때 그 파일에서 됐다는 뜻일 뿐이다. 상한을 걸고 통과해야 증명이고, 그 결과는 문장이 아니라 종료 코드로 남아야 다음 사람이 다시 묻지 않는다.
채점기는 여러분의 문구를 믿지 않는다. 임시 디렉터리에 채점기가 만든 로그를 차려 놓고 여러분의 도구를 메모리 상한 아래에서 실제로 실행해 답을 대조한다. 건수와 금액은 실행마다 바뀐다.
단계
1. /root/stream/gen_events.py 를 만들어 실행해 /root/stream/data/events.csv 를 만드세요. 30만 줄입니다.
2. /root/stream/stream.py 에 agg <파일> 을 만들어 한 줄씩 읽어 건수·합계·평균·최소·최대를 내게 하세요.
3. top <파일> <N> 을 더해 힙으로 상위 N 건만 유지하게 하세요.
4. distinct <파일> <칼럼> 을 더해 고유값을 세고 그때의 최대 메모리를 함께 내게 한 뒤, 가짓수가 적은 칼럼과 많은 칼럼을 비교해 /root/stream/distinct.json 에 적으세요.
5. /root/stream/cap.py 와 /root/stream/slurp.py 를 만들어 메모리 상한 64MiB 아래에서 두 판을 나란히 돌리고 결과를 /root/stream/limit.json 에 적으세요.
6. sortmerge <파일> <칼럼> <청크행수> 를 더해 분할·정렬·병합으로 외부 정렬하게 하세요.
7. verify <파일> <칼럼> 을 더해 정렬 여부와 순서에 무관한 지문을 내게 하고, 원본과 정렬본을 대조해 /root/stream/verify.json 에 적으세요.
8. 전체를 한 장으로 정리해 /root/stream/stream_report.json 과 /root/stream/stream_report.md 를 만드세요.
참고
- 실행 계약:
python3 /root/stream/stream.py <명령> <파일> [인자]. 명령은 agg·top·distinct·sortmerge·verify 다섯입니다. 성공하면 종료 코드 0, 파일이 없으면 3, 명령이나 인자 수가 틀리면 2 입니다. agg응답:rows·sum_amount·mean_amount·min_amount·max_amount·peak_kib. 평균은 소수 둘째 자리에서 반올림합니다.peak_kib는 그 프로세스가 쓴 최대 메모리이고resource.getrusage(resource.RUSAGE_SELF).ru_maxrss로 얻습니다. 리눅스에서 단위는 KiB 입니다.top응답:{"n": 정수, "rows": [[event_id, amount], ...], "peak_kib": 정수}. 금액 내림차순이고, 금액이 같으면 event_id 가 큰 쪽이 위입니다.distinct응답:column·rows·exact·peak_kib.sortmerge응답:rows·chunks·chunk_rows·output·peak_kib. 청크는 입력 파일이 있는 디렉터리의chunks/에 쓰고, 결과는 같은 디렉터리의sorted.csv에 머리글과 함께 씁니다.verify응답:rows·ordered·sum_amount·digest·peak_kib. digest 는 머리글을 뺀 줄마다sha256(줄 전체 바이트)의 앞 8바이트를 정수로 읽어 전부 더한 값을 2의 64제곱으로 나눈 나머지이고, 16자리 소문자 16진수로 냅니다. 순서가 달라도 같은 값이 나와야 합니다.- 정렬 키는 값이 정수 모양이면
(정수, 원래 문자열), 아니면(0, 원래 문자열)로 봅니다. 이 규칙은 이 실습의 가정입니다. python3 /root/stream/cap.py <MiB> <명령> [인자...]는 그 명령을 주소 공간 상한 아래에서 돌리고{"limit_mib": 정수, "argv": [...], "exit_code": 정수, "ok": true|false}를 냅니다. cap.py 자신은 자식이 죽어도 0 으로 끝납니다.python3 /root/stream/slurp.py <파일>은 파일을 통째로 읽어 리스트에 담는 판이고, 상한 아래에서 떨어지라고 만드는 대조군입니다.- 공식 문서: [heapq](https://docs.python.org/3/library/heapq.html) · [resource](https://docs.python.org/3/library/resource.html) · [hashlib](https://docs.python.org/3/library/hashlib.html) · [POSIX sort](https://pubs.opengroup.org/onlinepubs/9699919799/utilities/sort.html)
- 흔한 실수:
read()나readlines()로 시작하기, 전체를 정렬한 뒤 앞을 자르기, 검증하려고 원본과 결과를 둘 다 리스트에 올리기, 청크 파일을 안 지우기. - 파드 자원은 CPU 2코어·메모리 2Gi·임시 디스크 6Gi 입니다. 더 큰 파일을 만들지 마세요.
단계 8개
- 한 번에 못 읽는 파일 만들기
- 한 줄씩 읽어 집계하기
- 상위 N 건만 손에 쥐기
- 고유값 세기의 메모리는 어디에 비례하나
- 상한을 걸어 증명하기
- 분할·정렬·병합으로 외부 정렬하기
- 중간 산출물 없이 검증하기
- 증명을 한 장으로 남기기