![]()
`OrderedDict.popitem()`을 돌렸는데 메모리가 내려오지 않는다: PR #146537이 손본 ‘가능한 누수’의 실전 증상
meta_description: CPython PR #146537은 OrderedDict의 popitem() 경로에서 발생할 수 있는 메모리 누수 가능성을 수정한다. 반복 popitem을 쓰는 캐시/큐/스케줄러에서 “요소는 지웠는데 RSS가 안 내려가는” 현상이 왜 나올 수 있는지, 어떤 버전에서 조심해야 하는지, 그리고 운영 코드에서 진단(tracemalloc/heap snapshot)과 완화(업그레이드, 패턴 변경)를 실무적으로 정리한다. meta_keywords: python, OrderedDict, popitem, memory leak, RSS, tracemalloc, CPython, cache, queue, eviction, dict, reference, practical, 운영, 진단 meta_robots: index,follow
운영에서 메모리 이슈는 늘 불쾌하다. 특히 더 불쾌한 건 이런 형태다.
- 캐시에서 항목을 “계속 지운다”
- 모니터링 그래프에서 객체 개수도 줄어든다
- 그런데 RSS가 안 내려온다(혹은 아주 천천히만)
이때 팀은 서로 다른 결론으로 갈라진다.
- “파이썬은 원래 메모리 반환을 안 해”
- “어딘가 참조가 남았어(진짜 leak)”
- “컨테이너가 찢어져서 조각난 거야(fragmentation)”
정답은 케이스 바이 케이스인데, 이런 논쟁의 바닥엔 늘 같은 질문이 있다.
“내 코드가 지운 게 진짜로 지워졌나?”
CPython PR #146537은 이 질문에 직접 연결되는 작은 수정이다.
[3.14] Fix possible memory leak in OrderedDict popitem
이 글은 PR을 곧이곧대로 요약하기보다, “운영에서 popitem이 등장하는 상황”에 맞춰서 설명한다.
![]()
1) 왜 popitem()이 실무에서 중요하나: LRU 비슷한 걸 다들 한 번은 만든다
OrderedDict.popitem(last=False)는 사실상 “맨 앞(가장 오래된)”을 뽑는 도구다.
- 자체 캐시 eviction
- 작업 큐(먼저 들어온 것부터 처리)
- 최근 N개만 유지하는 로그 버퍼
이런 패턴에서 흔히 이렇게 쓴다.
from collections import OrderedDict
od = OrderedDict()
# ... 쌓고
while len(od) > LIMIT:
key, value = od.popitem(last=False)
이때 사람의 기대는 명확하다.
- popitem 했으니 그 엔트리는 없어졌고
- value 쪽 참조도 끊겼고
- 반복하면 메모리도 어느 정도 회수될 것이다
PR #146537은 바로 이 “참조가 제대로 끊기느냐” 쪽의 가능 누수를 다룬다.
2) PR #146537의 메시지(실무 번역): ‘지웠는데 남는 참조’는 최악이다
diff는 작지만 메시지는 크다.
- popitem 경로에서 가능한 누수(possible leak)가 있었다
- 그래서 그 경로를 수정했다
여기서 “possible”이 중요한 이유는,
- 모든 코드에서 항상 leak이 보이는 게 아니라
- 특정 반복 패턴/특정 GC 타이밍/특정 workload에서
- 오래 돌리면 “어딘가 찝찝한 증가”로 보일 수 있다는 뜻이기 때문이다.
이런 류의 bugfix는 운영자 입장에서는 아주 가치가 있다.
- 내가 잘못한 게 아닐 수 있다
- 업그레이드로 사라질 수 있다
- 그리고 “진단할 가치”가 생긴다
3) 현장에서 어떻게 보이나: ‘요소 개수는 맞는데 RSS가 찜찜하게 오른다’
OrderedDict 기반 eviction에서 가장 흔한 증상은 이거다.
len(od)는 LIMIT 근처로 유지된다- GC도 잘 돈다(major GC 로그가 정상)
- 그런데 RSS가 완만하게 오른다
이때 단순히 “파이썬은 메모리 반환을 안 한다”로 끝내면 위험하다.
- 진짜로는 참조가 남아 있을 수 있고
- 그 남은 참조가 특정 객체(큰 bytes/큰 list)를 붙잡고 있을 수 있다
그래서 나는 이런 이슈를 보면 두 단계를 나눈다.
1) 파이썬 객체가 남아 있나? (heap 관점) 2) 객체는 없어도 RSS가 안 내려오나? (allocator/fragmentation 관점)
PR #146537은 1)의 가능성을 줄이는 종류다.
4) 빠른 진단 루틴(운영에서 바로 쓰는 것들)
4-1) tracemalloc로 “누가 남는지” 본다
import tracemalloc
tracemalloc.start()
# workload 실행
snap = tracemalloc.take_snapshot()
for stat in snap.statistics('lineno')[:10]:
print(stat)
여기서 popitem을 반복하는 루프 주변에서 할당이 계속 누적되면, leak 가능성이 올라간다.
4-2) value가 큰 타입인지부터 체크한다
popitem 자체가 leak이 아니어도, value가 다음 패턴이면 RSS가 튄다.
- 큰 bytes (예: 압축/이미지/바이너리)
- 큰 문자열
- 큰 리스트/딕셔너리
그리고 그런 value를 로그/메트릭/예외 객체에 붙여두면 “지웠는데 남는” 착시가 생긴다.
4-3) ‘참조를 내가 붙잡고 있지 않나’ 먼저 의심한다
실무에서 제일 많은 leak은 popitem이 아니라 이런 것들이다.
seen.append(value)같은 디버그 코드- 예외를 리스트에 쌓는 에러 수집기
- 큐에 넣고 소비를 못 하는 백프레셔 붕괴
PR이 있다고 해서 내 코드가 무죄가 되는 건 아니다. 다만, PR이 있으면 “업그레이드로 사라지는지”를 테스트할 이유가 생긴다.
5) 완화 전략(가장 현실적인 순서)
1) 해당 PR이 포함된 버전으로 업그레이드 - 이게 가장 싸고 확실한 옵션이다.
2) OrderedDict eviction 패턴을 단순화 - 예: 지나치게 잦은 popitem을 줄이고 배치로 evict
3) 컨테이너 교체 고려
- 단순 LRU라면 functools.lru_cache 또는 검증된 캐시 라이브러리 사용
4) “RSS가 안 내려오는 게 정상인지”를 문서화 - 파이썬 메모리 할당자는 OS에 즉시 반환하지 않을 수 있다 - 하지만 그건 “heap이 안 남는다”는 전제가 있어야 안전하다
5) 재현을 ‘작게’ 만들어서 팀 논쟁을 끝내기
메모리 이슈는 감정이 붙으면 오래 간다. 그래서 나는 가능하면 최소 재현을 만든다.
- LIMIT=1000 정도로 고정
- value는 큰 bytes로 고정(예:
b'x' * 1_000_000같은) od[key] = value→popitem(last=False)루프를 충분히 돌린다
가능하면 “측정”도 같이 묶는다.
import os
import tracemalloc
from collections import OrderedDict
tracemalloc.start()
LIMIT = 1000
od = OrderedDict()
for i in range(50_000):
od[i] = b'x' * 100_000
if len(od) > LIMIT:
od.popitem(last=False)
snap = tracemalloc.take_snapshot()
print(snap.statistics('filename')[:3])
print('pid', os.getpid())
그리고 아래 두 값을 같이 본다.
- heap이 정말로 줄어드는지(tracemalloc)
- RSS가 어떤 패턴으로 움직이는지
이게 있으면 “파이썬은 원래 그래” vs “진짜 leak이야” 논쟁을 빠르게 끝낼 수 있다.
(코드는 예시일 뿐이고, 운영 환경에서 그대로 돌리라는 뜻은 아니다. 중요한 건 ‘작게 재현 + 같이 측정’이다.)
즉, heap leak인지 먼저 분리하고, 그 다음에 allocator 특성을 논의하는 게 순서다.
6) 마무리
PR #146537은 작은 수정이지만, 메시지는 명확하다.
OrderedDict.popitem()같은 기본 도구도- 특정 경로에선 “가능한 누수”가 있을 수 있고
- 그건 업그레이드로 해결될 수 있다
운영에서 내가 가져가는 결론은 이거다.
- popitem 기반 eviction을 쓰는 서비스라면
- 메모리 그래프가 찜찜할 때 “내 코드만” 탓하지 말고
- CPython 릴리즈 노트/백포트 PR도 같이 보는 게 시간 절약이다
Keywords
python,ordereddict,popitem,leak,memory,RSS,tracemalloc,cache,eviction,queue,gc,reference,heap,allocator,production,bugfix,3.14,collections
References
- CPython PR #146537
- https://github.com/python/cpython/pull/146537
- Diff
- https://github.com/python/cpython/pull/146537.diff
이미지 크레딧/라이선스
- Laptop Programmcode.jpg — Negative Space / CC0
- https://commons.wikimedia.org/wiki/File:Laptop_Programmcode.jpg
- http://creativecommons.org/publicdomain/zero/1.0/deed.en
댓글
댓글 쓰기