py-spy: 코드 수정 없는 샘플링 프로파일러
실행 중인 파이썬 프로세스에 코드 수정 없이 붙어 프로파일링하는 py-spy. top·record·dump 세 모드, 낮은 오버헤드로 운영에서도 쓰는 법, 샘플링 방식의 원리까지 정리합니다.
지난 글에서 tracemalloc으로 메모리 할당을 소스 위치까지 추적했다. 그런데 지금까지의 도구들에는 공통점이 있었다. cProfile이든 tracemalloc이든, 측정하려면 코드 안에 무언가를 심거나 특별한 방식으로 실행해야 했다. 이미 운영 서버에서 돌고 있는, 멈출 수 없는 프로세스가 갑자기 느려졌다면 어떻게 할까? 코드를 고쳐 재배포할 여유가 없을 때, py-spy는 발상을 통째로 뒤집는다. 돌고 있는 그 프로세스에 바깥에서 붙는다.
바깥에서 들여다본다
py-spy는 별도 프로세스로 실행되어, 대상 파이썬 프로세스의 메모리를 읽어 현재 어떤 함수를 실행 중인지 들여다본다. 대상 코드에는 import도, 데코레이터도, 단 한 줄의 수정도 필요 없다.
pip install py-spy
# 실행 중인 프로세스의 PID만 알면 된다
py-spy top --pid 12345
PID만 넘기면 그 프로세스가 멈추지 않고 계속 도는 동안, py-spy가 옆에서 스택을 들여다본다. 운영 중인 웹 서버가 갑자기 CPU를 100% 쓸 때, 서버를 내리거나 코드를 바꾸지 않고 “지금 무슨 함수에 갇혀 있는지”를 즉석에서 확인할 수 있다는 뜻이다.
샘플링이라는 발상
cProfile은 모든 함수 호출을 일일이 계측한다. 정확하지만 오버헤드가 크고, 호출이 많은 코드에서는 측정이 결과를 왜곡한다. py-spy는 다른 길을 택한다. 일정 간격(기본 초당 약 100번)으로 “지금 이 순간 어느 함수에 있나”를 사진 찍듯 표본만 수집한다. 이것이 샘플링 프로파일링이다.
수천 번 찍은 사진에서 특정 함수가 80%의 사진에 등장한다면, 그 함수가 시간의 약 80%를 차지한다고 통계적으로 추론하는 것이다. 모든 호출을 추적하지 않으니 오버헤드가 극히 낮아, 측정 대상의 성능에 거의 영향을 주지 않는다. 운영 환경에서도 안심하고 붙일 수 있는 이유가 바로 이것이다.
세 가지 모드
py-spy는 상황에 맞는 세 가지 모드를 제공한다.
top은 유닉스의 top 명령처럼, 함수별 점유율을 실시간으로 갱신하며 보여 준다. “지금 어디가 뜨거운가”를 즉석에서 보는 용도다. record는 일정 시간 동안 샘플을 모아 flame graph(불꽃 그래프) SVG로 저장한다. 호출 스택의 어느 경로가 시간을 잡아먹는지 시각적으로 한눈에 들어온다.
# 60초 동안 기록해 불꽃 그래프로 저장
py-spy record -o profile.svg --pid 12345 --duration 60
# 지금 이 순간의 스택 트레이스를 한 번 출력
py-spy dump --pid 12345
dump는 그 순간의 스택 트레이스를 한 번만 출력한다. 프로세스가 응답 없이 멈춘 것 같을 때(데드락이나 무한 루프 의심), “지금 정확히 어느 줄에 걸려 있는지”를 즉시 확인하는 데 쓴다. 디버거를 붙일 수 없는 운영 상황에서 특히 요긴하다.
직접 실행하며 프로파일링
이미 도는 프로세스에 붙는 것뿐 아니라, 처음부터 py-spy로 감싸 실행할 수도 있다.
py-spy record -o profile.svg -- python app.py
이렇게 하면 app.py를 실행하면서 동시에 프로파일을 기록한다. -- 뒤에 평소 실행하던 명령을 그대로 적으면 된다.
py-spy는 정밀한 호출 횟수가 필요할 때를 위한 도구가 아니다. 그 자리는 여전히 cProfile의 몫이다. 하지만 “운영 중인 프로세스가 왜 느린지 지금 당장 알아야 할 때”, 코드 수정도 재배포도 없이 PID 하나로 답을 주는 도구는 py-spy가 거의 유일하다. 측정 도구 여정의 마지막으로, 다음 글에서는 다시 일상으로 돌아와 print 디버깅을 견딜 만하게 만들어 주는 작고 즐거운 도구 icecream을 소개한다.
지난 글: tracemalloc: 메모리 할당을 추적하기
다음 글: icecream: print 디버깅을 견딜 만하게
읽어주셔서 감사합니다. 😊