프로세스와 자원 상태
100분 안팎
학습 목표
프로세스·스레드·파일 핸들·메모리와 지속 데이터를 구분합니다.
개념
응답 뒤에서 누가 일하는지 봅니다
HTTP 응답이 늦어졌을 때 로그에 오류가 없다는 이유만으로 자원 상태를 건너뛰면 원인을 놓칠 수 있습니다. 프로세스는 실행 중인 프로그램의 단위이며 PID는 관측할 대상을 고르는 번호입니다. 서비스 이름이 같아도 다른 PID일 수 있고 프로그램이 종료되면 PID는 나중에 다른 프로그램에 재사용될 수 있습니다. 그래서 이름 검색 결과 하나를 바로 믿지 않고 실행 명령, 관측 시각, 포트를 함께 확인합니다. 이 레슨은 개인 실습 프로세스만 관찰하고 다른 프로세스에 신호를 보내지 않습니다.
프로세스와 스레드를 구분합니다
하나의 Java 프로세스에는 HTTP 요청 처리, 가비지 수집, 애플리케이션 작업 등 여러 스레드가 있을 수 있습니다. 스레드는 같은 프로세스의 메모리와 일부 자원을 공유합니다. 스레드가 많다는 사실만으로 장애라고 판단하지 않습니다. 평소 수와 요청량, 대기 상태의 변화를 함께 봅니다. 반대로 PID가 하나라고 해서 요청이 한 번에 하나씩 처리된다고 생각하지 않습니다. 이번 관측 프로그램은 짧게 살아 있는 Python 프로세스와 작업 스레드를 사용하여 그 차이를 눈으로 확인합니다.
실제 자원과 교육용 관측 범위를 나눕니다
observe.py는 자기 PID, 활성 Python 스레드 수, 최고 상주 메모리, 실제로 열어 둔 실습 파일 두 개, 데이터 보존 여부를 JSON으로 출력합니다. 이는 Java JVM의 스레드 수나 메모리 크기를 대신 측정한 값이 아닙니다. 자원별로 무엇을 읽고 있는지 익히기 위한 짧은 프로브입니다. 첫 레슨의 Java 서비스를 실행한 개인 Linux 환경에서는 ps -p PID -o pid,ppid,rss,etime,args와 ls /proc/PID/fd로 같은 종류의 근거를 추가 수집합니다. PID는 실행 시점의 실제 값을 입력합니다.
메모리 한 숫자의 한계를 읽습니다
RSS는 현재 물리 메모리에 상주한 크기를 뜻하는 관측값입니다. Java 힙 제한인 -Xmx와 같지 않습니다. JVM은 힙 외에도 스레드 스택, 클래스 메타데이터, 코드 캐시와 다른 메모리를 사용합니다. observe.py의 peak_rss_kib는 실행 이후 최고 상주 메모리를 KiB로 맞춘 값이므로 현재 RSS와도 다릅니다. Linux의 ru_maxrss는 KiB, macOS 값은 바이트이므로 코드에서 단위를 맞춥니다. 단위와 수집 시점을 빠뜨리면 서로 다른 값을 같은 지표처럼 비교하게 됩니다.
추세와 한도를 함께 봅니다
메모리 100MiB라는 한 번의 관측만으로 누수나 부족을 결론 내리지 않습니다. 같은 부하에서 사용량이 계속 증가하는지, 요청 완료 후 내려오는지, 프로세스나 컨테이너의 제한에 얼마나 가까운지 확인합니다. 이 실습의 memory 분류는 항상 trend-needed입니다. 단일 스냅샷은 추가 관측이 필요하다는 뜻입니다. 높은 최고 사용량은 이미 지나간 순간을 나타낼 수도 있습니다. 지속적으로 높은 현재 RSS와는 다른 가설을 세워야 하며, 뒤 관측 모듈에서 시간에 따른 지표를 이어 붙입니다.
파일 핸들과 파일 내용을 구분합니다
파일을 열면 운영체제가 프로세스에 접근 손잡이를 제공합니다. 파일 디스크립터는 그 손잡이를 가리키는 번호이며 로그 파일, 일반 파일, 소켓 등이 자원을 사용합니다. observe.py의 open_lab_files는 직접 연 데이터와 로그 두 개만 셉니다. 전체 OS 핸들 수를 대표하는 값이 아닙니다. Linux에서는 /proc/PID/fd, macOS에서는 lsof -p PID로 범위를 넓혀 확인할 수 있습니다. 권한 때문에 목록이 안 보이면 핸들이 없다고 결론 내리지 않고 관측 계정과 접근 제한을 기록합니다.
Too many open files를 읽습니다
Too many open files는 해당 작업이 열 수 있는 파일 디스크립터 한도에 닿았을 가능성을 알려 줍니다. 파일 경로가 없다는 No such file or directory와는 다른 오류입니다. 개인 프로세스의 핸들 수 추세, 닫지 않은 소켓이나 파일, 적용 한도를 먼저 확인합니다. 한도를 올리는 것만으로 누수가 해결되지는 않습니다. 이번 코드는 with 블록을 끝내 두 파일을 닫습니다. 작업 스레드는 이벤트를 받아 끝나고 join으로 종료를 기다립니다. 관측 도구 자체가 자원을 남기지 않는 구조도 운영 작업의 일부입니다.
메모리와 지속 데이터의 수명을 봅니다
메모리에 있는 공지 목록은 프로세스가 끝나면 사라집니다. 반면 파일시스템의 notice.txt는 프로세스가 종료되어도 남습니다. 파일이 디스크에 있다는 이유만으로 다른 장치의 고장에서도 안전한 것은 아닙니다. 지금의 preserved는 같은 개인 폴더에서 두 번 실행한 사이 내용이 유지되었다는 제한된 계약입니다. 백업과 복원은 뒤 모듈에서 별도로 확인합니다. observe.py는 파일이 이미 있으면 덮어쓰지 않고 처음 실행에만 기본 내용을 만듭니다. 앞 모듈의 기존 데이터 보존 규칙과 연결되는 부분입니다.
상태와 증거를 분리합니다
summarize.py는 수집된 스냅샷을 짧은 분류로 바꿉니다. pid가 양수면 alive, 멈춤 fixture의 0이면 stopped입니다. threads가 둘 이상이면 multiple, 하나면 single입니다. open_lab_files가 양수면 open, 0이면 none입니다. data_preserved가 참이면 preserved, 거짓이면 lost입니다. 일반 사용자 프로세스의 PID 관측과 fixture의 0을 혼동하여 stopped 경계를 삭제하지 않습니다. 여기서 0은 종료된 프로세스를 나타내도록 제공한 교육용 fixture이며 살아 있는 자기 PID를 수집하는 규칙과 구분합니다.
짧은 프로브를 실행합니다
개인 폴더에서 python3 -B observe.py를 실행합니다. -B는 바이트코드 캐시 파일을 만들지 않도록 합니다. workspace/notice.txt와 workspace/requests.log가 생기며 JSON에 snapshot과 summary가 들어 있습니다. PID와 메모리 값은 환경과 시점에 따라 달라지므로 예제 숫자와 일치시키지 않습니다. summary는 같은 조건이면 동일해야 합니다. 시작 코드에서는 데이터가 살아 있는데 lost로 분류하므로 check.sh가 실패합니다. 실패하는 필드를 먼저 읽고 summarize의 규칙을 고칩니다.
재실행 검사를 읽습니다
bash check.sh는 임시 notice board 폴더에 기존 notice.txt를 만들고 관측 프로세스를 두 번 실행합니다. 두 실행이 종료된 뒤 파일 내용이 그대로인지, 로그가 두 줄 누적되었는지 확인합니다. 관측 코드의 PID·스레드·실제 핸들 값을 먼저 확인하고 분류 값을 비교합니다. stopped, single, none, lost fixture도 따로 검사하므로 정상 결과만 하드코딩하면 통과하지 않습니다. 프로세스가 끝나야 완료되는 검사이며 긴 서비스가 남지 않습니다. 검사기는 임시 폴더를 정리하지만 학습자의 실제 데이터는 삭제하지 않습니다.
Java 관측으로 이어 갑니다
첫 레슨의 JAR를 15초 동안 실행하고 그 시간 안에 개인 환경에서 ps와 요청 로그를 관찰합니다. Linux에서 ps의 RSS 열은 보통 KiB이고 ELAPSED는 살아 있던 시간입니다. macOS와 Linux 옵션이 완전히 같지는 않으므로 제공 README의 명령을 따라가되 안 되는 옵션은 오류를 기록합니다. 관측한 PID가 이미 종료되어 결과가 없으면 자원 사용량 0으로 적지 않습니다. 종료되었다고 기록하고 새 실행의 PID와 시각을 다시 얻습니다. 사용 중인 파일과 쌓인 로그는 프로세스 관측과 별도의 자료로 확인합니다.
인계 기록을 작성합니다
기록에는 수집 도구, 플랫폼, 시각, PID, 메모리 값의 단위와 현재/최고 구분을 넣습니다. 열린 실습 파일 수는 전체 핸들 수가 아니라는 범위도 적습니다. 요청 지연이 있었다면 요청 ID와 로그 시각을 연결하여 자원 관측을 같은 구간과 비교합니다. 단일 스냅샷에서 확정할 수 없는 원인은 추가 관측 항목으로 남깁니다. 프로세스·스레드의 이론적 차이는 서재에서 더 읽고, 여기서는 무엇을 직접 관찰했는지와 무엇이 종료 뒤 남았는지를 설명할 수 있으면 레슨 목표를 달성한 것입니다.
따라하기
메모리와 파일의 수명 비교
프로세스 재실행 뒤 메모리는 새로 구성되고 파일은 별도로 남는다는 개념 모형입니다. 실제 파일 보존은 아래 실습에서 확인합니다.
first = {'memory': ['안내'], 'disk': ['보관']}
second = {'memory': [], 'disk': first['disk']}
print('memory', len(second['memory']))
print('disk', len(second['disk']))실행 결과
memory 0 disk 1
실제 짧은 프로세스 관찰
solution zip 루트에서 실행합니다. snapshot에서 실제 PID, 2개 이상 스레드, 양수 peak_rss_kib, open_lab_files=2를 확인합니다. 이 프로세스는 자기 스레드를 정상 종료합니다. PID와 메모리가 환경마다 달라 출력 숫자를 고정하지 않습니다.
python3 -B observe.py재실행과 경계 검사
summarize.py를 고친 뒤 실행합니다. 아래는 solution에서 직접 실행한 결과입니다. stopped 등의 마지막 네 항목은 상태 경계 fixture이며 실제 프로세스를 종료하는 명령이 아닙니다.
bash check.sh실행 결과
PASS live process observed PASS worker thread observed PASS memory needs time series PASS two owned file handles PASS persistent file preserved PASS live process observed PASS worker thread observed PASS memory needs time series PASS two owned file handles PASS persistent file preserved PASS append log across launches PASS data survives process exit PASS stopped fixture PASS single thread fixture PASS no owned handles fixture PASS lost data fixture RESULT: 0 failure(s)
살아 있는 JVM 관찰
첫 레슨 Java solution 루트에서 package를 완료한 후 개인 포트 허용 환경에서 실행합니다. $!는 방금 시작한 자식 프로세스 PID입니다. 끝의 wait는 15초 정상 종료를 기다립니다. 다른 터미널에서 Linux는 ls /proc/PID/fd, macOS는 lsof -p PID로 확인하며 PID는 ps 결과의 값을 씁니다. 시작 직후 RSS와 요청 뒤의 RSS를 비교합니다. 샌드박스는 서버 실행이 차단되어 출력은 비웁니다.
java -jar target/notice-board-1.0.0.jar --lab.duration-ms=15000 &
service_pid=$!
ps -p "$service_pid" -o pid,ppid,rss,etime,args
wait "$service_pid"확인 문제
실습
summarize.py를 구현합니다. 관측값을 process/concurrency/files/data/memory로 분류하는 규칙은 README에 있습니다. 실제 observe.py의 PID·스레드·최고 RSS·실습 핸들을 확인하고 두 프로세스 종료 후 데이터 유지와 로그 누적도 검사합니다. 관측기와 check.py는 수정하지 않습니다. 단일 메모리 관측을 누수라고 단정하지 않습니다.
실행 명령
bash check.sh
기대 결과
관측·재실행·경계 검사 모두 PASS, RESULT: 0 failure(s), 종료 0
모범 답안
모범 답안 내려받기더 읽기
면접 질문
- 프로세스와 스레드의 차이, Java 힙과 RSS의 차이를 설명해 주세요.
- 메모리 누수를 의심할 때 한 번의 RSS 관측 외에 어떤 근거를 모으겠습니까?