장비 로그를 일별로 묶는 Python 집계 배치 daily_rollup.py 가 메모리를 많이 써서 컨테이너 한도에 가까웠다. 코드에 재귀 호출이 있어서 그것 때문이라고 생각했다.
Table of contents
Open Table of contents
해 본 것 — 스위치로 기여를 분리했다
고치기 전에 그 부분의 기여만 따로 재기로 했다. 마침 --no-recurse 로 재귀를 켜고 끄는 인자가 있었다. 켠 것과 끈 것의 최대 사용량을 tracemalloc.get_traced_memory() 의 peak 로 각각 쟀다.
차이는 백몇십 메가였고 전체 사용량에 비하면 작은 몫이었다. 추측한 원인이 실제로 얼마를 차지하는지는 그것만 빼고 한 번 돌려 보면 바로 나온다.
원인 — 기저와 집계 본체
그러면 나머지가 어디서 오는지가 남는다. load_input() 직후의 사용량을 재 보니 시작하기도 전에 1기가에 가까웠다.
거기에 집계 본체가 대부분을 더하고 있었다. 최적화 대상이 재귀가 아니라 집계 본체라는 것이 이 분해로 확정됐다.
막힌 지점 — 입력 로드가 남기는 것
입력 파일 자체는 백몇십 메가였는데 로드 직후가 1기가였다. 나머지는 json.loads 로 파싱하면서 문자열을 만들고 버리는 과정의 할당자 잔류였다.
실제로 안 쓰는데 운영체제에 돌려주지 않는 몫이라 코드를 줄여도 안 없어진다. 파싱 방식을 바꾸거나 for line in f 로 한 줄씩 읽어야 줄어드는 종류였다.
주의 — 계측 도구가 보는 범위
Python 공식 문서대로 tracemalloc 은 Python 이 할당한 메모리 블록을 추적한다. 수치 배열처럼 다른 경로로 잡히는 것은 안 나온다.
그래서 resource.getrusage 의 ru_maxrss 와 도구가 보고한 값의 차이가 그 몫이 된다. 이 데이터는 대부분 문자열이라 잘 잡혔지만 수치 위주였으면 도구만 보고 판단했을 때 크게 틀렸을 것이다.
검증 — 최대치와 증분이 답하는 질문
이 도구에는 Snapshot.compare_to 로 두 시점을 비교하는 기능이 있다. 먼저 그것을 썼는데 증가분이 아주 작게 나왔다. 그 기능은 반환 뒤에도 살아남은 것만 보여 주므로 중간에 썼다가 해제한 것은 안 나온다.
한도를 볼 때는 peak 를 봐야 하고 누수를 찾을 때는 compare_to 의 증분을 본다. 둘 다 유효한 지표인데 답하는 질문이 다르므로 무엇을 알고 싶은지에 따라 골라야 한다.
이 측정으로 아무리 줄여도 남는 하한이 있다는 것도 드러났다. 집계 코드를 최적화해도 입력 잔류가 그대로면 전체가 그만큼 아래로는 안 내려간다. 그래서 집계 본체보다 스트리밍 읽기를 먼저 하는 쪽을 골랐다. 이유는 하한을 내리지 않으면 본체를 줄여도 한도와의 거리가 안 벌어져서다. 대신 파일을 한 번에 읽던 단순한 구조를 잃는 것을 감수했다.
정리
- 원인을 추측하지 말고 스위치로 기여분을 분리해 잰다
- 그것만 빼고 돌려 보면 실제 몫이 바로 나온다
- 입력 로드만으로 큰 기저가 생긴다
- 할당자 잔류는 코드를 줄여도 안 없어진다
- 계측 도구가 무엇을 보는지 알고 쓴다
- 도구가 안 잡는 영역은 프로세스 전체 값과의 차이로 본다
- 최대치와 증분은 다른 질문의 답이다
- 하한 구조를 알아야 줄였는데 왜 그대로냐에 답할 수 있다