TT Lab
블로그 시작하기
배우기 러닝패스 코스

프로파일링: 느린 요청 하나를 함수까지

밖에서 붙잡는 도구: py-spy, perf, gdb, 코어 덤프, eBPF

TT Lab 에서 이어서 보기

한 줄 요약

이미 떠 있는 프로세스를 고치거나 다시 띄우지 않고 들여다보는 도구들은 모두 커널에 특별한 문을 요구합니다. ptrace(남의 레지스터와 메모리), perf_event_open(표본 타이머), bpf(커널 안의 작은 프로그램)입니다. 어떤 도구가 어떤 문을 여는지 알아야 운영에서 무엇을 허락할지 정할 수 있습니다.

왜 이게 필요했나

앞의 모듈들은 프로그램이 스스로 신호를 내도록 만들어 둔 세계였습니다. 실제 장애는 그런 준비가 없는 프로세스에서 납니다. 서드파티 바이너리일 수도 있고, 프로파일러를 붙이려면 배포를 다시 해야 하는 서비스일 수도 있습니다. 다시 띄우면 증상이 사라지는 교착이나 누수도 흔합니다. 그때는 밖에서 붙잡는 수밖에 없습니다.

어떻게 동작하나

도구 하는 일 커널에 요구하는 것
py-spy dump·record 파이썬 인터프리터의 메모리를 밖에서 읽어 스택을 짓는다 process_vm_readv, 기본 모드에서 ptrace(잠깐 멈춤)
perf record CPU 마다 표본 타이머를 걸어 스택을 찍는다 perf_event_open
gdb -p, gcore 프로세스에 붙어 멈추고 레지스터·메모리를 읽는다 ptrace
코어 덤프 죽는 순간 메모리를 파일로 남긴다 커널 설정 core_pattern, ulimit -c
bpftrace profile 커널 타이머에 작은 프로그램을 걸어 모든 CPU 를 센다 bpf, perf_event_open

리눅스는 이 문들을 여러 겹으로 지킵니다. Yama 의 ptrace_scope=1 은 「자기 자손에게만 붙을 수 있다」는 규칙이고, perf_event_paranoid 는 숫자가 클수록 성능 카운터를 좁게 엽니다. 컨테이너는 그 위에 capability 를 덜어 내고 seccomp 로 시스템 호출을 거릅니다. 이 플랫폼의 실습 파드(capability 0)에서 재 보면 자식이 아닌 프로세스에 ptrace 와 process_vm_readv 는 EPERM, perf_event_open 과 bpf 도 EPERM 입니다. 자기가 띄운 자식에게는 ptrace 가 됩니다. 그래서 py-spy record -- python app.py 처럼 py-spy 가 대상을 직접 띄우는 꼴은 권한 없이도 될 수 있고, 이미 떠 있는 것에 붙는 꼴은 안 됩니다.

도구마다 알아 둘 습관이 있습니다. gdb -p 로 붙어 있는 동안 대상은 멈춥니다. -batch -ex 'thread apply all bt' 로 보고 바로 놓습니다. 코어를 남기려면 우분투의 기본 core_pattern(apport 로 보내는 파이프)을 파일 경로로 바꾸고 크기 제한을 풉니다. gcore 는 대상을 잠깐 멈췄다 놓아 주므로 서비스를 죽이지 않고 사진을 남깁니다. perf 는 -F 99 처럼 100 을 살짝 비낀 주기를 씁니다. 다른 주기적인 일과 박자가 맞아 한쪽만 찍히는 것을 피하려는 관습입니다.

eBPF 프로파일러는 어느 프로세스가 범인인지 모를 때 먼저 씁니다. 프로세스 하나를 고르지 않고 시스템 전체를 한 번에 표본하기 때문입니다. Parca Agent, Pyroscope 의 eBPF 수집, OpenTelemetry eBPF profiler 가 이 방식으로 지속 프로파일링을 합니다.

현장에서 만나는 모습

쿠버네티스에서 이 도구들을 쓰려면 디버그용 임시 컨테이너(kubectl debug)에 SYS_PTRACE 를 주거나, 노드에 권한 있는 에이전트를 둡니다. 둘 다 「남의 메모리를 읽는 권한」을 운영 환경에 들이는 결정이라, 누가 언제 쓰는지 기록과 승인을 함께 설계합니다. 이 플랫폼이 실습 파드에 그 권한을 주지 않고 이 실습을 VM 으로 보낸 이유도 같습니다.

흔히 틀리는 읽기

perf 표에 함수 이름 대신 16진 주소만 보이면 대개 심볼 정보가 없는 바이너리입니다. 이 실습의 C 프로그램은 -g 로 빌드하고 프레임 포인터를 남겨(-fno-omit-frame-pointer) 이름과 호출 그래프가 보이게 했습니다. 운영 바이너리라면 디버그 심볼 패키지를 따로 받아야 합니다.

gcore 로 뜬 코어를 「프로세스가 죽은 기록」으로 읽는 것도 틀립니다. gcore 는 살아 있는 프로세스의 사진이고, 찍은 뒤에도 프로세스는 계속 돕니다. 반대로 진짜로 죽은 프로세스의 코어는 죽는 순간의 레지스터를 담고 있어서, gdb 의 bt 첫 줄이 곧 죽은 자리입니다.

bpftrace 의 프로세스 이름(comm)은 15자에서 잘립니다. 긴 이름의 프로세스는 잘린 이름으로 보이니, 이름만으로 찾지 말고 PID 도 함께 봅니다.

다음 실습에서 할 것

VM 에 미리 떠 있는 세 프로세스(정규식을 매번 컴파일하는 파이썬 일꾼, 해시만 계산하는 C 프로그램, 교착에 빠진 C 프로그램)를 밖에서 붙잡습니다. py-spy 로 파이썬 스택과 표본을, perf 로 네이티브 표본을, gdb 와 코어로 멈춘 스택을, bpftrace 로 시스템 전체를 봅니다.

English corner