infra
Platform

모듈 맵

[Network] ss/lsof로 포트를 점유한 좀비 프로세스 찾아 강제 종료하기

0 / 35 완료

펼치기
0 / 35 완료0%

네트워크 트러블슈팅 · 13 / 35

[Network] ss/lsof로 포트를 점유한 좀비 프로세스 찾아 강제 종료하기

port already in use 에러를 해결하고 포트를 점유한 프로세스를 정리합니다

🚨INCIDENT ALERT
HIGH

개발 서버 배포가 address already in use로 실패했습니다. 급해서 아무 PID나 kill했다가 같은 서버의 다른 테스트 작업까지 끊어버렸습니다.

포트를 잡은 프로세스를 정확히 찾고 안전하게 종료하는 순서가 필요합니다. 강제 종료는 마지막 선택이어야 합니다.

포트 사용 중인 프로세스 강제 종료

서버에서 새 서비스를 시작하려 할 때 address already in use 또는 port already in use 에러가 뜨는 경우가 있습니다. 이 장에서는 포트를 점유한 프로세스를 찾고, 상황에 맞게 종료하는 방법을 다룹니다.


이번 챕터에서 배울 것
  • 1포트 충돌(address already in use) 발생 원인과 소켓 구조를 설명할 수 있다
  • 2lsof / ss / fuser로 포트를 점유한 프로세스를 식별할 수 있다
  • 3kill SIGTERM·SIGKILL 시그널의 차이를 이해하고 올바른 종료 순서를 적용할 수 있다
  • 4systemd Restart=always 서비스의 포트 충돌을 해결할 수 있다
  • 5Docker 컨테이너의 포트 점유를 해제할 수 있다
  • 6TIME_WAIT 소켓으로 인한 바인딩 실패에 대응할 수 있다
실습 환경 준비
포트 점유 프로세스 확인
sudo ss -tulnp | grep :포트번호
테스트용 서버 프로세스 실행
python3 -m http.server 8888 &
lsof로 상세 소켓 정보 확인
sudo lsof -i :8888
systemd 서비스 상태 확인
systemctl list-units --type=service --state=running
💡개념

포트 충돌의 원인과 구조

서비스를 재시작했는데 "Address already in use: 8080"이 납니다. systemctl stop을 해서 프로세스를 내렸는데도 포트가 여전히 잡혀있습니다. 뭔가가 포트를 붙들고 있는데 무엇인지, 왜 stop을 해도 안 죽었는지 알 방법이 없습니다. 포트 충돌의 구조를 알아야 sslsof로 원인을 찾고 올바르게 처리할 수 있습니다.

포트 충돌의 원인과 구조 — "Address already in use"는 어떤 프로세스가 그 포트를 LISTEN 중이거나, 종료된 연결이 TIME_WAIT로 잔류하거나, 자식 프로세스가 포트를 물려받아 살아있을 때 발생. ss -tlnp·lsof -i :8080으로 점유 PID를 찾아 올바른 프로세스를 종료확대

"port already in use" 에러란

하나의 포트(IP:포트 조합)는 동시에 하나의 프로세스만 바인딩할 수 있습니다. 이미 사용 중인 포트에 새 프로세스가 바인딩하려 하면 OS가 오류를 반환합니다.

Node.js:   Error: listen EADDRINUSE: address already in use :::80
Python:    OSError: [Errno 98] Address already in use
Java:      java.net.BindException: Address already in use
nginx:     bind() to 0.0.0.0:80 failed (98: Address already in use)

포트 충돌이 발생하는 주요 원인

원인설명
이전 프로세스가 살아있음서비스 재시작 시 기존 인스턴스가 종료되지 않음
좀비 소켓 (TIME_WAIT)TCP 연결 종료 후 소켓이 일정 시간 유지됨
다른 서비스 충돌예: nginx와 Apache 둘 다 80 포트 사용 설정
Docker 컨테이너컨테이너가 호스트 포트를 점유 중
systemd 재시작 정책Restart=always로 인해 프로세스가 계속 살아남

포트와 소켓의 관계

포트 바인딩 충돌 — 프로세스 A가 0.0.0.0:80을 bind·listen하면, 같은 포트에 bind하려는 프로세스 B는 EADDRINUSE로 거부됨확대

0.0.0.0:80은 "모든 인터페이스의 80번 포트"를 의미합니다. 반면 127.0.0.1:80으로 바인딩된 프로세스가 있어도, 0.0.0.0:80에 바인딩하려 하면 충돌이 발생합니다.

포트 상태 종류

LISTEN    — 포트를 열고 새 연결 대기 중 (점유 중)
ESTABLISHED — 현재 활성 연결 (통신 중)
TIME_WAIT — 연결 종료 후 일정 시간 유지 (보통 60~120초)
CLOSE_WAIT — 원격 측이 연결 종료 (로컬 애플리케이션이 close 안 함)

TIME_WAIT 상태 소켓은 프로세스가 없어도 잠시 포트를 점유합니다.


💡개념

포트 점유 프로세스 확인 도구

포트 충돌을 확인하려 합니다. 어떤 프로세스가 포트를 쓰는지, PID는 무엇인지, 시스템에 따라 lsof가 있는 서버도 있고 ss밖에 없는 서버도 있습니다. 두 도구의 사용법을 알아야 어떤 환경에서든 포트 점유 프로세스를 찾고 처리할 수 있습니다.

1. lsof: 열린 파일/소켓 목록

lsof(list open files)는 프로세스가 열고 있는 파일, 소켓, 포트 정보를 보여줍니다.

로컬 터미널
# 실습 디렉토리 준비
mkdir -p /tmp/networking/part3/exam_13 && cd /tmp/networking/part3/exam_13

# 80번 포트를 사용 중인 프로세스 확인
$ sudo lsof -i :80
COMMAND  PID   USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
nginx   1234   root   6u  IPv4  12345      0t0  TCP *:http (LISTEN)
nginx   1235 nobody   6u  IPv4  12345      0t0  TCP *:http (LISTEN)

# 특정 포트 범위 확인
$ sudo lsof -i :8080-8090

# TCP만 확인
$ sudo lsof -i TCP:80

# 특정 프로세스가 열고 있는 포트 확인
$ sudo lsof -p 1234 -i
🔍실행 후 확인할 것
  • ss -tulnp 출력에서 users:(("프로세스명",pid=숫자)) 형식 확인 — 먼저 프로세스명이 예상 서비스가 맞는지 보고, 그 다음 PID를 기록한다
  • PID 확인 후 ps aux | grep PID 로 프로세스 실행 경로까지 확인 — 동명 프로세스가 여러 개일 때 잘못된 PID를 종료하면 서비스 장애가 된다
  • kill 실행 후 5초 이내 ss -tulnp | grep :포트번호 출력이 사라지면 정상 종료; 여전히 나타나면 systemd Restart=always 가능성 — systemctl status $(ps -o unit= -p PID)로 확인

출력 필드 의미:

  • COMMAND: 프로세스 이름
  • PID: 프로세스 ID
  • USER: 실행 사용자
  • FD: 파일 디스크립터
  • NODE NAME: 소켓 주소 (IP:포트)

2. ss: 소켓 상태 조회 (권장)

ssnetstat의 현대적 대체 도구로 더 빠르고 정확합니다.

로컬 터미널
# 80번 포트 상태 확인
$ ss -tulnp | grep :80
tcp   LISTEN 0      128    0.0.0.0:80    0.0.0.0:*    users:(("nginx",pid=1234,fd=6))

# 옵션 설명:
# -t: TCP 소켓
# -u: UDP 소켓
# -l: LISTEN 상태 (대기 중인 포트)
# -n: 이름 해석 없이 숫자로 표시
# -p: 프로세스 정보 포함

# 특정 포트 연결 상태 포함 조회
$ ss -tnp | grep :80

3. fuser: 파일/소켓 사용 프로세스

로컬 터미널
# 80번 TCP 포트를 사용하는 PID 확인
$ fuser 80/tcp
80/tcp:  1234 1235

# 프로세스 정보 포함
$ fuser -v 80/tcp
                     USER        PID ACCESS COMMAND
80/tcp:              root       1234 F.... nginx
                     nobody     1235 F.... nginx

# 확인 없이 즉시 종료
$ sudo fuser -k 80/tcp

# 확인하면서 종료
$ sudo fuser -ki 80/tcp

4. netstat (구형, 참고용)

로컬 터미널
# 80번 포트 확인 (일부 구형 환경)
$ netstat -tlnp | grep :80
tcp   0   0 0.0.0.0:80   0.0.0.0:*   LISTEN   1234/nginx

도구 비교

도구장점단점
ss빠름, 현대적일부 구형 OS 미설치
lsof상세 정보느릴 수 있음
fuser직접 kill 가능정보 제한적
netstat익숙함느림, deprecated

포트 점유 확인 도구 비교와 안전한 종료 순서 — ss -tlnp(빠름·현대적, 권장)·lsof -i(상세 정보)·fuser -k(직접 kill 가능하나 무관 프로세스까지 위험)·netstat(구형·deprecated). 종료는 ① ss/lsof로 PID·정체 확인 → ② SIGTERM(kill) → ③ 5~10초 대기·재확인 → ④ 안 풀리면 SIGKILL(kill -9) 순서로. TIME_WAIT 바인딩 거부는 정상(죽일 것 없음), Restart=always로 되살아나면 systemctl stop으로 감독자를 멈춘다확대

💡개념

포트 충돌을 만나 프로세스를 정리하기까지 — 역추적부터 종료까지 6단계

Address already in use는 "그 포트를 이미 누군가 쥐고 있다"는 커널의 거절입니다. 여기서 곧장 kill -9부터 치면 엉뚱한 프로세스를 죽이기 쉽습니다. 충돌이 나는 원리부터 안전한 종료까지는 bind 거절 → PID 역추적 → 정체 확인 → 정상 종료 → 강제 종료 → 재확인의 순서를 따릅니다. 이 흐름을 알면 각 단계에서 무엇이 어긋났는지로 "왜 안 죽지", "왜 자꾸 살아나지"를 구분할 수 있습니다.

TEXT
[새 프로세스]  bind(0.0.0.0:8080)
   │
   ① 이미 그 IP·포트를 점유한 소켓이 있으면 커널이 EADDRINUSE 반환
   │      → "Address already in use"
   │
   ② ss -tlnp / lsof -i 로 그 포트의 LISTEN 소켓 → users:(("proc",pid=N)) 로 PID 역추적
   │
   ③ ps 로 그 PID의 실행 경로·부모를 확인       (죽여도 되는 프로세스인가?)
   │
   ④ SIGTERM(kill PID) → 앱이 커넥션·임시파일 정리 후 소켓 close → 포트 해제
   │
   ⑤ 5~10초 뒤에도 LISTEN이 남으면 SIGKILL(kill -9) → 커널이 강제로 소켓 회수
   │
   ⑥ ss 로 포트가 비었는지 재확인 (PID가 바뀌어 살아났으면 감독자 재시작)
   ▼
[정리 완료]

각 단계에서 하는 일과, 막히는 지점의 증상:

단계하는 일여기서 막히면
① bind 충돌한 IP·포트는 한 소켓만 점유 가능. 겹치면 커널이 EADDRINUSE로 거절프로세스가 죽었는데도 TIME_WAIT 소켓이 잔류하면 같은 에러 — 이건 죽일 대상이 없다
② PID 역추적ss -tlnp 또는 lsof -i로 그 포트를 쥔 소켓의 PID·프로세스명을 얻는다출력이 비면 그 포트를 LISTEN하는 프로세스가 없는 것 — 컨테이너(docker-proxy)나 TIME_WAIT를 의심
③ 정체 확인ps로 실행 경로·부모를 확인해 정말 죽여도 되는지 판단동명 프로세스가 여러 개면 엉뚱한 PID를 골라 다른 서비스를 중단시킴
④ 정상 종료SIGTERM으로 앱에 정리할 기회를 준다. 대부분 여기서 깔끔히 해제앱이 시그널 핸들러로 무시하거나 정리에 오래 걸리면 포트가 바로 안 풀림
⑤ 강제 종료SIGKILL은 앱이 잡을 수 없는 신호로, 커널이 프로세스를 즉시 회수프로세스가 D 상태(커널 I/O 대기)면 SIGKILL도 밀려 같은 PID가 살아남음
⑥ 재확인포트가 비었는지, PID가 바뀌어 부활했는지 확인PID가 바뀌어 되살아나면 systemd·supervisor가 재시작 중 → systemctl stop으로 감독자를 멈춰야 함

"안 죽는다"는 두 가지로 갈립니다kill -9 후 같은 PID가 그대로면 시그널이 전달 못 된 것(⑤, D 상태처럼 커널 I/O에 갇힘)이고, PID가 바뀌어 부활하면 감독 프로세스의 재시작(⑥)입니다. PID가 그대로냐 바뀌냐, 이 한 가지가 두 원인을 가릅니다. 그래서 순서는 언제나 '무엇인지 확인(②③) → 부드럽게(④) → 강하게(⑤) → 되살아나면 감독자부터(⑥)'이며, PID 확인 없는 kill -9는 사고의 지름길입니다.

포트 점유 프로세스 종료 판단 — 무엇을, 어떤 신호로, 언제까지
프로세스 정체부터 확인죽이기 전에 ss -ltnp/lsof로 PID와 '무엇인지' 확인 — 같은 포트를 다른 중요 서비스가 쓰고 있을 수 있다. 'PID 확인 없는 kill -9는 사고'
정상 종료 시도먼저 SIGTERM(kill <PID> 또는 systemctl stop). 앱이 커넥션·임시파일 정리. 5~10초 기다린 뒤 ss로 포트가 풀렸는지 재확인 — '신호 보내고 바로 -9 아님'
TERM 후에도 포트가 안 풀림10초 정도 기다려도 LISTEN이 남으면 SIGKILL(kill -9). systemd 서비스라면 TimeoutStopSec(기본 90s) 안에 안 죽으면 자동 KILL되는 것과 동일 원리
포트는 풀렸는데 TIME_WAIT로 바인딩 거부프로세스는 죽었지만 커널이 TIME_WAIT 유지(기본 60s). 죽일 대상이 아니라 SO_REUSEADDR 설정 또는 잠깐 대기 — 'TIME_WAIT은 정상, 강제로 죽일 것 없음'
이름으로 한꺼번에 (fuser/pkill)fuser -k <port>/tcp는 편하지만 같은 이름 무관 프로세스까지 휩쓸 위험. 운영 서버는 PID 지정 kill이 안전 — '편의 vs 안전'에서 운영은 안전
죽여도 계속 되살아남systemd/supervisor가 Restart=always로 재기동 중. kill 말고 systemctl stop으로 관리자를 멈춰야 함. '감독 프로세스부터 멈춰라'

실습 1: 포트 점유 프로세스 식별

목표

포트를 점유한 프로세스를 세 가지 방법으로 확인하고 결과를 비교합니다.

단계

1단계: 테스트용 서버 프로세스 실행 — nc로 간단한 리스닝 서버를 띄웁니다.

로컬 터미널
# Python으로 간단한 HTTP 서버 실행 (8888번 포트)
$ python3 -m http.server 8888 &
[1] 12345
Serving HTTP on 0.0.0.0 port 8888 ...

2단계: lsof로 확인 — 포트를 점유한 프로세스의 PID와 경로를 확인합니다.

로컬 터미널
$ sudo lsof -i :8888
COMMAND   PID USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
python3 12345 user    3u  IPv4  98765      0t0  TCP *:8888 (LISTEN)

3단계: ss로 확인 — 소켓 상태와 PID를 함께 출력합니다.

로컬 터미널
$ ss -tulnp | grep :8888
tcp   LISTEN 0      5    0.0.0.0:8888  0.0.0.0:*  users:(("python3",pid=12345,fd=3))

4단계: fuser로 확인 — 포트를 직접 지정해 사용 중인 PID를 찾습니다.

로컬 터미널
$ fuser 8888/tcp
8888/tcp:             12345

5단계: PID로 프로세스 상세 정보 확인 — PID로 어떤 프로세스인지 상세 확인합니다.

로컬 터미널
# PID로 프로세스 정보 조회
$ ps aux | grep 12345
user  12345  0.0  0.1  15000  8000 pts/0  S  10:30  0:00 python3 -m http.server 8888

# 프로세스 실행 경로 확인
$ ls -la /proc/12345/exe
lrwxrwxrwx 1 user user 0 Mar 27 10:30 /proc/12345/exe -> /usr/bin/python3

6단계: 테스트 서버 종료 — 실습 후 nc 프로세스를 정리합니다.

위험 명령어

이 명령은 프로세스를 종료해 연결 중인 사용자나 배치 작업을 중단시킬 수 있습니다. PID와 프로세스 이름이 목표 서비스인지 확인한 뒤 실행하세요.

로컬 터미널
$ kill %1   # 백그라운드 작업 번호로 종료
# 또는
$ kill 12345

정리: 포트 확인 원라이너

로컬 터미널
# 가장 간결한 방법
$ sudo ss -tulnp | grep :포트번호

# PID만 빠르게 확인
$ fuser 포트번호/tcp

실습 2: 포트를 점유한 프로세스 종료

목표

포트를 점유한 프로세스를 상황별로 올바르게 종료합니다.

단계

1단계: 테스트 환경 준비 — 종료 연습용 프로세스를 두 개 띄웁니다.

로컬 터미널
# 두 개의 서버 프로세스를 다른 포트로 실행
$ python3 -m http.server 9001 &
$ python3 -m http.server 9002 &

$ ss -tulnp | grep '900[12]'
tcp LISTEN 0 5  0.0.0.0:9001  0.0.0.0:*  users:(("python3",pid=11111,fd=3))
tcp LISTEN 0 5  0.0.0.0:9002  0.0.0.0:*  users:(("python3",pid=11112,fd=3))

2단계: kill 명령어로 PID 종료 — SIGTERM(15)으로 정상 종료를 먼저 시도합니다.

위험 명령어

이 명령은 프로세스를 종료해 연결 중인 사용자나 배치 작업을 중단시킬 수 있습니다. PID와 프로세스 이름이 목표 서비스인지 확인한 뒤 실행하세요.

로컬 터미널
# PID 확인 후 종료
$ PID=$(ss -tulnp | grep :9001 | grep -oP 'pid=\K[0-9]+')
$ echo "종료할 PID: $PID"
$ kill $PID    # SIGTERM (정상 종료 요청)

# 응답 없을 때 강제 종료
$ kill -9 $PID   # SIGKILL (즉시 강제 종료)

3단계: fuser -k로 포트 직접 종료 — PID를 몰라도 포트 번호만으로 종료할 수 있습니다.

로컬 터미널
# 포트 번호로 직접 종료 (PID 확인 불필요)
$ sudo fuser -k 9002/tcp

# 확인
$ ss -tulnp | grep :9002
# 아무것도 출력되지 않으면 성공

4단계: kill -9 시그널 차이 이해 — SIGKILL은 프로세스가 무시할 수 없는 강제 종료입니다.

위험 명령어

이 명령은 프로세스를 종료해 연결 중인 사용자나 배치 작업을 중단시킬 수 있습니다. PID와 프로세스 이름이 목표 서비스인지 확인한 뒤 실행하세요.

로컬 터미널
# SIGTERM (15): 정상 종료 요청, 프로세스가 정리 작업 수행 가능
$ kill -15 [PID]   # 또는 kill [PID]

# SIGKILL (9): 즉시 강제 종료, OS가 직접 처리
$ kill -9 [PID]

# 실무 권장 순서
$ kill [PID]        # 먼저 SIGTERM 시도
$ sleep 5           # 5초 대기
$ kill -9 [PID]     # 응답 없으면 SIGKILL

5단계: 종료 후 포트 확인 — 포트가 해제됐는지 ss로 확인합니다.

로컬 터미널
$ ss -tulnp | grep '900[12]'
# 아무것도 출력되지 않으면 포트 해제 완료

실습 3: systemd 서비스의 포트 충돌 해결

목표

Restart=always 설정으로 인해 kill해도 살아나는 서비스를 올바르게 중지합니다.

systemd Restart 정책 이해

로컬 터미널
# 서비스 설정 파일 예시
$ cat /etc/systemd/system/myapp.service
[Unit]
Description=My Application

[Service]
ExecStart=/usr/bin/node /opt/myapp/server.js
Restart=always          # 어떤 이유로든 종료되면 재시작
RestartSec=3            # 3초 후 재시작

[Install]
WantedBy=multi-user.target

단계

1단계: 서비스 상태 확인 — systemctl로 nginx가 실제로 실행 중인지 확인합니다.

로컬 터미널
# 서비스가 포트를 점유 중인지 확인
$ sudo ss -tulnp | grep :3000
tcp LISTEN 0 128  0.0.0.0:3000  0.0.0.0:*  users:(("node",pid=5678,fd=15))

# 해당 PID의 서비스 확인
$ systemctl status $(ps -o unit= -p 5678)
● myapp.service - My Application
   Loaded: loaded (/etc/systemd/system/myapp.service; enabled)
   Active: active (running)

2단계: kill -9로 시도 (실패 케이스) — systemd 관리 서비스는 kill로 종료해도 즉시 재시작됩니다.

위험 명령어

이 명령은 프로세스를 종료해 연결 중인 사용자나 배치 작업을 중단시킬 수 있습니다. PID와 프로세스 이름이 목표 서비스인지 확인한 뒤 실행하세요.

로컬 터미널
$ sudo kill -9 5678
# 3초 후 확인
$ sleep 4 && sudo ss -tulnp | grep :3000
tcp LISTEN 0 128  0.0.0.0:3000  0.0.0.0:*  users:(("node",pid=5901,fd=15))
# PID가 바뀐 채로 다시 살아남!

3단계: systemctl stop으로 올바르게 중지 — systemd가 관리하는 서비스는 반드시 systemctl로 중지합니다.

위험 명령어

이 명령은 실행 중인 서비스 상태를 바꿔 순간적인 중단이나 설정 반영 실패를 만들 수 있습니다. 운영 트래픽 영향과 재시작 후 확인 명령을 먼저 준비하세요.

로컬 터미널
# systemd를 통해 서비스 중지 (재시작 정책 무시)
$ sudo systemctl stop myapp.service

# 확인
$ sudo ss -tulnp | grep :3000
# 출력 없음 — 포트 해제됨

# 서비스 상태 확인
$ systemctl status myapp.service
● myapp.service - My Application
   Active: inactive (dead)

4단계: 서비스 비활성화 (부팅 시 자동 시작 제거) — disable하면 재부팅 후 자동 시작을 막습니다.

위험 명령어

이 명령은 실행 중인 서비스 상태를 바꿔 순간적인 중단이나 설정 반영 실패를 만들 수 있습니다. 운영 트래픽 영향과 재시작 후 확인 명령을 먼저 준비하세요.

로컬 터미널
# 중지 + 부팅 자동 시작 해제
$ sudo systemctl stop myapp.service
$ sudo systemctl disable myapp.service

Docker 컨테이너 포트 점유 해결

Docker 포트 포워딩 충돌과 대화형 모니터링 연계

SRE는 프로세스가 점유한 포트를 강제 해제할 때, 일반 OS 데몬뿐 아니라 커널 레벨에서 Docker 엔진이 기동시킨 포트 바인딩도 함께 통제해야 합니다.

  1. Docker 컨테이너 포트 점유 해소: Docker는 컨테이너 실행 시 호스트 커널의 iptables NAT 테이블을 관리합니다. 따라서 일반적인 kill -9 대신 docker psdocker port로 소유 컨테이너를 확인한 뒤 docker stop <컨테이너명>(필요 시 승인된 docker rm)으로 정리합니다. Docker가 관리하는 iptables 규칙을 수동으로 iptables -D하지 마세요. 엔진 상태와 규칙이 어긋날 수 있으므로 계획된 점검에서 Docker 설정을 통해 해결해야 합니다.
  2. 대화형 리소스 모니터링 도구 활용: 단순히 포트를 점유한 PID만으로 소유 서비스를 알기 어려우면 htop이나 ps --forest로 부모·자식 관계를 확인합니다. 프로세스 트리를 한꺼번에 종료하지 말고, 먼저 systemd·Docker·프로세스 매니저 중 누가 재기동을 관리하는지 찾은 뒤 해당 관리 도구의 정상 stop 절차를 사용합니다.
로컬 터미널
# Docker가 포트를 점유 중인 경우
$ sudo ss -tulnp | grep :80
tcp LISTEN 0 128  0.0.0.0:80  0.0.0.0:*  users:(("docker-proxy",pid=7890,fd=4))

# 어떤 컨테이너인지 확인
$ docker ps | grep "0.0.0.0:80"
a1b2c3d4e5f6  nginx:latest  "/docker-entrypoint…"  Up 2 hours  0.0.0.0:80->80/tcp

# 컨테이너 중지
$ docker stop a1b2c3d4e5f6

# 확인
$ sudo ss -tulnp | grep :80
# 출력 없음

현상

포트를 점유한 프로세스를 kill -9로 종료했는데 수 초 후 다시 살아납니다.

위험 명령어

이 명령은 프로세스를 종료해 연결 중인 사용자나 배치 작업을 중단시킬 수 있습니다. PID와 프로세스 이름이 목표 서비스인지 확인한 뒤 실행하세요.

로컬 터미널
$ sudo kill -9 $(fuser 8080/tcp)
$ sleep 3
$ fuser 8080/tcp
8080/tcp:  9999   # 다른 PID로 다시 살아남!

원인 분석

  1. systemd Restart=always: 가장 흔한 원인. systemd가 프로세스를 자동 재시작
  2. supervisor/pm2 관리: 프로세스 관리 도구가 재시작
  3. crontab: 주기적으로 프로세스를 실행하는 크론 작업
  4. init.d 스크립트: 구형 SysV init 방식의 모니터링

원인 파악 및 해결

위험 명령어

이 명령은 실행 중인 서비스 상태를 바꿔 순간적인 중단이나 설정 반영 실패를 만들 수 있습니다. 운영 트래픽 영향과 재시작 후 확인 명령을 먼저 준비하세요.

로컬 터미널
# 1. systemd 서비스인지 확인
$ systemctl list-units --type=service --state=running | grep -i 해당서비스명

# systemd 서비스면
$ sudo systemctl stop 서비스명

# 2. supervisor로 관리되는지 확인
$ sudo supervisorctl status

# supervisor 프로세스면
$ sudo supervisorctl stop 프로그램명

# 3. pm2로 관리되는지 확인 (Node.js)
$ pm2 list

# pm2 프로세스면
$ pm2 stop 앱이름

# 4. crontab 확인
$ sudo crontab -l
$ crontab -l   # 현재 사용자

완전 종료 확인 방법

위험 명령어

이 명령은 실행 중인 서비스 상태를 바꿔 순간적인 중단이나 설정 반영 실패를 만들 수 있습니다. 운영 트래픽 영향과 재시작 후 확인 명령을 먼저 준비하세요.

로컬 터미널
# 종료 후 5초 대기하며 재확인
$ sudo systemctl stop myapp
$ for i in {1..5}; do
    echo "[$i초] 포트 상태:"
    ss -tulnp | grep :8080 || echo "포트 해제됨"
    sleep 1
  done

현상

기존 프로세스를 종료했는데도 새 서비스 시작 시 여전히 포트 충돌이 발생합니다.

위험 명령어

이 명령은 실행 중인 서비스 상태를 바꿔 순간적인 중단이나 설정 반영 실패를 만들 수 있습니다. 운영 트래픽 영향과 재시작 후 확인 명령을 먼저 준비하세요.

로컬 터미널
$ sudo systemctl stop nginx
$ sudo systemctl start myapp
Job for myapp.service failed. See 'journalctl -xe'
# Error: bind EADDRINUSE: address already in use :::80

원인 분석

TIME_WAIT 소켓

TCP 연결 종료 후 소켓은 TIME_WAIT 상태로 일정 시간(기본 60초) 유지됩니다.

로컬 터미널
$ ss -tn | grep TIME-WAIT | grep :80
TIME-WAIT  0  0  192.168.1.50:80  10.0.0.1:54321

SO_REUSEADDR 미설정

애플리케이션이 SO_REUSEADDR 소켓 옵션을 설정하지 않으면 TIME_WAIT 상태의 소켓과 충돌할 수 있습니다.

해결 방법

위험 명령어

이 명령은 실행 중인 서비스 상태를 바꿔 순간적인 중단이나 설정 반영 실패를 만들 수 있습니다. 운영 트래픽 영향과 재시작 후 확인 명령을 먼저 준비하세요.

로컬 터미널
# 방법 1: 잠시 대기 (TIME_WAIT 소멸 기다림)
$ sleep 60 && sudo systemctl start myapp

# 방법 2: 커널 파라미터로 TIME_WAIT 재사용 허용
$ sudo sysctl -w net.ipv4.tcp_tw_reuse=1

# 방법 3: ss로 TIME_WAIT 상태 소켓 모니터링
$ watch -n 1 'ss -tn | grep TIME-WAIT | grep :80'

# 방법 4: 남아있는 프로세스 최종 확인
$ sudo lsof -i :80
$ sudo ss -tulnp | grep :80
$ fuser 80/tcp

예방 방법

  • 애플리케이션 코드에 SO_REUSEADDR 옵션 설정 (개발자에게 요청)
  • 서비스 중지 후 최소 30초 대기 후 재시작
  • systemd 서비스에 TimeoutStopSec 설정으로 충분한 종료 시간 확보

심화 — kill -9가 정말로 안 먹힐 때

💡개념

심화: SIGKILL도 못 죽이는 프로세스 — 시그널은 언제 전달되는가

kill -9를 쐈는데 프로세스가 안 죽는 상황은 두 종류로 갈립니다. 하나는 앞에서 본 재시작(supervisor·systemd가 다른 PID로 부활시키는 것), 다른 하나는 커널이 아직 시그널을 전달하지 못한 경우입니다. 이 둘을 구분하지 못하면 없는 supervisor를 계속 뒤지게 됩니다.

  • 시그널은 '즉시'가 아니라 복귀 시점에 처리됩니다: 시그널은 프로세스가 커널 모드에서 사용자 모드로 돌아오는 순간에 전달·처리됩니다. SIGKILL(9)은 프로세스가 잡거나 무시할 수 없는 유일한 시그널이지만, 그렇다고 커널이 전달 '시점'까지 앞당겨 주지는 않습니다.
  • D 상태(uninterruptible sleep)에 갇히면 SIGKILL도 밀립니다: 프로세스가 디스크·NFS 같은 커널 I/O를 기다리며 D 상태에 들어가면, 그 syscall이 끝나 사용자 모드로 복귀하기 전까지는 SIGKILL조차 대기열에 밀려 적용되지 않습니다. ps에서 STAT가 D로 보이고, 응답 없는 NFS 서버를 기다리면 무한정 D에 갇힐 수 있습니다. 그동안 포트도 그대로 잡혀 있습니다.
  • 재시작과의 결정적 차이는 PID: kill -9 PIDss -tlnp의 PID가 그대로면 재시작이 아니라 '안 죽은' 것이고, PID가 바뀌면 supervisor/systemd 재시작입니다. 이 한 가지로 두 원인을 즉시 가릅니다.
  • 좀비(defunct, Z 상태)는 포트를 안 잡습니다: 좀비는 이미 소켓·FD를 전부 반납하고 종료 코드만 남은 상태라, kill의 대상도 포트 점유자도 아닙니다(부모가 wait해야 사라집니다). 포트를 붙든 건 살아 있는(R/S/D) 프로세스입니다.

상황: 배포하려고 8080을 점유한 프로세스를 kill -9로 종료했는데, fuser 8080/tcp로 다시 보면 매번 똑같은 PID입니다. supervisor·pm2·크론을 뒤졌지만 이 프로세스를 재시작하도록 등록된 것은 없습니다.

원인: 그 프로세스가 D 상태(uninterruptible sleep) 에 빠져 있었습니다. 마운트가 끊긴 NFS 경로에 로그를 쓰다가 커널 I/O에서 블록됐고, syscall이 끝나지 않아 SIGKILL이 전달되지 못한 것입니다. '부활'이 아니라 '안 죽는' 것이며, PID가 바뀌지 않은 게 결정적 단서입니다.

진단: ps -o pid,stat,wchan,cmd -p PID로 STAT가 D인지, wchan(대기 중인 커널 함수)이 무엇인지 확인합니다. cat /proc/PID/stack으로 어느 커널 경로에 갇혔는지, mountdmesg로 응답 없는 NFS/스토리지가 있는지 봅니다. STAT가 D면 kill로 풀 문제가 아니라는 신호입니다.

해결: 근본은 막힌 I/O를 풀어 주는 것입니다. 끊긴 NFS를 복구하거나 umount -f/-l로 강제 언마운트하면 syscall이 반환되고, 그 순간 밀려 있던 SIGKILL이 적용되며 프로세스가 죽고 포트가 풀립니다. 스토리지가 반복 원인이면 hard mount 대신 soft/intr 옵션과 헬스체크를 검토합니다. D 상태 프로세스는 kill로 없앨 수 없으니, 초점을 '왜 I/O가 안 끝나는가'로 옮겨야 합니다.


💼
실무 맥락
현업 패턴

배포 시 포트 충돌 시나리오

새 버전 배포 과정에서 포트 충돌은 흔히 발생합니다. 현업에서는 다음과 같은 패턴으로 처리합니다.

표준 대응 절차

위험 명령어

이 명령은 실행 중인 서비스 상태를 바꿔 순간적인 중단이나 설정 반영 실패를 만들 수 있습니다. 운영 트래픽 영향과 재시작 후 확인 명령을 먼저 준비하세요.

로컬 터미널
# 1. 포트 점유 상태 확인
$ sudo ss -tulnp | grep :8080

# 2. 점유 프로세스가 서비스인지 확인
$ systemctl status $(sudo ss -tulnp | grep :8080 | grep -oP '"[^"]+",pid=\K[0-9]+' | xargs -I{} ps -o unit= -p {})

# 3. 서비스면 systemctl stop, 아니면 kill
# (서비스)
$ sudo systemctl stop old-service

# (일반 프로세스)
$ sudo kill -TERM [PID]
$ sleep 5
$ sudo kill -9 [PID] 2>/dev/null || true

# 4. 포트 해제 확인
$ sudo ss -tulnp | grep :8080 || echo "포트 해제됨"

# 5. 새 서비스 시작
$ sudo systemctl start new-service

무중단 배포와의 관계

포트 충돌 없이 서비스를 교체하려면:

방법 1: Blue-Green 배포
  - 기존: 8080 포트에서 실행
  - 새 버전: 8081 포트에서 먼저 시작
  - 로드밸런서에서 트래픽을 8081로 전환
  - 기존 8080 서비스 중지

방법 2: systemd socket activation
  - systemd가 소켓을 관리
  - 서비스 재시작 시 소켓은 유지
  - 포트 충돌 없이 교체 가능

포트 현황 모니터링

로컬 터미널
# 시스템 전체 포트 현황 스냅샷
$ sudo ss -tulnp | awk 'NR>1 {print $5, $7}' | sort -k1

# 특정 사용자가 열고 있는 포트
$ sudo lsof -u www-data -i | grep LISTEN

# 포트별 프로세스 매핑 저장
$ sudo ss -tulnp > /var/log/port-snapshot-$(date +%Y%m%d-%H%M%S).txt

알아두어야 할 포트 범위

범위이름설명
1-1023Well-knownroot 권한 필요 (HTTP:80, HTTPS:443)
1024-49151Registered일반 사용자 가능 (8080, 3000 등)
49152-65535Dynamic/EphemeralOS가 클라이언트 연결에 자동 할당

: 애플리케이션은 가능하면 1024 이상 포트를 사용하세요. root 권한 없이 실행 가능하고, 보안상으로도 더 안전합니다.


핵심 명령어 치트시트

위험 명령어

이 명령은 실행 중인 서비스 상태를 바꿔 순간적인 중단이나 설정 반영 실패를 만들 수 있습니다. 운영 트래픽 영향과 재시작 후 확인 명령을 먼저 준비하세요.

로컬 터미널
# ===== 포트 점유 확인 =====
sudo ss -tulnp | grep :포트번호          # 권장
sudo lsof -i :포트번호                   # 상세 정보
fuser 포트번호/tcp                       # PID만 빠르게

# ===== 프로세스 종료 =====
kill [PID]                              # SIGTERM (정상 종료)
kill -9 [PID]                           # SIGKILL (강제 종료)
sudo fuser -k 포트번호/tcp               # 포트로 직접 종료

# ===== systemd 서비스 =====
sudo systemctl stop 서비스명             # 서비스 중지
sudo systemctl disable 서비스명          # 자동 시작 해제

# ===== Docker =====
docker ps | grep 포트번호                # 어떤 컨테이너인지 확인
docker stop 컨테이너ID                   # 컨테이너 중지

정리

포트 충돌 해결의 핵심은 무엇이 포트를 점유하고 있는지 정확히 파악한 뒤, 그 프로세스의 관리 방식에 맞게 종료하는 것입니다. 단순히 kill -9를 남발하면 systemd나 supervisor에 의해 즉시 재시작될 수 있습니다. 서비스 관리 도구를 통해 올바르게 중지하는 습관을 들이세요.


관련 모듈로 더 깊이:

다음 모듈에서는 traceroutemtr로 패킷이 목적지까지 거치는 경로와 구간별 지연을 시각화하는 방법을 다룹니다.

지식 확인

퀴즈 — 8문제

Q1

nginx 배포 중 `address already in use :80` 에러가 납니다. 기존 nginx는 `systemctl stop nginx`로 멈췄다고 생각했는데도 발생합니다. 어떻게 확인해야 합니까?

Q2

systemd에서 `Restart=always`로 설정된 서비스를 `kill -9`로 종료하면 어떻게 되는가?

Q3

`fuser -k 80/tcp` 명령어의 동작 방식은?

Q4

Docker 컨테이너가 80번 포트를 점유한 경우 올바른 해결 방법은?

Q5

`ss -tulnp | grep :8080` 출력에서 확인할 수 있는 정보가 아닌 것은?

Q6

특정 포트를 점유한 프로세스를 찾을 때 ss -tulnp, lsof -i, fuser는 각각 어떤 도움을 주나?

Q7

[심화] SIGKILL(kill -9)을 보냈는데도 프로세스가 종료되지 않고, 재시작도 아닌데 계속 살아 있습니다. PID도 그대로입니다. 가장 가능성 높은 원인은?

Q8

[심화] 8080을 점유한 프로세스를 kill -9로 여러 번 종료했는데 매번 같은 PID로 남아 있습니다. 원인을 가장 잘 가려낼 진단은?

0 / 8 답변

🧪 실습으로 확인하기

포트는 열렸다는데 왜 안 되지? — ss/netstat/telnet으로 TCP 진단

초급

"포트 8080 열었는데요?"와 "왜 안 돼요?" 사이의 간극을 메우는 실습. ss로 바인딩 상태를 확인하고, telnet/nc으로 원격 연결을 테스트하고, iptables 방화벽을 진단하고, 바인딩 주소(0.0.0.0 vs 127.0.0.1)까지 수정하는 4단계 TCP 포트 진단 플로우를 완성한다.

35📋 4단계💻 직접 환경
실습 시작하기 →

이것도 배워보세요