데이터 파이프라인 · 파티션과 파일 크기 · 실습
파일이 놓인 모양이 조회 비용을 정한다 — 파티션과 묶기
목표
파일 시스템 위의 파티션 레이크를 다루는 도구 pq.py 를 만든다. 키를 바꿔 가며 파일 수와 크기 분포를 재고, 파티션 키에 걸린 조건이 여는 파일 수를 얼마나 줄이는지 재고, 묶기를 돌리고, 묶는 도중에 읽는 쪽이 무엇을 보는지 확인하고, 키를 바꾸는 비용을 잰다.
왜 중요한가
같은 행을 넣어도 파일이 어떻게 놓여 있느냐에 따라 조회가 여는 파일 수가 수십 배 달라진다. 파티션은 값마다 디렉터리를 나눠 두는 것이고, 열지 않은 파일은 비용이 0 이다. 다만 그 이득은 디렉터리 이름에 들어 있는 칸으로 거를 때만 생긴다.
그렇다고 자주 쓰는 칸을 모두 키로 넣으면 파티션 수가 값의 곱만큼 늘어나고 파일 하나가 몇 줄짜리가 된다. 파일을 여는 고정 비용이 내용을 읽는 비용보다 커지고, 목록의 항목 수가 폭발하고, 압축이 먹지 않는다. 그래서 키를 고르는 일은 언제나 맞바꿈이다.
작은 파일이 쌓이면 묶기를 돌린다. 어려운 것은 묶는 일 자체가 아니라 그 도중에 읽는 사람이 무엇을 보는가다. 디렉터리를 훑어 파일을 모으는 독자는 옛 파일과 새 파일을 모두 집어 같은 줄을 두 번 센다. 읽는 쪽이 목록을 보게 만들고, 목록 교체를 마지막에 한 번만 하면 그 사이가 없어진다.
Parquet 은 쓰지 않는다. 실습 이미지에 pyarrow 가 없고 파드는 런타임 설치를 할 수 없다. 행 그룹 같은 개념은 읽기에서 공식 문서로 다뤘고, 여기서는 파티션 디렉터리와 매니페스트와 JSON Lines 로 같은 구조를 손으로 만든다.
채점기는 여러분이 적어 낸 문구를 믿지 않는다. 임시 작업 폴더에 채점기가 만든 원본을 차려 놓고 여러분의 도구를 실제로 돌려, 매니페스트에 적힌 크기가 디스크의 실제 크기와 맞는지, 줄이 보존되는지, 잘라 낸 파일 수가 맞는지를 본다. 원본과 목표 크기는 실행마다 바뀐다.
단계
1. /root/parts/gen_orders.py 를 만들어 실행해 /root/parts/work/raw.jsonl 을 만드세요.
2. /root/parts/pq.py 에 write 를 만들어 키 하나로 쪼갠 레이크와 매니페스트를 만들게 하세요.
3. layout 을 더해 파일 수와 크기 분포와 작은 파일 개수를 내게 하세요.
4. write 가 키를 여럿 받고 한 파일의 줄 수를 제한할 수 있게 해 잘게 쪼갠 레이크를 만드세요.
5. query 를 더해 파티션 키에 걸린 조건으로 열 파일을 잘라 내게 하세요.
6. compact 를 더해 한 파티션 안의 작은 파일을 목표 크기에 가깝게 묶게 하세요.
7. --crash=before-swap 과 --via=glob 을 더해 묶는 도중에 읽는 쪽이 무엇을 보는지 확인하세요.
8. repartition 을 더해 키를 바꿔 다시 쓰고 /root/parts/work/partition_report.md 에 비용을 적으세요.
참고
- 작업은 전부
/root/parts아래에서 합니다. 작업 폴더는/root/parts/work이고 원본은 그 안의raw.jsonl입니다. - 레이크 하나는 작업 폴더 아래 디렉터리 하나입니다. 그 안에 파티션 디렉터리와 매니페스트
_manifest.json이 들어갑니다. - 파티션 디렉터리 이름은
칸=값이고, 키가 여럿이면 키 차례대로 겹쳐 쌓습니다. 예:day=2026-01-03/region=seoul/. - 파티션 파일 이름은
part-로 시작하고.jsonl로 끝납니다. 한 줄이 원본의 한 줄입니다. - 매니페스트:
{"lake": 이름, "key": [칸 이름], "generation": 정수, "files": [{"path": 레이크 기준 상대 경로, "partition": {칸: 값}, "rows": 정수, "bytes": 정수}]}. files 는 path 오름차순이고, bytes 는 디스크의 실제 파일 크기여야 합니다. - 실행 계약:
python3 /root/parts/pq.py <명령> <작업폴더> [...]. 답은 JSON 한 덩어리로 표준출력에 냅니다. 성공하면 종료 코드 0, 작업 폴더나 매니페스트가 없으면 3, 사용법이 틀리면 2, 일부러 죽는 길로 들어가면 9 입니다. write <작업폴더> --lake=<이름> --key=<칸[,칸]> [--rows=<줄 수>]응답:{"lake": 이름, "key": [칸], "partitions": 정수, "files": 정수, "rows": 정수, "bytes": 정수}.--rows는 한 파일에 담을 최대 줄 수이고, 없거나 0 이면 파티션마다 파일 하나입니다. 같은 이름의 레이크가 이미 있으면 지우고 새로 씁니다.layout <작업폴더> --lake=<이름> --small=<바이트>응답:{"lake": 이름, "files": 정수, "partitions": 정수, "rows": 정수, "bytes": 정수, "avg_bytes": 정수, "p50_bytes": 정수, "min_bytes": 정수, "max_bytes": 정수, "small_files": 정수}. avg_bytes 는 전체 바이트를 파일 수로 나눈 몫(버림)이고, p50_bytes 는 파일 크기의 가장 가까운 순위 중앙값이며, small_files 는 크기가--small미만인 파일 수입니다.query <작업폴더> --lake=<이름> --where=<칸=값[,칸=값]> [--via=manifest|glob]응답:{"lake": 이름, "via": 문자열, "files_total": 정수, "files_scanned": 정수, "rows": 정수, "amount": 정수}. 값은 문자열로 견줍니다. 파티션 키에 있는 칸으로만 파일을 잘라 낼 수 있습니다 — 키에 없는 칸이 조건에 있으면 그 조건으로는 아무것도 못 자르고 파일을 열어 봐야 압니다.--via=glob은 매니페스트를 무시하고 디렉터리에서part-*.jsonl을 훑는 읽기입니다.compact <작업폴더> --lake=<이름> --target=<바이트> [--crash=before-swap]응답:{"lake": 이름, "before_files": 정수, "after_files": 정수, "partitions": 정수, "rows": 정수, "bytes": 정수}. 한 파티션 안의 파일을 path 차례대로 이어 붙이되, 다음 파일을 더하면 목표를 넘길 때 거기서 끊습니다(한 파일은 아무리 커도 혼자 하나가 됩니다). 새 파일을 모두 쓴 다음에 매니페스트를 바꿔 달고, 그다음에 옛 파일을 지웁니다. 새 파일 이름은 옛 이름과 겹치면 안 됩니다.--crash=before-swap은 새 파일을 다 쓴 뒤 매니페스트를 바꿔 달기 직전에 종료 코드 9 로 죽습니다. 옛 파일도 옛 매니페스트도 그대로 둡니다.repartition <작업폴더> --from=<이름> --to=<이름> --key=<칸[,칸]>응답:{"from": 이름, "to": 이름, "rows_read": 정수, "rows_written": 정수, "files_before": 정수, "files_after": 정수, "partitions_after": 정수, "bytes_read": 정수, "bytes_written": 정수}. 원본 파일이 아니라--from레이크의 매니페스트를 따라 읽습니다.- 보고서 MD 의 절 제목은
## 무엇을 어떻게 쪼갰나## 작은 파일 문제## 묶기## 파티션을 바꾸는 비용입니다. - 공식 문서: [Parquet Concepts](https://parquet.apache.org/docs/concepts/) · [Parquet Configurations](https://parquet.apache.org/docs/file-format/configurations/) · [Parquet File Format](https://parquet.apache.org/docs/file-format/) · [os.replace](https://docs.python.org/3/library/os.html)
- 흔한 실수: 매니페스트의 bytes 를 실제 크기가 아닌 계산값으로 적기, 키에 없는 칸으로 잘라 냈다고 세기, 매니페스트를 먼저 바꿔 달고 파일을 나중에 쓰기, 묶은 새 파일에 옛 이름을 그대로 쓰기.
- 파일이 어떻게 놓였는지 눈으로 보려면
find /root/parts/work/<레이크> -name 'part-*' | head와du -a를 쓰세요.
단계 8개
- 파티션 키 후보가 여럿인 원본 만들기
- 키 하나로 쪼개고 목록을 남기기
- 파일이 어떤 크기로 놓였는지 재기
- 잘게 쪼개 작은 파일 만들어 보기
- 열지 않은 파일은 비용이 0 이다
- 작은 파일 묶기
- 묶는 도중에 읽는 사람이 보는 것
- 키를 바꾸는 값 매기기