
FastAPI와 Uvicorn으로 구성된 NLP 서비스를 운영하던 중, "과연 내 서비스의 병목은 어디일까?"라는 근본적인 질문에 부딪혔다. 병목이 모델 연산(CPU) 때문일까, 아니면 DB에서 데이터를 읽어오는 I/O 때문일까?
이 질문에 답하기 위해, 리눅스 커널 레벨에서 시스템의 모든 동작을 추적할 수 있는 eBPF(Extended Berkeley Packet Filter) 기반의 프로파일러(ebpf-model-profiler)를 사용해 보기로 결정했다.
https://github.com/cloud-club/08th-ebpf/tree/main/projects/kyj/ebpf-model-profiler
(예지님 감사합니다!! 클클 짱)
하지만 이 과정은 순탄치 않았다.. 운영 중인 서비스는 Nginx 리버스 프록시 뒤에서 20개가 넘는 Uvicorn 워커(프로세스)가 동시에 실행되는 '다중-프로세스' 환경이었고, 사용하려는 프로파일러는 '단일-프로세스'만 추적하도록 설계되어 있었다...
이 글은 Events: 0이라는 절망적인(?) 결과에서 시작해, 프로파일러의 C 코드와 Python 코드를 직접 수정하여 20개가 넘는 자식 프로세스들을 모두 추적하고, 다중 프로세스인 내 서비스 환경에서도 적용된 모습인Total Events: 488이라는 성공적인 결과를 얻기까지의 모든 과정을 담은 튜토리얼이다!
이 실습을 100% 이해하기 위해선, 내가 사용한 도구와 환경의 원리를 깊게 알 필요가 있었다.
read, write 같은 특정 시스템 콜(Syscall)이 호출되는 순간(kprobe)에 eBPF 코드가 실행되어, "A 프로세스가 read를 호출했음" 같은 정보를 수집할 수 있다..c 파일과 .py 파일이 바로 이 BCC 툴킷의 일부였다.pstree는 이 PID와 PPID의 관계를 시각적으로 보여주는 도구이다.read (파일 읽기), write (파일 쓰기), sendto (네트워크 데이터 전송). 우리는 이 "주문서"가 처리되는 데 걸리는 시간을 측정했다.sudo와 가상 환경(venv)의 충돌venv를 활성화하면 ubuntu 유저의 PATH가 바뀐다. 하지만 sudo는 명령어를 root 유저로 실행한다. root 유저는 venv의 존재를 모르기 때문에 command not found 오류가 발생했다.sudo ./venv/bin/python3처럼, sudo 명령어를 사용할 때에는sudo에게 venv 안의 파이썬 실행 파일의 절대 경로를 알려주어 해결했다.C_INCLUDE_PATHsudo 환경에서는 common.h 같은 헤더 파일의 경로를 잃어버렸다. (fatal error: 'common.h' file not found)C_INCLUDE_PATH=$(pwd)/src/ebpf라는 환경 변수를 설정하여, C 컴파일러에게 "헤더 파일은 이 폴더 안에 있다"라고 명시적으로 알려주었다.301 Moved Permanently 오류에서 봤듯이, http:// (80) 요청을 https:// (443)로 강제 리디렉션하는 역할도 한다.pstree를 보니, python(매니저) -> uvicorn(수석 일꾼) -> {uvicorn}(진짜 일꾼 20+명) 구조였다.Events: 0이 떴던 것이다.※ 해당 서버는 aws 비용 이슈로 곧 내릴 예정이라 ㅎㅎ.. 귀찮아서 ip를 가리진 않았다..
최초 설치 과정에서 수많은 오류를 만났다...
ERROR: No matching distribution found for bccbcc는 pip가 아닌 apt로 설치해야 하는 시스템 라이브러리이다.sudo apt-get install bpfcc-tools ...로 설치 후, requirements.txt에서 bcc 라인을 주석 처리했다.sudo: ebpf-profiler: command not foundsudo가 venv 경로를 인식하지 못했다.sudo -E가 실패하여, sudo ./venv/bin/python3 -m src.cli처럼 venv 안의 파이썬을 절대 경로로 직접 실행했다.fatal error: 'common.h' file not foundsudo 환경에서 C 컴파일러가 헤더 파일 경로를 잃었다.sudo C_INCLUDE_PATH=$(pwd)/src/ebpf ... 환경 변수를 추가했다.error: cannot call non-static helper functionsrc/ebpf/syscall_tracer.c 파일의 trace_syscall_enter 함수 정의부 앞에 static inline을 추가했다.AttributeError: 'BPF' object has no attribute 'ctype'bcc API를 사용했다.src/collector/tracer.py 파일의 .ctype을 Python 내장 ctypes 모듈을 사용하도록 수정했다. (import ctypes as ct 추가, ct.c_uint(...)로 변경)

이것도 하고 스웨거로도 울 서버에 여러 api 요청들을 보냈다.
모든 오류를 고쳤지만, 결과는 Total Events: 0이었다.
root가 내가 띄운 서버의 PID를 나타내는 것이고, 아래ubuntu는 내가grep명령어를 친 순간 실행된 프로세스의 PID를 나타낸다.root의 PID는 불변하지만ubuntu의 PID는 계속 변하고 있는 것을 볼 수 있었다.
하지만 이 root PID조차 "진짜"가 아니었다.
아아.. 알고 보니python(362284)는 "매니저"였고,uvicorn(362406)이 "수석 일꾼", 그 밑의{uvicorn}(362408...)20여 개가 "진짜 일꾼"이었다. Nginx가 요청을 20명에게 분산시키니, "일꾼 1"만 감시해서는 아무것도 잡히지 않았던 것이다..!!!!!!!!
이 문제를 해결하기 위해, "일꾼"(PID)뿐만 아니라 그 "일꾼의 부모"(PPID)도 추적하도록 프로파일러의 C 코드를 수정했다.
nano src/ebpf/syscall_tracer.c를 열어 trace_syscall_enter 함수와 9개의 모든 _exit 함수의 should_trace(pid) 검사 로직을 다음과 같이 변경했다.
// (수정 전)
// if (!should_trace(pid)) {
// return 0;
// }
// (수정 후)
// "현재 PID"가 추적 대상이 아니면, "부모 PID"도 검사!
if (!should_trace(pid)) {
struct task_struct *task = (struct task_struct *)bpf_get_current_task();
u32 ppid = 0;
bpf_probe_read_kernel(&ppid, sizeof(ppid), &task->real_parent->tgid);
if (!should_trace(ppid)) {
return 0; // 둘 다 아니면 무시
}
}
[터미널 A] 프로파일러 시작: C 코드와 Python 코드가 모두 수정된 상태에서, "수석 일꾼"의 PID(362406)를 추적하도록 start 명령어를 실행하고 켜 둔다.
(venv) $ sudo C_INCLUDE_PATH=$(pwd)/src/ebpf ./venv/bin/python3 -m src.cli start --pid 362406
...
INFO - Tracing started. Press Ctrl+C to stop.
[터미널 B] 부하 테스트: 스웨거나 curl을 이용해 HTTPS API를 10~20회 호출한다.
$ curl -k "https://<EC2_IP>/api/articles/search?query=test"
[터미널 A] 결과 확인: 부하 테스트 후, 터미널 A로 돌아와 Ctrl+C를 누른다.
참고:
export명령어는 작동하지 않았다..
export명령어는start가 실행 중일 때(터미널 A) 다른 터미널(터미널 C)에서 실시간 데이터를 가져오는 명령어이다. 하지만 복잡한sudo와venv환경 때문에 프로세스 간 통신(IPC)에 실패하여Note: This command requires...(데이터 없음) 오류만 발생한 것 같다.하지만
start명령어 자체는Ctrl+C로 종료될 때, 수집한 통계를 스스로 요약 출력하는 기능이 있었다.
최종 결과:
Ctrl+C를 누르자, 통계가 터미널 A에 출력되었다.

Summary:
- Total Events: 488
- Unique Syscalls: 2 (아래를 보면 read, write만 잡혔다)
- Average Latency: 69us (평균 지연 시간)
Top 10 syscalls by total time:
read: 28.85ms (276 calls)write: 5.01ms (212 calls)
분석:
이 요약은 스웨거로 API를 호출하는 동안, 20명이 넘는 "진짜 일꾼"(uvicorn)들이 총 488번의 read/write 시스템 콜을 발생시켰으며, 가장 시간이 많이 걸린 작업은 read(디스크/네트워크 읽기)였음을 명확하게 보여준다.
서비스 느림 의심되는 원인에 대한 모니터링 진행
로그인 1회
서비스 이용 중 병목이 직접 체감됐던 의미기반 검색 api 5회 호출