[Linux] 프로세스 관리 (Process Management)

2026. 5. 22. 22:20·Linux
SMALL
시나리오

새벽 2시, 서버 CPU가 98%를 찍고 알람이 울렸습니다. SSH로 접속은 됐는데 응답이 느립니다. 어떤 프로세스가 원인인지, 어떻게 종료하는지, 종료했는데 왜 다시 살아나는지 — 모르면 30분이 지나도 원인을 못 찾습니다.

이 모듈을 마치면 ps, top, kill, strace로 프로덕션 서버의 프로세스를 추적하고, 안전하게 종료하고, 좀비·고아 프로세스를 정리하는 전체 흐름을 혼자 할 수 있습니다.

프로세스 관리 (Process Management)

이번 챕터에서 배울 것
1ps와 top으로 프로세스 상태(R/S/D/Z)를 확인하고 CPU/메모리 사용률을 실시간 모니터링할 수 있다
2fg, bg, jobs, &로 포그라운드·백그라운드 전환을 제어할 수 있다
3kill로 시그널(SIGTERM/SIGKILL/SIGHUP)을 전송해 프로세스를 안전하게 종료·재로드할 수 있다
4nice와 renice로 CPU 우선순위를 조정해 프로세스 간 자원을 분배할 수 있다
5좀비·고아 프로세스 발생 원인을 이해하고 pstree로 부모-자식 관계를 추적할 수 있다
실습 환경 준비
현재 실행 중인 프로세스 수 확인
ps aux | wc -l
프로세스 트리 확인 (pstree 설치 필요)
pstree -p | head -20
좀비 프로세스 확인
ps aux | awk '$8 ~ /Z/' 
CPU 사용률 상위 프로세스 확인
ps aux --sort=-%cpu | head -10

모든 프로세스는 부모-자식 관계로 연결된 트리 구조를 형성합니다. PID 1(systemd 또는 init)이 최상위 조상이며, 모든 프로세스는 이 트리에서 어딘가에 위치합니다.

PID 1 (systemd/init)
├── PID 234 (sshd)
│   └── PID 891 (sshd: user session)
│       └── PID 892 (bash)
│           └── PID 1042 (vim)
└── PID 456 (nginx)
    ├── PID 457 (nginx worker)
    └── PID 458 (nginx worker)
프로세스 상태 머신과 시그널 — STAT 코드 읽기, SIGTERM vs SIGKILL

프로세스 상태 코드 — ps aux의 STAT 컬럼에 나타남:

상태코드의미주의 포인트RunningRCPU에서 실행 중 또는 실행 대기여러 개면 정상, 한 개가 100% 점유하면 문제SleepingSI/O 대기 (인터럽트 가능)대부분의 프로세스가 이 상태Disk SleepDI/O 대기 (인터럽트 불가)NFS 행, 디스크 불량 의심StoppedT정지됨 (Ctrl+Z 또는 SIGSTOP)개발 중 일시정지ZombieZ종료됐지만 부모가 회수 안 함수백 개 쌓이면 PID 테이블 고갈 위험

STAT 코드 뒤에 붙는 수식어도 중요합니다: s는 세션 리더, +는 포어그라운드 프로세스, l은 멀티스레드, <는 높은 우선순위, N은 낮은 우선순위(nice 적용)를 의미합니다.

시그널은 프로세스 간 또는 커널이 프로세스에게 보내는 비동기 알림입니다. kill 명령의 이름은 "종료"지만 실제로는 "시그널 전송"이 더 정확한 표현입니다.

실무에서 자주 쓰는 시그널:

시그널번호기본 동작언제 쓰는가SIGTERM15프로세스에게 정상 종료 요청항상 먼저 시도. 프로세스가 cleanup 로직을 실행할 기회를 줌SIGKILL9커널이 즉시 강제 종료SIGTERM 이후 5~10초 기다려도 안 죽을 때만 사용SIGHUP1원래 의미: 터미널 끊김. 현재 관례: 설정 재로드nginx, sshd 등 데몬 설정을 재시작 없이 다시 읽힐 때SIGINT2키보드 인터럽트 (Ctrl+C와 동일)터미널 프로그램을 부드럽게 중단할 때SIGSTOP19프로세스 일시 정지 (무시 불가)디버깅 중 프로세스 상태 고정SIGCONT18SIGSTOP 이후 재개SIGSTOP으로 멈춘 프로세스 재개SIGCHLD17자식 프로세스 상태 변경 알림Zombie 문제 해결 시 부모에게 전송SIGUSR1/210/12사용자 정의애플리케이션마다 의미가 다름 (로그 로테이션 등)

핵심 규칙: SIGKILL은 커널이 직접 프로세스를 제거하므로 프로세스가 가로챌 수 없습니다. 반면 SIGTERM은 프로세스가 핸들러를 등록해서 무시하거나 지연 처리할 수 있습니다. 프로덕션에서 kill -9를 남발하면 데이터 유실, 락 파일 미정리, 커넥션 비정상 종료 같은 부작용이 생깁니다.

SIGHUP 실무 예시:

bash# nginx 설정 변경 후 재시작 없이 적용
nginx -t && kill -HUP $(cat /var/run/nginx.pid)

# sshd 설정 재로드
kill -HUP $(pgrep sshd)

기본 실습

실습 전 디렉토리와 예제 파일을 먼저 준비합니다.

bash# 실습 디렉토리 준비
mkdir -p /tmp/linux/part2/exam_2 && cd /tmp/linux/part2/exam_2

# 백그라운드 실습용 장시간 실행 스크립트 생성
cat > /tmp/linux/part2/exam_2/long_task.sh << 'EOF'
#!/bin/bash
# 백그라운드 실습용 장시간 실행 스크립트
count=0
while true; do
  count=$((count + 1))
  echo "$(date): iteration $count" >> /tmp/linux/part2/exam_2/task.log
  sleep 2
done
EOF
chmod +x /tmp/linux/part2/exam_2/long_task.sh

이제 실습을 진행합니다.

ps aux는 현재 시스템의 모든 프로세스 스냅샷을 출력합니다. top과 달리 실시간 갱신이 없어 스크립트에 쓰기 좋습니다.

bash# 모든 프로세스 출력
ps aux

# CPU 사용량 높은 순서로 정렬
ps aux --sort=-%cpu | head -10

# 메모리 사용량 높은 순서로 정렬
ps aux --sort=-%mem | head -10

# 특정 프로세스 이름으로 검색
ps aux | grep nginx

# 프로세스 트리 형태로 보기 (--forest)
ps auxf

출력 컬럼 의미: USER 실행 사용자, PID 프로세스 ID, %CPU CPU 점유율, %MEM 메모리 점유율, VSZ 가상 메모리(KB), RSS 실제 물리 메모리(KB), STAT 상태, START 시작 시간, COMMAND 실행 명령어.

top은 CPU/메모리 사용량을 실시간으로 보여줍니다. 프로덕션 서버 알람을 받고 ssh 접속 직후 가장 먼저 실행하는 명령입니다.

bash# 기본 실행
top

# 특정 사용자의 프로세스만
top -u www-data

# 업데이트 간격 1초로 설정하고 10회 후 종료 (스크립트용)
top -bn10 -d1 | grep "Cpu\|Mem"

top 내 단축키 (실행 중 누르기):

  • P: CPU 사용량 기준 정렬
  • M: 메모리 사용량 기준 정렬
  • k: 프로세스 종료 (PID 입력 후 시그널 번호 입력)
  • r: nice 값 변경 (renice)
  • 1: CPU 코어별 사용량 토글
  • H: 스레드 레벨로 표시 토글
  • q: 종료

htop이 설치된 서버라면 더 직관적인 UI를 제공합니다. 화살표 키로 프로세스 선택, F9로 시그널 전송, F6로 정렬 기준 변경이 가능합니다.

pstree는 프로세스 계층 구조를 트리 형태로 보여줍니다. 어떤 프로세스가 어디서 spawned됐는지 파악할 때 ps auxf보다 읽기 좋습니다.

bash# PID 포함 전체 트리
pstree -p

# 특정 프로세스 기준으로 트리 보기
pstree -p $(pgrep nginx | head -1)

# 특정 사용자의 프로세스 트리
pstree -pu www-data

# 스레드도 포함해서 보기
pstree -pt

실무 활용: 장애 대응 시 "이 프로세스가 왜 이렇게 많이 떠 있지?"라는 질문에 답하려면 부모 프로세스를 추적해야 합니다. pstree가 그 시작점입니다. 예를 들어 PHP-FPM 워커가 수백 개 뜬 상황에서 부모 프로세스의 설정을 확인하는 식으로 사용합니다.

bash# 정상 종료 요청 (항상 먼저 시도)
kill 1234
kill -15 1234
kill -SIGTERM 1234    # 셋 다 동일

# 5초 기다린 후 살아있으면 강제 종료
kill -SIGTERM 1234 && sleep 5 && kill -SIGKILL 1234 2>/dev/null

# 이름으로 모든 같은 이름 프로세스 종료
killall nginx

# 패턴으로 종료 (프로세스 이름 전체가 아닌 일부 매칭)
pkill -f "python manage.py"

# SIGHUP으로 설정 재로드 (재시작 없이)
kill -HUP $(pgrep -o nginx)

# 시그널 전송 전 대상 프로세스 확인 (dry-run 역할)
pgrep -a -f "gunicorn"

안전한 종료 스크립트 패턴:

bashPID=$(pgrep -f "myapp")
if [ -n "$PID" ]; then
    kill -SIGTERM $PID
    # 최대 10초 대기
    for i in $(seq 1 10); do
        kill -0 $PID 2>/dev/null || break
        sleep 1
    done
    # 아직 살아있으면 강제 종료
    kill -0 $PID 2>/dev/null && kill -SIGKILL $PID
fi

SSH 세션이 끊기면 해당 세션의 모든 자식 프로세스에게 SIGHUP이 전달됩니다. 이를 막는 방법이 두 가지 있습니다.

bash# nohup: SIGHUP을 무시하고 실행, 출력은 nohup.out으로
nohup python3 long_job.py > /var/log/job.log 2>&1 &

# 실행 중인 프로세스를 shell에서 분리 (이미 실행한 경우)
./long_job.sh &
disown %1         # 마지막 백그라운드 작업 분리
disown -a         # 모든 백그라운드 작업 분리

# disown 후 확인 (jobs 목록에서 사라져야 함)
jobs

# 더 나은 방법: tmux나 screen 사용
tmux new-session -d -s myjob 'python3 long_job.py'
tmux attach -t myjob

nohup vs disown 차이: nohup은 처음부터 SIGHUP을 무시한 상태로 프로세스를 시작합니다. disown은 이미 실행 중인 프로세스를 shell의 job 테이블에서 제거해서 shell이 종료될 때 시그널을 안 보내게 만듭니다. 장기 배치 작업이라면 tmux/screen이 훨씬 관리하기 편합니다.

Linux 스케줄러는 nice 값(-20에서 19)으로 CPU 우선순위를 결정합니다. 낮을수록 우선순위가 높고, 높을수록 낮습니다(역직관적이지만 "남에게 양보하는 정도"라고 기억하세요).

bash# nice 값 10으로 백업 실행 (낮은 우선순위 = 다른 작업 방해 최소화)
nice -n 10 tar -czf backup.tar.gz /var/data

# 실행 중인 프로세스의 nice 값 변경
renice -n 15 -p 1234

# 특정 사용자의 모든 프로세스 우선순위 낮추기
renice -n 10 -u batchuser

# 현재 nice 값 확인
ps -o pid,ni,comm -p 1234

# nice 값 -5 (높은 우선순위, root만 가능)
sudo nice -n -5 ./critical_task.sh

실무 시나리오: 주간에 대용량 파일 압축이나 DB 덤프를 해야 할 때 nice -n 19로 실행하면 서비스에 영향을 주지 않고 백그라운드에서 돌릴 수 있습니다. 반대로 특정 프로세스에 더 많은 CPU를 주고 싶으면 (root 권한으로) nice 값을 음수로 줄입니다.

strace는 프로세스가 커널에 요청하는 시스템 콜을 실시간으로 보여줍니다. "이 프로세스가 왜 CPU를 많이 쓰는 거야?", "어느 파일을 열고 있어?", "왜 hang이 걸렸어?"라는 질문에 직접적인 답을 줍니다.

bash# 새 프로세스를 strace로 실행
strace ls /tmp

# 실행 중인 프로세스 attach
strace -p 1234

# 시스템 콜 통계 (어떤 콜이 많이 호출됐는지)
strace -c ls /tmp

# 특정 시스템 콜만 필터링 (파일 관련만)
strace -e trace=open,openat,read,write,close ls /tmp

# 타임스탬프 포함
strace -t -p 1234

# 자식 프로세스도 추적
strace -f -p 1234

# 출력을 파일로 저장 (긴 세션용)
strace -o /tmp/strace_output.txt -p 1234

strace 읽는 법:

openat(AT_FDCWD, "/etc/passwd", O_RDONLY) = 3
read(3, "root:x:0:0:root:/root:/bin/bash\n", 4096) = 32
close(3) = 0

시스템콜(인자) = 반환값 형식입니다. 반환값이 -1이면 에러이고 바로 뒤에 errno가 나옵니다.

주의: strace는 프로세스 성능을 크게 저하시킵니다. 프로덕션에서는 문제 재현 중에만 짧게 쓰고 바로 detach하세요.

lsof(List Open Files)는 프로세스가 열어둔 파일, 소켓, 파이프를 모두 보여줍니다. Linux에서 "모든 것이 파일"이라는 철학 덕분에 네트워크 포트도 lsof로 조회됩니다.

bash# 특정 포트를 점유한 프로세스 찾기
lsof -i :8080
lsof -i :80 -i :443

# 특정 PID의 열린 파일 전체
lsof -p 1234

# 특정 파일을 열고 있는 프로세스 찾기
lsof /var/log/app.log

# 삭제됐지만 아직 열린 파일 찾기 (디스크 공간 안 돌아오는 원인)
lsof | grep deleted

# 특정 사용자의 열린 파일
lsof -u www-data

# TCP/UDP 연결 상태
lsof -i TCP -n -P
lsof -i UDP -n -P

# 열린 파일 개수 (파일 디스크립터 고갈 진단)
lsof -p 1234 | wc -l

lsof 출력 컬럼: COMMAND 프로세스 이름, PID, USER, FD 파일 디스크립터 번호/타입, TYPE 파일 유형(REG/DIR/IPv4/IPv6/FIFO), NODE inode 번호, NAME 파일 경로 또는 소켓 정보.

/proc는 커널이 메모리에 제공하는 가상 파일시스템입니다. 각 PID별 디렉토리에서 프로세스의 모든 정보를 읽을 수 있습니다.

bash# 프로세스 상태 요약 (메모리, 스레드 수, 시그널 마스크 등)
cat /proc/1234/status

# 현재 열린 파일 디스크립터 목록 (lsof보다 빠름)
ls -la /proc/1234/fd

# 파일 디스크립터 개수만 빠르게
ls /proc/1234/fd | wc -l

# 프로세스 환경변수 확인 (실행 시점의 env)
cat /proc/1234/environ | tr '\0' '\n'

# 실행 파일 경로 (심볼릭 링크)
readlink /proc/1234/exe

# 현재 작업 디렉토리
readlink /proc/1234/cwd

# 메모리 맵 (어떤 라이브러리를 로드했는지)
cat /proc/1234/maps

# CPU/메모리 통계 (원시 데이터)
cat /proc/1234/stat
cat /proc/1234/statm

# 시스템 전체 통계
cat /proc/meminfo
cat /proc/cpuinfo
cat /proc/loadavg

실무 활용: 프로세스가 예상치 못한 설정으로 실행됐는지 의심될 때 /proc/PID/environ으로 환경변수를 직접 확인합니다. 또 파일 디스크립터 한도(ulimit -n)에 걸릴 것 같으면 /proc/PID/fd의 파일 수를 모니터링합니다.

CPU 친화성(Affinity)은 특정 프로세스가 특정 CPU 코어에서만 실행되도록 제한하는 기능입니다. NUMA 아키텍처에서 성능을 최적화하거나 실시간 작업을 격리할 때 씁니다.

bash# 0번, 1번 코어만 사용하도록 실행
taskset -c 0,1 ./myapp

# 실행 중인 프로세스의 CPU 친화성 변경
taskset -cp 0,1 1234

# 현재 프로세스의 CPU 친화성 확인
taskset -cp 1234

# 16코어 서버에서 0~7번 코어만 사용
taskset -c 0-7 ./myapp

# 코어 번호 대신 비트마스크로 지정 (0x3 = 코어 0,1)
taskset 0x3 ./myapp

언제 쓰나: 고빈도 거래 시스템, 실시간 미디어 처리, CPU 집약적 배치 작업을 특정 코어에 격리해서 다른 서비스의 캐시를 방해하지 않게 할 때 씁니다. 일반 웹 서비스에서는 불필요하지만 SRE 면접에서 자주 나오는 개념입니다.

트러블슈팅

원인: 자식 프로세스가 종료됐지만 부모 프로세스가 wait() 시스템 콜을 호출하지 않아 프로세스 테이블 엔트리가 남아 있는 상태. 자식이 종료 코드를 전달하려고 기다리는 중입니다.

특징:

  • ps aux에서 Z 상태, COMMAND에 표시
  • 실제 메모리/CPU는 거의 사용하지 않음
  • PID 테이블 슬롯을 점유해서 PID가 고갈될 수 있음 (기본 PID 최대값 32768)

진단 및 해결:

bash# Zombie 프로세스 찾기
ps aux | awk '$8 ~ /Z/'

# Zombie의 부모 PID 확인
ps -el | grep Z
ps -o ppid= -p 

# 부모에게 SIGCHLD 보내서 wait() 유도
kill -SIGCHLD 

# 그래도 안 되면 부모 프로세스 종료 (zombie도 자동 정리됨)
kill -SIGTERM 
# 부모가 죽으면 zombie는 PID 1(systemd)에 입양되고 즉시 회수됨

# strace로 부모가 SIGCHLD 핸들러 있는지 확인
strace -e signal -p 

장기 해결: 애플리케이션 코드에서 SIGCHLD 시그널 핸들러로 waitpid(-1, WNOHANG)를 비동기 호출하거나, 멀티스레드 서버라면 pthread_join()을 제대로 호출해야 합니다.

원인: NFS/CIFS 마운트 문제, 디스크 I/O 행, 드라이버 버그 등으로 커널 I/O를 무한 대기 중. 커널 레벨에서 잠자고 있어서 어떤 시그널도 처리되지 않습니다.

왜 kill -9가 안 되는가: SIGKILL은 커널이 다음번에 해당 프로세스를 CPU에 스케줄링할 때 처리합니다. 그런데 D 상태 프로세스는 커널 I/O 경로 안에 갇혀 스케줄링 자체가 안 되므로 시그널 처리 기회가 없습니다.

진단:

bash# D 상태 프로세스 찾기
ps aux | awk '$8 == "D"'

# 어떤 파일/소켓을 기다리는지
lsof -p 

# 커널 스택 덤프 (무엇을 기다리는지)
cat /proc//wchan
cat /proc//stack    # root 권한 필요

# 시스템 전체 I/O 상태
iostat -x 1 5
dmesg | tail -30

# NFS 마운트가 원인인 경우
mount | grep nfs
umount -f -l /mnt/nfs_share    # force + lazy unmount

해결 순서:

  1. NFS/CIFS라면 마운트 강제 해제 시도
  2. 블록 디바이스 I/O 오류라면 dmesg에서 disk error 확인
  3. 위 모두 실패 시 재부팅이 유일한 해결책

새 서비스를 띄우려는데 포트가 점유돼 있는 상황. 가장 흔한 신입 엔지니어 당황 포인트입니다.

진단:

bash# 어떤 프로세스가 8080을 쓰는지 확인
lsof -i :8080
ss -tlnp | grep 8080    # ss가 더 빠름

# PID 확인 후 해당 프로세스 정보 조회
ps -p  -o pid,ppid,user,command

# 혹시 TIME_WAIT 상태 커넥션인지 확인 (이 경우 프로세스가 없을 수도 있음)
ss -tnp | grep 8080

해결:

bash# 확인 후 종료
kill -SIGTERM 

# 자동으로 찾아서 종료하는 원라이너
lsof -t -i :8080 | xargs kill -SIGTERM

# 서비스가 systemd 관리라면
systemctl stop myservice

# SO_REUSEADDR가 없는 프로그램이라면 TIME_WAIT 소진까지 기다리거나
# net.ipv4.tcp_tw_reuse 커널 파라미터 조정 (주의해서 사용)

예방: 서비스 스크립트에 SO_REUSEADDR 소켓 옵션을 활성화하면 TIME_WAIT 상태 포트를 즉시 재사용할 수 있습니다.

rm으로 큰 로그 파일을 지웠는데 df -h에서 공간이 줄지 않는 상황. 파일을 열고 있는 프로세스가 살아있으면 inode가 해제되지 않습니다.

원인과 진단:

bash# 삭제됐지만 아직 열린 파일 찾기
lsof | grep deleted

# 출력 예시:
# nginx  1234  www-data  1w  REG  8,1  4294967296  12345  /var/log/nginx/access.log (deleted)
# 프로세스가 살아있어서 파일 디스크립터를 통해 여전히 파일에 접근 중

# 해당 파일 디스크립터를 /proc에서 직접 확인
ls -la /proc/1234/fd | grep 1    # fd 번호 1번

해결:

bash# 방법 1: 프로세스에 로그 재오픈 시그널 전송 (nginx의 경우 USR1)
kill -USR1 $(cat /var/run/nginx.pid)

# 방법 2: /proc/fd를 통해 파일 내용 비우기 (프로세스 재시작 없이)
# FD 번호를 lsof에서 확인 후:
> /proc/1234/fd/5    # fd 5번 파일 내용 truncate

# 방법 3: 서비스 재시작 (파일 디스크립터가 닫히면서 inode 해제)
systemctl restart nginx
OSError: [Errno 24] Too many open files

진단:

bash# 현재 프로세스의 열린 FD 수
ls /proc//fd | wc -l

# 시스템 한도 확인
ulimit -n                           # 현재 세션 soft limit
cat /proc//limits              # 특정 프로세스의 한도

# 어떤 파일이 많이 열렸는지 분석
lsof -p  | awk '{print $5}' | sort | uniq -c | sort -rn

# 소켓이 많다면 연결 상태 확인
lsof -p  -i | head -20

해결:

bash# 임시: 실행 중인 프로세스의 한도 늘리기 (root 필요)
prlimit --nofile=65536 --pid 

# 영구: /etc/security/limits.conf
echo "www-data soft nofile 65536" >> /etc/security/limits.conf
echo "www-data hard nofile 65536" >> /etc/security/limits.conf

# systemd 관리 서비스라면 [Service] 섹션에:
# LimitNOFILE=65536
systemctl edit myservice

실무 맥락

새벽 2시, PagerDuty 알람이 울립니다. "API 서버 CPU 사용률 98%, 응답 지연 10초." SSH 접속 직후 다음 순서로 움직입니다.

bash# Step 1: 현황 파악 (30초 이내)
top -bn1 | head -25
# 또는 더 상세하게:
ps aux --sort=-%cpu | head -10

# Step 2: 범인 프로세스 특정
# CPU 1위 프로세스의 PID를 기록: 예) PID=7823

# Step 3: 프로세스 신원 확인
ps -p 7823 -o pid,ppid,user,lstart,command
readlink /proc/7823/exe
cat /proc/7823/cmdline | tr '\0' ' '

# Step 4: 무슨 일을 하는지 strace로 확인 (10초만)
timeout 10 strace -c -p 7823
# 시스템 콜 통계에서 write()나 futex()가 압도적이면 각각 I/O 폭주, 락 경합

# Step 5: 열린 파일/소켓 확인
lsof -p 7823 | head -30

# Step 6: Java 프로세스라면 스레드 덤프
jstack 7823 > /tmp/threaddump_$(date +%Y%m%d_%H%M%S).txt

# Step 7: /proc로 추가 정보 수집
cat /proc/7823/status | grep -E "Threads|VmRSS|VmPeak"
cat /proc/7823/environ | tr '\0' '\n' | grep -E "JAVA|NODE|PYTHON"

# Step 8: 즉시 부하 경감이 필요하면
renice -n 19 -p 7823    # CPU 우선순위 낮춰서 다른 서비스 보호

# Step 9: 종료 결정 시 (데이터 유실 방지 우선)
kill -SIGTERM 7823
sleep 10
kill -0 7823 2>/dev/null && kill -SIGKILL 7823

# Step 10: 로그 확인
journalctl -u myservice --since "1 hour ago" | tail -100

핵심 원칙: 원인 모르게 kill -9부터 치면 문제 재현 기회를 잃습니다. 최소 30초는 정보 수집에 씁니다. 코어 덤프나 스레드 덤프를 떠놓으면 사후 분석이 가능합니다.

배포 스크립트를 돌렸는데 실제로 새 버전이 뜬 건지 확신이 안 설 때. 특히 무중단 배포(rolling update) 후에 흔히 하는 검증 루틴입니다.

bash# 1. 현재 실행 중인 프로세스와 실행 파일 경로 확인
ps aux | grep myapp
readlink /proc//exe

# 2. 프로세스 시작 시각 확인 (새로 뜬 게 맞는지)
ps -p  -o pid,lstart,etimes,command
# lstart: 시작 절대 시각, etimes: 실행 경과 초

# 3. 환경변수로 버전/환경 확인
cat /proc//environ | tr '\0' '\n' | grep -E "VERSION|ENV|APP"

# 4. 열린 파일로 어떤 config를 읽었는지 확인
lsof -p  | grep -E "\.conf|\.yaml|\.env"

# 5. 포트 바인딩 확인 (새 프로세스가 포트를 잡았는지)
lsof -i :8080 -P -n

# 6. 이전 프로세스가 아직 살아있는지 pstree로 확인
pstree -p | grep myapp

# 7. /proc/PID/maps로 로드된 라이브러리 버전 확인
grep "myapp" /proc//maps

이 루틴을 배포 후 체크리스트로 만들어두면 "배포했는데 왜 이전 버전으로 요청이 가지?"라는 황당한 상황을 막을 수 있습니다. 특히 심볼릭 링크로 배포하는 레거시 환경에서 readlink /proc/PID/exe는 강력한 검증 도구입니다.

개념
프로세스 소유권과 서비스 계정 — systemd User=/Group= 연계
프로세스 소유권 — root 실행 위험 vs 전용 서비스 계정 보안 비교

보안 감사에서 Node.js 웹 서버가 root로 실행되고 있다는 지적이 나왔습니다. 서비스가 정상 동작 중이라서 문제없어 보였지만, Node.js에 취약점이 생겼을 때 공격자가 root 권한으로 서버 전체를 장악할 수 있는 구조였습니다. systemd 유닛 파일에 User=와 Group= 한 줄씩 추가하면 해당 서비스를 전용 계정으로 실행할 수 있고, 파일 접근 범위도 그 계정으로 제한됩니다. 데몬 프로세스를 어느 계정으로 실행하느냐는 서비스 설계의 기본 보안 요소입니다.

실무에서 서비스는 반드시 전용 계정으로 실행해야 합니다. root로 실행하면 취약점 하나로 서버 전체가 위험해집니다.

systemd 서비스에서 계정 지정:

ini# /etc/systemd/system/myapp.service
[Service]
User=myapp              # 이 계정으로 프로세스 실행
Group=myapp
SupplementaryGroups=www-data   # 추가 그룹 (필요 시)

# 서비스 계정 보안 강화
NoNewPrivileges=yes     # SUID/권한 상승 금지
PrivateTmp=yes          # /tmp를 서비스 전용으로 격리
ProtectSystem=full      # /usr, /etc를 읽기 전용으로 마운트
ProtectHome=yes         # /home 접근 금지

서비스 전용 계정 생성 표준:

bash# 로그인 불가, 홈 없는 시스템 계정 생성
sudo useradd \
    --system \                    # UID 1-999 범위 (시스템 계정)
    --no-create-home \            # 홈 디렉토리 미생성
    --shell /usr/sbin/nologin \   # 로그인 차단
    --comment "MyApp Service" \
    myapp

# 확인: 로그인 시도 시 차단
su - myapp  # → This account is currently not available.

프로세스 소유권 확인:

bash# 서비스가 어떤 계정으로 실행 중인지 확인
ps aux | grep myapp | grep -v grep
# myapp   1234  0.0  0.1  ... ./myapp-server  ← 올바름
# root    1234  0.0  0.1  ... ./myapp-server  ← 위험!

# systemd로 실행 중인 서비스의 계정 확인
systemctl show myapp --property=User,Group
개념
컨테이너 환경에서 PID 해석 — 네임스페이스 차이
컨테이너 PID 네임스페이스 — 호스트 PID 847 = 컨테이너 PID 1, 같은 프로세스 다른 번호

컨테이너 내부와 호스트에서 같은 프로세스의 PID가 다릅니다. 이를 혼동하면 잘못된 프로세스에 kill 명령을 내릴 수 있습니다.

bash# 컨테이너 내부 PID (컨테이너 네임스페이스)
docker exec <container_id> ps aux
# PID 1 = 컨테이너의 메인 프로세스 (실제 호스트 PID와 다름)

# 호스트에서 같은 프로세스의 PID 찾기
docker inspect --format '{{.State.Pid}}' <container_id>
# 출력: 12345  ← 호스트에서의 실제 PID

# 호스트에서 확인
ps aux | grep 12345

# PID 1 원칙: 컨테이너에서 PID 1은 신호를 특별하게 처리
# SIGTERM을 받아도 기본 핸들러가 없으면 무시 → kill -9 필요
# 해결책: tini, dumb-init 같은 init 래퍼 사용
docker run --init myapp  # Docker 내장 tini 사용
트러블슈팅
OOM Killer가 엉뚱한 프로세스를 죽임 — 새벽에 DB 연결이 갑자기 끊기는 원인

새벽 3시, 슬랙에 알람이 쏟아집니다. "결제 서비스가 DB 연결 오류로 다운됐다"고 합니다. systemctl status payment를 보면 SIGKILL로 종료됐다는 로그가 있습니다. 프로세스가 스스로 크래시한 게 아니라 커널 OOM Killer에 의해 강제 종료된 것입니다.

bash# OOM Killer 발동 여부 확인
dmesg | grep -i "oom\|killed process" | tail -20
# [123456.789] Out of memory: Kill process 7823 (payment-svc) score 450 or sacrifice child
# [123456.790] Killed process 7823 (payment-svc) total-vm:2048000kB, anon-rss:1536000kB

# 어떤 프로세스들이 메모리를 많이 쓰는지 확인
ps aux --sort=-%mem | head -15

# 메모리 현황
free -h
cat /proc/meminfo | grep -E "MemTotal|MemFree|MemAvailable|SwapTotal|SwapFree"

# OOM 점수 확인 (점수 높을수록 먼저 죽음, 0=절대 안 죽임, 1000=무조건 죽임)
cat /proc/7823/oom_score         # 현재 점수
cat /proc/7823/oom_score_adj     # 조정값 (-1000 ~ 1000)

# 중요한 프로세스(DB)가 죽지 않도록 점수 낮추기
# oom_score_adj를 -500으로 설정 → OOM Killer가 기피
echo -500 | sudo tee /proc/$(pgrep postgres)/oom_score_adj

# 덜 중요한 서비스(배치 작업)가 먼저 죽도록 점수 높이기
echo 500 | sudo tee /proc/$(pgrep batch_job)/oom_score_adj

# 영구 설정: systemd 서비스에 OOMScoreAdjust 추가
# [Service]
# OOMScoreAdjust=-500

# 근본 원인 조사: 메모리 누수인지 한계 설정 문제인지
# 프로세스 메모리 증가 추이 확인 (재발 시)
while true; do
    ps -p $(pgrep payment-svc) -o pid,rss,vsz --no-headers
    sleep 5
done

교훈: OOM Killer는 기준이 있는 듯 없는 것처럼 느껴지지만, oom_score로 우선순위를 조정할 수 있습니다. 프로덕션에서 DB나 캐시 서버는 반드시 oom_score_adj를 낮게 설정해두세요.

트러블슈팅
프로세스가 SIGTERM을 무시함 — docker stop이 10초 후 강제 종료되는 이유

docker stop mycontainer를 실행하면 10초 대기 후 컨테이너가 강제 종료됩니다. 로그를 보면 정상 종료 처리(connection drain, flush to disk)가 완료되기 전에 잘려나가서 데이터가 유실됩니다.

bash# 증상: docker stop이 항상 10초 후 강제 종료
docker stop mycontainer
# (10초 침묵)
# mycontainer  ← 이게 뜨면 SIGKILL로 강제 종료된 것

# 원인 1: PID 1이 shell 스크립트일 때 (신호 전달 안 됨)
# Dockerfile에서 CMD ["./start.sh"] → start.sh가 PID 1
# shell은 기본적으로 SIGTERM을 자식에게 전달하지 않음

# 컨테이너 내부 PID 1 확인
docker exec mycontainer ps aux
# PID 1: /bin/sh ./start.sh  ← 문제! sh가 PID 1

# 해결 1: exec 형식 CMD 사용 (shell 래퍼 없이 직접 실행)
# Dockerfile에서:
# CMD ["uvicorn", "main:app", "--host", "0.0.0.0"]  # exec 형식

# 해결 2: 셸 스크립트에서 exec로 메인 프로세스 교체
# start.sh 마지막 줄:
# exec uvicorn main:app --host 0.0.0.0  # exec로 PID 1 교체

# 해결 3: tini/dumb-init으로 신호 전달 보장
docker run --init mycontainer   # Docker 내장 tini 사용

# 또는 Dockerfile에:
# RUN apt-get install -y tini
# ENTRYPOINT ["/usr/bin/tini", "--"]
# CMD ["uvicorn", "main:app"]

# 애플리케이션에서 SIGTERM 핸들러 구현 확인
# Python 예시:
# import signal, sys
# def graceful_shutdown(sig, frame):
#     print("Shutting down gracefully...")
#     db.close(); cache.disconnect()
#     sys.exit(0)
# signal.signal(signal.SIGTERM, graceful_shutdown)

# 테스트: SIGTERM 후 종료 시간 측정
time docker stop mycontainer
# real 0m2.3s  ← 2초 내 종료되면 graceful shutdown 성공

핵심: 컨테이너에서 PID 1의 신호 처리는 일반 프로세스와 다릅니다. exec 형식 CMD와 SIGTERM 핸들러를 애플리케이션에 구현하는 것이 표준 패턴입니다.

실무 맥락
프로덕션 API 서버 CPU 스파이크 장애 대응 — 30분 안에 원인 특정

시나리오: 프로덕션 API 서버 CPU 스파이크 장애 대응 — 30분 안에 원인 특정

금요일 오후 5시, "결제 API 응답 시간이 10초 이상으로 치솟고 있다"는 PagerDuty 알람. 30분 내로 원인을 찾거나 서비스를 복구해야 합니다.

bash# T+0:00 — SSH 접속 직후 현황 파악
top -bn1 | head -20
# CPU 사용률 97%, 특정 PID가 상위 점령 확인

# T+0:01 — 범인 프로세스 특정
ps aux --sort=-%cpu | head -5
# payment-svc  9823  94.3  2.1  ...  ./payment-server

# T+0:02 — 신원 확인 (언제부터? 배포 후인가?)
ps -p 9823 -o pid,lstart,etimes,command
# 실행 시각이 최근 배포 시각과 일치 → 코드 문제 의심

# T+0:03 — 무슨 시스템 콜을 하는지 (10초만)
timeout 10 strace -c -p 9823
# % time   calls  syscall
#  89.2   45231  futex     ← 락 경합! 스레드 간 데드락 의심

# T+0:05 — 스레드 상태 확인
ps -eLf | grep 9823
# 스레드가 모두 D 상태 → I/O 행이나 락 대기

# T+0:07 — Java라면 스레드 덤프, Python이라면 traceback 강제 출력
kill -SIGUSR1 9823   # JVM 스레드 덤프
# 또는
kill -SIGQUIT 9823   # Python traceback to stderr

# T+0:10 — 열린 DB 연결 확인 (커넥션 풀 고갈?)
lsof -p 9823 -i TCP | grep ESTABLISHED | wc -l
# 500개 → 커넥션 풀 설정(max_connections=10)보다 훨씬 많음

# T+0:12 — 즉각 부하 경감 (서비스 보호)
renice -n 15 -p 9823   # 다른 서비스에 CPU 양보

# T+0:15 — 근본 원인 파악됨: 특정 엔드포인트의 무한 재시도로 DB 연결 고갈
# /proc/environ으로 환경변수 확인 (재시도 설정)
cat /proc/9823/environ | tr '\0' '\n' | grep -i retry

# T+0:20 — 해당 엔드포인트 트래픽 차단 (nginx 레벨)
# upstream에서 해당 경로 임시 503 처리

# T+0:25 — 프로세스 정상 재시작
kill -SIGTERM 9823
sleep 5
systemctl restart payment.service

# T+0:30 — 복구 확인
curl -w "\nHTTP %{http_code} in %{time_total}s\n" https://api.example.com/health

실무 포인트: 장애 대응의 핵심은 순서입니다. 증거 수집(strace, lsof, /proc) → 가설 수립 → 부하 경감 → 근본 원인 해결. kill -9부터 치는 것은 증거를 파괴하는 행위입니다. 스레드 덤프와 strace 결과를 /tmp에 저장해두면 사후 포스트모텀에서 값진 자료가 됩니다.

다음 모듈에서는 프로세스 시그널(Process Signals)을 더 깊이 다룹니다 — SIGTERM, SIGKILL, SIGHUP, SIGUSR1/2의 동작 차이, 시그널 핸들러 작성, 그리고 graceful shutdown 패턴을 배웁니다.

반응형
LIST

'Linux' 카테고리의 다른 글

[Linux ] systemd 서비스 관리  (0) 2026.05.22
[Linux ] 시그널 & 프로세스 종료  (0) 2026.05.22
[Linux] 환경변수 & dotfiles  (0) 2026.05.22
[Linux] tmux & 백그라운드 세션 관리  (0) 2026.05.22
[Linux] 패키지 관리 (apt/yum/dnf)  (0) 2026.05.22
'Linux' 카테고리의 다른 글
  • [Linux ] systemd 서비스 관리
  • [Linux ] 시그널 & 프로세스 종료
  • [Linux] 환경변수 & dotfiles
  • [Linux] tmux & 백그라운드 세션 관리
cumo
cumo
  • cumo
    이것저것
    cumo
    • 분류 전체보기 (147)
      • 이것저것 (1)
      • 보안뉴스 (15)
      • Project (12)
      • wargame (1)
      • Cloud (25)
      • DevOps (21)
      • Linux (43)
      • 네트워크 (23)
      • AWS Developer BootCamp (1)
      • WEB&WAS (3)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

    • 도구모음 사이트
    • 참고 기술 블로그
  • 공지사항

  • 인기 글

  • 태그

    nano
    bash입문
    디렉토리구조
    서버편집
    서버관리
    텍스트편집기
    리눅스
    Linux
    vi
    인프라
    눅스네트워킹
    ubuntu
    스토리지확장
    포트진단
    Volume Group
    터미널멀티플렉서
    vim
    부팅서비스
    데몬
    논리볼륨
  • 최근 댓글

  • 최근 글

  • 반응형
  • hELLO· Designed By정상우.v4.10.3
cumo
[Linux] 프로세스 관리 (Process Management)
상단으로

티스토리툴바