![]()
CPython: 서브인터프리터 데이터 공유에서 ‘할당 실패’ 한 번이 누적 메모리 누수로 이어질 수 있던 경로(gh-147960)
meta_description: CPython의 cross-interpreter data 공유 코드에서 튜플을 공유 데이터로 바꾸는 과정(_tuple_shared) 중 메모리 할당이 실패하면, 에러는 MemoryError로 떨어지지만 일부 구조체가 해제되지 않아 누수가 생길 수 있었다. gh-147960은 실패 경로에서 shared 구조체를 RawFree 해 누수를 막는다. 이 글은 “왜 이런 한 줄이 중요해지는지”, 서브인터프리터/임베딩/확장모듈 개발자가 놓치기 쉬운 실패 경로 설계, 테스트/관측 포인트를 실무 관점으로 정리한다. meta_keywords: python,cpython,subinterpreters,cross-interpreter data,_tuple_shared,MemoryError,allocation failure,memory leak,PyMem_RawFree,PyMem_Calloc,error path,임베딩,확장모듈,안정성,회귀,테스트,관측,메모리 meta_robots: index,follow
CPython PR 하나가 병합됐다.
- 제목은 소박하다: “할당 실패 시 _tuple_shared()에서 메모리 누수 방지”
- diff는 더 소박하다: 한 줄
하지만 이런 종류의 패치는, 특정 환경에서는 체감 임팩트가 크다.
- 서버가 오래 떠 있고
- 서브인터프리터(subinterpreters)를 많이 돌리거나
- 임베딩/확장 모듈이 많고
- 메모리 압박 상황(컨테이너 제한/스파이크)이 가끔 생기는 곳
여기서는 “드물게 한 번 터지는 할당 실패”가 누적으로 번지는 문제가 되기 쉽다.
![]()
1) 무엇이 바뀌었나: 실패 경로에서 구조체를 하나 더 해제한다
PR에서 바뀐 코드는 이거다.
- 튜플 공유 데이터를 만들면서
shared->items배열을PyMem_Calloc()으로 잡는다 - 그런데 그 할당이 실패하면
PyErr_NoMemory()로 MemoryError를 세팅하고-1로 실패를 반환한다
여기까지는 정상.
문제는 그 직전에 이미 shared 구조체 자체는 할당되어 있었고, items만 실패했을 때 shared가 해제되지 않으면 누수가 된다.
그래서 패치가 추가한 한 줄:
PyMem_RawFree(shared);
정리하면:
- 에러는 그대로 MemoryError
- 동작도 그대로 실패 반환
- 하지만 실패 직전에 잡아둔 메모리는 확실히 반환
이다.
이게 “한 줄인데 중요한 이유”는 다음 섹션에서 나온다.
2) 왜 중요한가: ‘할당 실패’는 0이 아니라, 가끔은 1이 된다
일반적인 개발 환경에서는 메모리 할당 실패를 잘 안 본다.
그래서 많은 코드(특히 내부/저수준)에서 실패 경로가 허술해지기 쉽다.
그런데 운영 환경에서는 아래 조건이 겹치면 할당 실패가 실제로 발생한다.
- 컨테이너 메모리 제한이 빡빡함
- 동시 요청이 순간적으로 몰림
- 메모리 단편화가 쌓임
- (특히) 여러 인터프리터/샌드박스/플러그인 구조로 객체를 많이 만들고 지움
이때 “가끔 한 번의 할당 실패”가 생기고, 그 경로에서 누수가 나면 상황이 이렇게 변한다.
- 메모리 압박 → 할당 실패 발생
- 실패 경로 누수 → 회복이 안 됨
- 누적 → 다음 압박이 더 빨리 옴
- 결국 OOM/재시작으로 이어짐
즉, 누수는 ‘평소엔 안 보이는’ 문제가 아니라 압박이 올 때 폭발하는 문제다.
3) 이 코드는 어디에 있나: cross-interpreter data(서브인터프리터용) 쪽
수많은 사용자에게는 생소한 파일이다.
Python/crossinterp_data_lookup.h 는 이름 그대로 “서브인터프리터 간에 데이터를 안전하게 넘기기 위한 내부 경로”에 가깝다.
여기서 중요한 건:
- 이 코드는 “기능 추가”보다 “안정성”이 더 중요하고
- 오류가 나면 파이썬 예외로 끝나길 기대하지만
- 실패 경로에서 누수가 있으면 서비스 전체가 영향을 받는다는 점이다.
특히 서브인터프리터를 활용하는 쪽(또는 앞으로 활용할 가능성이 있는 쪽)은, 이런 작은 안정성 패치가 누적되어야 운영이 쉬워진다.
4) 실무에서 어떤 팀이 관심 가져야 하나
이 패치가 당장 “내 코드가 느려졌다/빨라졌다”를 만드는 건 아니다.
대신 아래 팀은 관심을 가져야 한다.
(A) 서브인터프리터 기반 아키텍처를 실험 중인 팀
- 플러그인/멀티테넌트 격리를 인터프리터로 해결하려는 경우
- 워커 프로세스를 늘리기보다 내부 격리를 늘리는 방향
이때 cross-interpreter data 공유는 피할 수 없이 등장한다.
(B) 파이썬을 임베딩하는 제품/서버
- C/C++ 앱 안에 Python을 심는 경우
- “한 프로세스가 오래 살아있고” 메모리 프로파일이 중요함
이런 환경에서는 드문 누수도 시간이 지나면 부담이 된다.
(C) 확장 모듈 개발자(특히 메모리/레퍼런스에 민감한 코드)
이 패치가 직접적으로 extension API를 바꾸는 건 아니지만,
- 내부에서는 이런 실패 경로를 계속 다듬고 있다는 신호고
- 확장 모듈 쪽도 “실패 경로 정리”를 습관으로 가져가야 한다는 알림이 된다.
5) 개발자 팁: 실패 경로를 설계할 때 ‘한 덩어리만’ 누수 나도 패턴이 된다
이 PR에서 제일 배우기 좋은 포인트는 이거다.
성공 경로가 아니라, 실패 경로를 끝까지 걷는다.
실전에서 누수는 보통 이런 모양으로 생긴다.
- 큰 구조체 A 할당
- A 안의 배열 B 할당
- B가 실패
- 예외 반환
- A가 해제되지 않음
즉, “A를 free 해야 한다”가 코드 리뷰에서 빠지면 그대로 누수가 된다.
추천 습관 2개
1) 자원 획득 순서를 따라 반대로 해제한다 - A 잡고 → B 잡고 → 실패하면 B는 없고 A는 있음 → A 해제
2) 실패 지점마다 ‘지금까지 잡은 것’을 주석으로 남긴다 - “여기선 shared만 잡혀있다” 같은 메모가 있으면 리뷰가 쉬워진다.
6) 관측(Observability) 관점: 누수는 로그가 아니라 지표에서 먼저 보인다
이 패치 같은 건 보통 로그로는 안 잡힌다.
왜냐하면 할당 실패는 예외로 올라가고, 누수는 조용히 누적되기 때문이다.
그래서 추천하는 관측 포인트는 단순하다.
- 프로세스 RSS/메모리 사용량 추세
- 요청량 대비 메모리 곡선(steady state가 있는지)
- MemoryError 발생 빈도(완전 0이 아닌지)
“MemoryError가 가끔 뜨는데 서비스는 살아있다”는 상태가 가장 위험하다.
- 살아있다는 이유로 무시하기 쉽고
- 하지만 그때마다 실패 경로가 자원을 더 잡고 있다면
- 곡선은 서서히 위로 기울어진다
이번 패치는 이런 종류의 기울기를 한 군데 줄여준다.
7) 결론: ‘한 줄 패치’는 결국 운영을 편하게 한다
정리하면, gh-147960은 대단한 기능 추가가 아니다.
하지만 “할당 실패가 실제로 발생하는 환경”에서는
- 실패는 어차피 실패지만
- 실패 후 회복이 가능하게 만들고
- 장기 운영에서 메모리 곡선을 덜 위험하게 만든다
는 의미가 있다.
파이썬을 제품/서비스로 운영하는 입장에서는, 이런 안정성 패치를 꾸준히 태우는 게 결국 가장 싸다.
덧붙이면, 이런 류의 버그는 “한 번의 큰 장애”로 나타나기보다, 아래처럼 회색 지대로 오래 남아있다가 문제를 만든다.
- 가끔 MemoryError가 뜨는데 재시도하면 살아난다
- RSS가 조금씩 올라가지만 그래프가 급격하진 않다
- 특정 시간대(트래픽 피크)만 지나면 안정된다
이 상태가 지속되면, 팀은 문제를 “일시적 트래픽”으로 오해하기 쉽다.
그래서 나는 이런 패치를 볼 때마다 체크를 하나 한다.
- 우리 서비스는 ‘희귀 실패’가 나올 수 있는 조건(메모리 제한/긴 런타임/내부 격리)이 있는가?
답이 Yes면, 기능 패치보다 이런 안정성 패치가 더 ‘ROI가 좋다’고 느끼는 편이다.
Keywords
python,cpython,subinterpreters,cross-interpreter data,_tuple_shared,MemoryError,allocation failure,memory leak,PyMem_RawFree,PyMem_Calloc,error path,embedding,extension,stability,testing,observability
이미지 크레딧/라이선스
- Macro laptop coding (Unsplash).jpg — Marc Mueller / CC0
- https://commons.wikimedia.org/wiki/File:Macro_laptop_coding_(Unsplash).jpg
- http://creativecommons.org/publicdomain/zero/1.0/deed.en
댓글
댓글 쓰기