infra
Platform

모듈 맵

[Network] netstat과 ss 명령어로 커넥션 상태(ESTABLISHED 등) 분석

0 / 35 완료

펼치기
0 / 35 완료0%

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

[Network] netstat과 ss 명령어로 커넥션 상태(ESTABLISHED 등) 분석

LISTEN/ESTABLISHED/TIME_WAIT 소켓 상태를 이해하고 서버 부하를 점검합니다

🚨INCIDENT ALERT
HIGH

장애 시간에 서버 CPU는 낮은데 연결 수가 폭증했습니다. LISTEN, ESTABLISHED, TIME_WAIT 상태를 구분하지 못해 서비스가 정말 바쁜지 커넥션이 쌓였는지 판단이 늦어졌습니다.

소켓 상태는 서버의 현재 통신 장부입니다. ss 출력만 읽어도 병목의 방향이 보입니다.

소켓 상태와 연결 모니터링 (netstat / ss)

서버가 느려졌다는 신고를 받았을 때, 혹은 포트가 열려있는데 접속이 안 된다는 문의가 왔을 때 가장 먼저 확인해야 할 것은 소켓 상태입니다. 이 챕터에서는 TCP 소켓의 생명주기와 4가지 핵심 상태를 이해하고, netstat와 ss 명령으로 서버의 연결 현황을 점검하는 방법을 배웁니다.


이번 챕터에서 배울 것
  • 1TCP 소켓 생명주기인 3-Way Handshake(연결 수립)와 4-Way Handshake(연결 종료)를 설명할 수 있다
  • 2LISTEN, ESTABLISHED, TIME_WAIT, CLOSE_WAIT 등 핵심 소켓 상태의 의미와 발생 원인을 이해할 수 있다
  • 3ss 명령 핵심 옵션(-antp, -s, -o)을 활용하고 netstat과 비교할 수 있다
  • 4ESTABLISHED 수로 동시 접속자를 파악하고 서버 부하를 점검할 수 있다
  • 5CLOSE_WAIT 소켓 누수를 탐지하고 코드 버그를 진단할 수 있다
  • 6TIME_WAIT 과다 시 커널 파라미터(tcp_tw_reuse, tcp_fin_timeout)를 튜닝할 수 있다
실습 환경 준비
ss 명령 설치 확인 (iproute2 패키지 포함)
ss --version
netstat 설치 확인 (net-tools 패키지)
netstat --version || sudo apt-get install -y net-tools
실습용 포트 리스닝 서버 실행 (Python 간이 HTTP 서버)
python3 -m http.server 8080 &
실습 후 리스닝 프로세스 종료
kill $(lsof -ti:8080)
💡개념

TCP 소켓의 생명주기와 4가지 핵심 상태

포트 8080을 사용하는 애플리케이션을 재시작했는데 "Address already in use" 오류가 납니다. 프로세스를 종료했는데도 왜 포트가 잡혀있는지 이해가 안 됩니다. ss -tan으로 확인하면 TIME_WAIT 상태 소켓이 수십 개 쌓여 있습니다. 소켓 생명주기를 모르면 이 상태가 정상인지 문제인지, 얼마나 기다리면 되는지 알 수 없습니다.

TCP 소켓의 생명주기와 핵심 상태 — LISTEN(연결 대기)·ESTABLISHED(연결됨)·TIME_WAIT(능동 종료 후 잔류)·CLOSE_WAIT(상대 종료 대기) 등 소켓이 거치는 상태 전이. CLOSE_WAIT가 쌓이면 앱이 close()를 안 한 것, TIME_WAIT 과다는 단명 연결이 많은 것확대

TCP 연결 수립 — 3-Way Handshake

소켓 상태를 이해하려면 TCP 연결 수립과 종료 과정을 알아야 합니다.

TCP 연결 수립과 소켓 상태 — 3-way handshake 동안 SYN_RECEIVED를 거쳐 양측이 ESTABLISHED가 됨확대

TCP 연결 종료 — 4-Way Handshake

TCP 연결 종료와 소켓 상태 — CLOSE_WAIT(서버 close 대기)·TIME_WAIT(2×MSL 대기) 전이확대

4가지 핵심 소켓 상태

LISTEN

서버가 특정 포트에 바인딩되어 클라이언트의 연결 요청을 기다리는 상태입니다.

서버 소켓이 LISTEN 상태가 되기까지 — 서버 프로세스 → bind(포트) → listen() → LISTEN확대

예: Nginx가 80 포트를 LISTEN → 클라이언트 HTTP 요청 대기 중

ESTABLISHED

3-way handshake 완료 후 양방향 데이터 전송이 가능한 상태입니다. 현재 활성 연결의 수가 ESTABLISHED 소켓 수입니다.

ESTABLISHED 소켓 수 = 현재 서버에 연결된 클라이언트(동시 접속자) 수

TIME_WAIT

TCP 연결 종료 후 일정 시간(기본 60초, 커널 파라미터로 조정) 동안 유지되는 상태입니다.

TIME_WAIT가 필요한 이유:

  1. 마지막 ACK가 유실됐을 때 재전송할 수 있도록 대기
  2. 네트워크에 떠돌아다니는 지연 패킷이 새 연결에 영향을 주지 않도록 방지

TIME_WAIT 소켓 고갈 문제: 대용량 요청을 처리하는 서버에서 TIME_WAIT 소켓이 과도하게 쌓이면 사용 가능한 포트(로컬 포트 범위 1024~65535)가 고갈되어 새 연결을 맺지 못합니다.

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

# 로컬 포트 범위 확인
cat /proc/sys/net/ipv4/ip_local_port_range
# 32768   60999  (약 28,000개)
🔍실행 후 확인할 것
  • 읽기 순서 A — ss -s 요약 먼저`ss -s`로 전체 소켓 상태 분포를 먼저 봅니다. timewait 수치가 전체 수의 80% 이상이면 단기 연결 폭증 상황입니다.
  • 읽기 순서 B — ESTABLISHED 수 확인현재 활성 연결(동시 접속자) 수를 확인합니다. 베이스라인 대비 갑자기 2배 이상 증가했다면 트래픽 폭증 또는 연결 누수입니다.
  • 읽기 순서 C — TIME_WAIT와 CLOSE_WAIT 집중TIME_WAIT 수천 개는 단기 연결이 많은 것(keep-alive 미사용)이고, CLOSE_WAIT 10개 이상 지속은 앱이 소켓을 닫지 않는 버그(메모리 누수 징후)입니다.
  • ESTABLISHED 판단 기준서비스 규모마다 다르지만 베이스라인 대비 갑작스러운 2배 이상 증가 → 부하 급증. 증가 없이 CPU만 높으면 처리 지연(slow query, blocking I/O)입니다.
  • TIME_WAIT 판단 기준수천 개(~5,000) → 단기 연결 빈번, keep-alive 설정 권장. 로컬 포트 범위(약 28,000개)의 70% 초과 → 포트 고갈 위험, 즉시 tcp_tw_reuse 설정.
  • CLOSE_WAIT 판단 기준10개 이상 지속 → 앱 레벨 소켓 누수 의심. 시간이 지나도 줄지 않으면 애플리케이션이 close()를 호출하지 않는 코드 버그입니다. 재시작은 임시 방편.
  • 조합 해석 — TIME_WAIT 많음 + ESTABLISHED 적음연결이 빠르게 열고 닫히는 패턴. HTTP/1.1 keep-alive 미사용 또는 짧은 keepalive timeout이 원인입니다.
  • 조합 해석 — CLOSE_WAIT 증가 + CPU 낮음앱 레벨 소켓 누수. CPU나 네트워크 문제가 아닌 코드 버그입니다. 코드의 finally 블록이나 커넥션 풀 반납 로직을 점검하세요.
  • 조합 해석 — SYN_RECV 다수SYN flood 공격 의심 또는 서버 처리 능력 초과. `ss -antp | grep SYN-RECV | wc -l`로 수량 확인 후 백로그 크기와 비교합니다.

CLOSE_WAIT

상대방이 FIN을 보냈고 내가 ACK로 응답했지만, 내 애플리케이션이 아직 소켓을 close()하지 않은 상태입니다.

CLOSE_WAIT가 누적되는 원인:

  • 애플리케이션 코드에서 소켓을 close()하지 않는 버그
  • 커넥션 풀(connection pool)에서 반납되지 않는 연결
  • 예외 발생 시 finally 블록에서 소켓 정리를 누락

CLOSE_WAIT는 커널이 자동으로 처리하지 않습니다. 오직 애플리케이션이 close()를 호출해야만 LAST_ACK → CLOSED로 전환됩니다.

전체 상태 전이도

TCP 연결 상태 전이도 — CLOSED→LISTEN→ESTABLISHED 수립 경로와 FIN_WAIT/TIME_WAIT 능동 종료·CLOSE_WAIT/LAST_ACK 수동 종료 경로확대


💡개념

ss와 netstat 명령 비교 및 TIME_WAIT 소켓 관리

장애 상황에서 "지금 이 서버에 열린 연결이 몇 개야?" 를 빠르게 확인해야 합니다. 구형 서버에는 netstat밖에 없고 최신 서버엔 ss만 있습니다. 두 명령어의 출력 형식이 달라서 같은 정보를 보는데 명령어마다 다른 옵션을 외워야 합니다. 그리고 TIME_WAIT 소켓이 수천 개 쌓이면 새 연결이 안 생기는 문제가 발생하는데, 이걸 관리하는 방법을 모르면 재부팅 외에 해결책이 없습니다.

ss와 netstat 비교 및 TIME_WAIT 관리 — ss는 커널에 직접 접근해 빠르고 iproute2에 기본 포함, netstat는 /proc 파싱으로 느리고 net-tools 별도 설치 필요. ss -tlnp(TCP 리스닝)·ss -tn sport = :80(포트 필터). TIME_WAIT 고갈은 tcp_tw_reuse·fin_timeout·local_port_range 커널 파라미터로 대응확대

ss vs netstat

항목ssnetstat
속도빠름 (커널 직접 조회)느림 (/proc 파일 파싱)
패키지iproute2 (기본 설치)net-tools (별도 설치 필요)
권장RHEL 7+ 기본 도구레거시
대용량수만 개 소켓도 빠름소켓 많으면 느려짐

현대 Linux 배포판에서는 ss를 기본으로 사용하고, 익숙함을 위해 netstat 옵션도 알아두는 것이 좋습니다.

ss 주요 옵션

로컬 터미널
# 기본 — TCP 소켓 전체 (숫자, 프로세스 포함)
ss -antp

# 플래그 설명
# -a: 모든 상태 (LISTEN 포함)
# -n: 숫자로 표시 (DNS/포트 이름 변환 안 함)
# -t: TCP만
# -p: 프로세스 정보 (PID, 프로세스명)
# -u: UDP만
# -l: LISTEN 상태만

# LISTEN 포트만 확인
ss -tlnp

# UDP 소켓 확인
ss -ulnp

# 요약 통계
ss -s

netstat 주요 옵션

로컬 터미널
# 리스닝 포트 + UDP + 프로세스
netstat -tulnp

# 플래그 설명
# -t: TCP
# -u: UDP
# -l: LISTEN 상태만
# -n: 숫자로 표시
# -p: 프로세스 정보

# 전체 소켓 (ESTABLISHED 포함)
netstat -antp

TIME_WAIT 소켓 고갈 대응 — 커널 파라미터

로컬 터미널
# 현재 TIME_WAIT 소켓 수 확인
ss -antp | grep TIME-WAIT | wc -l

# 또는 ss -s 요약에서 확인
ss -s
# Total: 1245
# TCP:   1189 (estab 45, closed 1100, orphaned 0, timewait 1100)

TIME_WAIT 소켓 고갈 방지를 위한 커널 파라미터:

로컬 터미널
# 현재 설정 확인
sysctl net.ipv4.tcp_tw_reuse
sysctl net.ipv4.tcp_fin_timeout

# net.ipv4.tcp_tw_reuse: TIME_WAIT 소켓을 새 연결에 재사용 허용
# 1로 설정하면 TIME_WAIT 상태의 포트를 outbound 연결에 재사용
sudo sysctl -w net.ipv4.tcp_tw_reuse=1

# net.ipv4.tcp_fin_timeout: FIN_WAIT_2 타임아웃(초), 기본 60
sudo sysctl -w net.ipv4.tcp_fin_timeout=30

# 영구 적용 (/etc/sysctl.conf 또는 /etc/sysctl.d/)
echo "net.ipv4.tcp_tw_reuse = 1" | sudo tee -a /etc/sysctl.d/99-tcp-tuning.conf
echo "net.ipv4.tcp_fin_timeout = 30" | sudo tee -a /etc/sysctl.d/99-tcp-tuning.conf
sudo sysctl -p /etc/sysctl.d/99-tcp-tuning.conf

참고: net.ipv4.tcp_tw_recycle은 커널 4.12에서 제거됐습니다. 사용하지 마세요.

💡개념

ss 한 줄이 소켓 상태를 보여주기까지 — 커널 장부에서 진단까지 6단계

ss -antpESTABLISHED·TIME_WAIT·CLOSE_WAIT를 표시할 수 있는 건, 그 상태를 ss가 계산하는 게 아니라 커널이 이미 관리하고 있는 것을 읽어 오기 때문입니다. 커널은 열린 모든 연결을 소켓 테이블이라는 장부에 적어 두고 연결마다 지금 어느 상태인지를 실시간으로 갱신합니다. 이 경로 — 커널이 상태를 기록하고, 도구가 읽고, 우리가 원인으로 매핑하는 — 를 알면 "이 상태가 왜 보이는지"와 "이 상태가 쌓이면 누구 잘못인지"를 단계로 짚을 수 있습니다.

TEXT
[앱]  socket() → listen() 또는 connect()
   │
   ① 커널이 연결마다 소켓 구조체(TCB) 생성 → 상태 필드에 LISTEN·SYN-SENT 기록
   │
   ② 패킷 이벤트마다 TCP 상태머신을 전이        (SYN·ACK·FIN 도착 → 상태 갱신)
   │      ESTABLISHED → FIN_WAIT → TIME_WAIT   /   ESTABLISHED → CLOSE_WAIT → ...
   │
   ③ 갱신된 상태·큐 길이가 커널 소켓 테이블에 상주   (sock_diag · /proc/net/tcp)
   │
   ④ ss는 sock_diag 넷링크로, netstat는 /proc 파싱으로 이 테이블을 읽음
   │
   ⑤ State · Recv-Q · Send-Q · 프로세스 열로 화면에 표시
   │
   ⑥ 상태 분포를 원인으로 매핑 → 진단
   ▼
[운영자]  "CLOSE_WAIT 30개 = 앱이 close()를 안 함"

각 단계가 하는 일과, 그 단계가 어긋날 때의 신호:

단계하는 일여기서 어긋나면
① 소켓 생성앱이 소켓을 열면 커널이 연결마다 구조체를 만들어 상태를 기록. LISTEN 소켓이 있어야 그 포트로 온 연결을 받는다LISTEN 소켓이 없으면 그 포트로 온 SYN에 커널이 RST로 응답 → 클라이언트는 Connection refused
② 상태 전이패킷이 오갈 때마다 상태머신을 전이시킨다. 정상 종료는 FIN 교환으로 TIME_WAIT까지 진행앱이 close()를 호출 안 하면 CLOSE_WAIT에서 전이가 멈춰 소켓이 잔류(누수)
③ 테이블 상주상태와 함께 Recv-Q·Send-Q(LISTEN이면 accept 큐 깊이·backlog)가 테이블에 남는다LISTEN 소켓의 Recv-Q가 Send-Q(backlog)까지 차면 커널이 완성 연결을 드랍
④ 도구가 읽기ss는 sock_diag 넷링크로 커널에 직접 질의, netstat는 /proc/net/tcp를 한 줄씩 파싱소켓이 수만 개면 /proc 파싱(netstat)이 느려짐 — 대규모는 ss가 수십 배 빠름
⑤ 화면 표시State·주소·큐·소유 프로세스(PID)를 열로 출력-n을 빼면 포트를 서비스명으로 바꿔 표시해 grep 필터가 어긋남
⑥ 원인 매핑상태별 개수의 이상 패턴을 원인으로 해석CLOSE_WAIT 누적 = 내 앱이 close 안 함 · TIME_WAIT 과다 = 단명 연결 폭주 · Recv-Q 상주 = 앱이 accept·read를 못 따라감

즉 ss가 보여주는 상태는 커널이 실시간으로 갱신하는 장부의 스냅샷입니다. 그래서 상태 하나하나가 곧 진단 단서가 됩니다 — CLOSE_WAIT은 커널이 못 지우는 게 아니라 내 앱이 마지막 close()를 호출해야 사라지므로 '내 코드' 문제를, TIME_WAIT 과다는 연결을 짧게 맺고 끊는 '연결 패턴' 문제를, LISTEN 소켓의 Recv-Q 상주는 accept·read가 밀린 '처리 속도' 문제를 가리킵니다. 상태를 읽는다는 것은 결국 커널의 장부를 읽어 원인 계층을 좁히는 일입니다.

소켓 상태 수치 판단 — 몇 개부터, 무엇을 의미하나
TIME_WAIT가 수천~수만 개그 자체로는 대개 정상 — 연결을 많이 맺고 끊는 서버(프록시·LB)의 자연스러운 결과. ip_local_port_range·net.ipv4.tcp_max_tw_buckets 한계에 근접(예: 28000 포트 중 대부분 소진)할 때만 위험. '개수보다 포트 고갈 여부'
CLOSE_WAIT가 쌓이고 안 줄어든다거의 항상 애플리케이션 버그 — 상대가 끊었는데 내 앱이 close()를 안 함. 커널 튜닝으로 안 풀린다. 수십 개↑ 지속이면 코드(커넥션 누수)·커넥션풀 점검. 'CLOSE_WAIT=내 앱 잘못'
SYN_RECV가 많다SYN 플러드 의심 또는 백로그 부족. net.core.somaxconn·tcp_max_syn_backlog 확인, syncookies 활성. 정상 트래픽이면 백로그를 올린다
ESTABLISHED가 예상보다 많다유휴 커넥션이 안 닫히는 것 — keepalive·커넥션풀 max 설정 점검. 'Recv-Q/Send-Q'가 계속 쌓이면 앱이 처리를 못 따라가는 신호
접속자/연결 수를 세고 싶다ss -s로 요약, ss state established '( dport = :443 )' | wc -l 로 특정 포트 집계. netstat보다 ss가 수만 소켓에서 수십 배 빠름 — 대규모는 ss
포트 고갈로 connect 실패ephemeral 포트 소진. ip_local_port_range 확장(32768-60999→ 더 넓게) + tcp_tw_reuse=1(클라이언트측 안전). tw_recycle은 제거됨, 쓰지 말 것

ss와 netstat로 소켓 상태 현황 점검

기본 소켓 상태 확인

로컬 터미널
# 1. LISTEN 중인 TCP 포트 확인 (어떤 서비스가 어느 포트를 점유했나)
ss -tlnp

# 출력 예시
# State    Recv-Q  Send-Q  Local Address:Port  Peer Address:Port  Process
# LISTEN   0       128     0.0.0.0:22          0.0.0.0:*          users:(("sshd",pid=1234,fd=3))
# LISTEN   0       511     0.0.0.0:80          0.0.0.0:*          users:(("nginx",pid=5678,fd=6))
# LISTEN   0       128     127.0.0.1:3306      0.0.0.0:*          users:(("mysqld",pid=9012,fd=23))
# LISTEN   0       511     [::]:443            [::]:*             users:(("nginx",pid=5678,fd=7))

# 확인 포인트
# - 0.0.0.0: 모든 인터페이스에서 수신
# - 127.0.0.1: 로컬 루프백만 (외부에서 접근 불가)
# - [::]: IPv6 와일드카드
# - Recv-Q: 수신 큐에 쌓인 바이트 수 (큰 값이면 처리 지연)

전체 소켓 상태 현황 파악

로컬 터미널
# 모든 TCP 소켓 목록
ss -antp

# 상태별 소켓 수 집계
ss -antp | awk 'NR>1 {print $1}' | sort | uniq -c | sort -rn

# 출력 예시
#    1100 TIME-WAIT
#      45 ESTAB
#      10 LISTEN
#       3 CLOSE-WAIT
#       1 FIN-WAIT-1

동시 접속자 수 확인

로컬 터미널
# ESTABLISHED 소켓 수 = 현재 활성 연결 수
ss -antp | grep ESTAB | wc -l

# 특정 포트의 연결 수 (예: HTTP 80포트)
ss -antp | grep ':80' | grep ESTAB | wc -l

# 연결 중인 원격 IP별 집계 (어떤 IP에서 많이 붙어있나)
ss -antp | grep ESTAB | awk '{print $5}' | cut -d: -f1 | sort | uniq -c | sort -rn | head -20

# netstat으로 동일하게 확인
netstat -antp | grep ESTABLISHED | wc -l

실시간 모니터링

로컬 터미널
# 1초마다 소켓 현황 갱신
watch -n 1 'ss -s'

# ESTABLISHED와 TIME_WAIT만 실시간 추적
watch -n 2 'echo "ESTABLISHED: $(ss -antp | grep ESTAB | wc -l)  TIME_WAIT: $(ss -antp | grep TIME-WAIT | wc -l)"'

# 특정 프로세스의 소켓만 확인 (예: nginx PID 5678)
ss -antp | grep 'pid=5678'

TIME_WAIT 고갈 상황 점검과 커널 파라미터 튜닝

TIME_WAIT 상황 진단

로컬 터미널
# 1. TIME_WAIT 소켓 수 확인
ss -s
# TCP:   2100 (estab 50, closed 2010, orphaned 0, timewait 2010)
# timewait 값이 수천~수만이면 고갈 위험

# 2. 로컬 포트 가용 범위 확인
cat /proc/sys/net/ipv4/ip_local_port_range
# 32768   60999
# 최대 사용 가능한 포트: 60999 - 32768 = 28,231개

# 3. 현재 사용 중인 포트 수
ss -antp | wc -l

# 4. 에러 카운터 확인 (포트 고갈 발생 시 증가)
netstat -s | grep -i "failed\|overflow\|reuse"

커널 파라미터 단계별 튜닝

위험 명령어

이 명령은 시스템 설정 파일을 변경합니다. 기존 파일 백업과 적용 후 검증 방법을 준비하지 않으면 재부팅이나 서비스 재시작 때 장애가 반복될 수 있습니다.

로컬 터미널
# 현재 설정 전체 확인
sysctl -a | grep tcp | grep -E "tw_reuse|fin_timeout|port_range|max_syn"

# --- 단계 1: FIN_WAIT_2 타임아웃 단축 ---
# 기본 60초 → 30초로 단축
sudo sysctl -w net.ipv4.tcp_fin_timeout=30
echo "net.ipv4.tcp_fin_timeout = 30" | sudo tee /etc/sysctl.d/99-tcp.conf

# --- 단계 2: TIME_WAIT 소켓 재사용 허용 ---
# Outbound 연결에서 TIME_WAIT 소켓 재사용 (클라이언트 역할 서버에 적용)
sudo sysctl -w net.ipv4.tcp_tw_reuse=1
echo "net.ipv4.tcp_tw_reuse = 1" | sudo tee -a /etc/sysctl.d/99-tcp.conf

# --- 단계 3: 로컬 포트 범위 확장 ---
# 기본 32768-60999 → 1024-65535로 확장 (약 64,511개)
sudo sysctl -w net.ipv4.ip_local_port_range="1024 65535"
echo "net.ipv4.ip_local_port_range = 1024 65535" | sudo tee -a /etc/sysctl.d/99-tcp.conf

# 설정 적용
sudo sysctl -p /etc/sysctl.d/99-tcp.conf

# 적용 확인
sysctl net.ipv4.tcp_tw_reuse net.ipv4.tcp_fin_timeout net.ipv4.ip_local_port_range

효과 검증

로컬 터미널
# 튜닝 전후 비교를 위해 현재 TIME_WAIT 수 기록
BEFORE=$(ss -antp | grep TIME-WAIT | wc -l)
echo "튜닝 전 TIME_WAIT: $BEFORE"

# ... 파라미터 설정 ...

# 60초 후 다시 확인
sleep 60
AFTER=$(ss -antp | grep TIME-WAIT | wc -l)
echo "튜닝 후 TIME_WAIT: $AFTER"

CLOSE_WAIT 소켓 누수 탐지와 원인 분석

CLOSE_WAIT 소켓 존재 확인

로컬 터미널
# CLOSE_WAIT 소켓 확인
ss -antp | grep CLOSE-WAIT

# 출력 예시
# CLOSE-WAIT  1   0   10.0.0.1:8080   192.168.1.100:54321  users:(("java",pid=12345,fd=42))
# CLOSE-WAIT  1   0   10.0.0.1:8080   192.168.1.101:54322  users:(("java",pid=12345,fd=43))
# CLOSE-WAIT  1   0   10.0.0.1:8080   192.168.1.102:54323  users:(("java",pid=12345,fd=44))

# 개수 집계
CLOSE_WAIT_COUNT=$(ss -antp | grep CLOSE-WAIT | wc -l)
echo "CLOSE_WAIT 소켓 수: $CLOSE_WAIT_COUNT"

# 10개 이상이면 누수 의심
if [ "$CLOSE_WAIT_COUNT" -gt 10 ]; then
  echo "경고: CLOSE_WAIT 소켓 누수 의심!"
fi

누수 프로세스 특정

로컬 터미널
# CLOSE_WAIT 소켓을 가진 프로세스 확인
ss -antp | grep CLOSE-WAIT | grep -oP 'pid=\K[0-9]+' | sort | uniq -c | sort -rn

# 해당 PID의 프로세스 정보 확인
ps aux | grep 12345

# 해당 프로세스의 열린 파일/소켓 수 확인
ls /proc/12345/fd | wc -l
# 수천 개면 소켓 누수 확인

# 어떤 소켓들인지 확인
ls -la /proc/12345/fd | grep socket

애플리케이션 레벨 확인

Java Spring Boot 애플리케이션의 커넥션 풀 상태 확인 예시:

로컬 또는 서버
# Actuator가 활성화된 경우 커넥션 풀 통계 조회
curl -s http://localhost:8080/actuator/metrics/hikaricp.connections.active
curl -s http://localhost:8080/actuator/metrics/hikaricp.connections.idle

# JVM 힙 덤프 → 소켓 객체 분석 (심층 분석 시)
jmap -dump:format=b,file=/tmp/heap.hprof 12345

임시 해결책 (서비스 재시작 없이)

위험 명령어

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

서버 터미널
# CLOSE_WAIT 소켓을 강제로 닫는 방법은 없음 (커널 설계상)
# 유일한 해결책: 애플리케이션이 close()를 호출하도록 수정

# 임시: 해당 프로세스 재시작 (소켓 모두 정리됨)
systemctl restart myapp

# 또는 graceful reload가 지원되면
kill -HUP 12345

증상

로컬 터미널
# 서버에서 확인: 80 포트 LISTEN 중
ss -tlnp | grep :80
# LISTEN   0   511   127.0.0.1:80   0.0.0.0:*   users:(("nginx",pid=5678))

# 외부에서 접속 시 실패
curl http://서버IP/
# curl: (7) Failed to connect to 서버IP port 80: Connection refused

원인 분석

원인: 127.0.0.1에만 바인딩됨

출력에서 Local Address127.0.0.1:80이므로 루프백 인터페이스에만 바인딩되어 있습니다. 외부 IP에서는 접근할 수 없습니다.

로컬 터미널
# 어떤 IP에 바인딩되어 있는지 정확히 확인
ss -tlnp | awk '{print $4}' | grep -v "Local"
# 127.0.0.1:80    ← 외부 접근 불가
# 0.0.0.0:80      ← 모든 인터페이스에서 접근 가능
# [::]:80         ← IPv6 와일드카드 (IPv4 포함되기도 함)

해결 방법

Nginx 설정 수정 — worker_connections와 keepalive 값을 조정합니다.

Nginx
# /etc/nginx/nginx.conf 또는 /etc/nginx/conf.d/default.conf
server {
    # 변경 전
    listen 127.0.0.1:80;

    # 변경 후 — 모든 인터페이스
    listen 0.0.0.0:80;
    # 또는 그냥
    listen 80;
}
위험 명령어

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

로컬 터미널
# 설정 검증 후 리로드
nginx -t && systemctl reload nginx

# 변경 확인
ss -tlnp | grep :80
# LISTEN   0   511   0.0.0.0:80   0.0.0.0:*   users:(("nginx",pid=5678))

방화벽도 함께 확인 — 포트가 열려 있어도 방화벽이 막으면 LISTEN 상태가 의미 없습니다.

위험 명령어

이 명령은 방화벽 정책을 변경해 현재 접속 중인 세션이나 운영 트래픽에 즉시 영향을 줄 수 있습니다. 적용 전 허용 대상 IP·포트와 롤백 명령을 확인하세요.

로컬 터미널
# firewalld
firewall-cmd --list-all | grep 80

# iptables
iptables -L INPUT -n | grep 80

# 방화벽이 차단 중이면 허용
firewall-cmd --permanent --add-port=80/tcp
firewall-cmd --reload

증상

로컬 터미널
# 서버 로그
[ERROR] bind: address already in use
[ERROR] connect: Cannot assign requested address

# ss로 확인
ss -s
# TCP:   32000 (estab 50, closed 31900, orphaned 0, timewait 31900)
# timewait이 3만 개를 넘어섬

클라이언트 역할을 하는 서버(예: 내부 API 서버가 DB나 외부 API를 반복 호출)에서 자주 발생합니다.

원인

로컬 터미널
# 로컬 포트 가용 범위 확인
cat /proc/sys/net/ipv4/ip_local_port_range
# 32768   60999  → 최대 28,231개

# TIME_WAIT 소켓이 28,231개를 넘으면 포트 고갈
ss -antp | grep TIME-WAIT | wc -l
# 28500   ← 초과!

연결을 짧게 끊고 새로 맺는 패턴(HTTP keep-alive 미사용, 커넥션 풀 없음)에서 TIME_WAIT가 누적됩니다.

해결 방법

단기 해결 — 커널 파라미터 튜닝 — TIME_WAIT 소켓이 쌓이는 것을 줄이는 즉각적 조치입니다.

위험 명령어

이 명령은 시스템 설정 파일을 변경합니다. 기존 파일 백업과 적용 후 검증 방법을 준비하지 않으면 재부팅이나 서비스 재시작 때 장애가 반복될 수 있습니다.

로컬 터미널
# TIME_WAIT 소켓 재사용
sudo sysctl -w net.ipv4.tcp_tw_reuse=1

# FIN_WAIT_2 타임아웃 단축
sudo sysctl -w net.ipv4.tcp_fin_timeout=30

# 포트 범위 확장
sudo sysctl -w net.ipv4.ip_local_port_range="10000 65535"

# 영구 저장
cat << 'EOF' | sudo tee /etc/sysctl.d/99-tcp-tuning.conf
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 30
net.ipv4.ip_local_port_range = 10000 65535
EOF
sudo sysctl -p /etc/sysctl.d/99-tcp-tuning.conf

근본적 해결 — 애플리케이션 개선 — Connection Pool을 쓰면 소켓 생성 빈도 자체가 줄어듭니다.

로컬 또는 서버
# HTTP 클라이언트에서 Keep-Alive 활성화 (연결 재사용)
# curl의 경우
curl --keepalive-time 60 http://internal-api/endpoint

# 애플리케이션 레벨에서 Connection Pool 사용:
# Python requests.Session, Java HikariCP, Node.js http.Agent(keepAlive:true)

모니터링 알림 설정

로컬 터미널
#!/bin/bash
# /usr/local/bin/check-timewait.sh

THRESHOLD=20000
COUNT=$(ss -antp | grep TIME-WAIT | wc -l)

if [ "$COUNT" -gt "$THRESHOLD" ]; then
  echo "TIME_WAIT 경고: 현재 ${COUNT}개 (임계값: ${THRESHOLD})" \
    | mail -s "[ALERT] TIME_WAIT 고갈 위험" admin@example.com
fi
로컬 터미널
# crontab -e
*/5 * * * * /usr/local/bin/check-timewait.sh

심화 — LISTEN 소켓의 Recv-Q/Send-Q가 말해주는 것

💡개념

심화: accept 큐 넘침 — LISTEN인데도 접속이 튕기는 이유

'LISTEN이면 접속 OK'는 절반만 맞습니다. LISTEN 소켓 뒤에는 커널이 관리하는 두 개의 큐가 있고, 부하가 몰리면 이 큐가 넘쳐 이미 완성된 연결조차 버려집니다. ss -tlnp가 정상으로 보여도 접속이 간헐적으로 튕기는 전형적 원인입니다.

  • LISTEN 소켓에는 두 개의 큐가 있습니다: SYN 큐(반연결, SYN_RECV)는 SYN을 받고 SYN-ACK을 보낸 뒤 상대의 ACK를 기다리는 half-open 연결을 담습니다. accept 큐(완성 큐)는 3-way 핸드셰이크가 끝나 애플리케이션이 accept()로 가져가기만 기다리는 완성된 연결을 담습니다.
  • ss -ltn의 두 열이 바로 accept 큐입니다: LISTEN 소켓에서 Recv-Q는 '현재 accept 큐에 쌓여(아직 accept()되지 않은) 대기 중인 완성 연결 수', Send-Q는 'accept 큐의 최대치(backlog)'입니다. Recv-Q가 Send-Q에 닿으면 큐가 꽉 찬 것입니다. (같은 두 열이 ESTABLISHED 소켓에서는 수신·송신 버퍼 바이트를 뜻하니, LISTEN이냐 ESTABLISHED냐로 의미가 달라집니다.)
  • 큐가 넘치면 완성된 연결이 버려집니다: 커널은 accept 큐가 꽉 차면 완성된 연결을 드랍하거나 리셋합니다. 클라이언트는 타임아웃이나 connection reset을 겪지만, 서버는 멀쩡히 LISTEN 중이라 원인을 놓치기 쉽습니다. accept 루프가 느리거나 워커가 고갈되면 CPU가 남아도 이 현상이 납니다.
  • backlog는 두 곳이 함께 정합니다: 앱의 listen(fd, backlog) 인자와 커널의 net.core.somaxconn작은 값이 실제 상한입니다. somaxconn만 올리고 앱 backlog가 낮으면 소용없습니다(반대도 마찬가지). 넘침 여부는 nstat -az | grep -i ListenOverflows(또는 netstat -s | grep -i listen)의 카운터 증가로 확정합니다.

상황: 평소엔 멀쩡한데 프로모션처럼 트래픽이 튀는 순간 일부 요청이 접속 타임아웃 또는 connection reset을 겪습니다. ss -tlnp로 보면 서비스는 정상 LISTEN, CPU·메모리도 한가한 편이고 방화벽도 손댄 적이 없습니다.

원인: accept 큐(백로그) 넘침입니다. 순간적으로 몰린 연결을 애플리케이션의 accept 루프가 제때 비우지 못했거나 backlog/somaxconn이 너무 작아, 3-way 핸드셰이크까지 끝난 완성 연결이 큐에 쌓이다 넘쳐 커널이 버린 것입니다. 서비스가 '떠 있는 것'과 '연결을 제때 받아 처리하는 것'은 다릅니다.

진단: 부하 시점에 ss -ltn으로 해당 LISTEN 소켓의 Recv-Q가 Send-Q(백로그 상한)에 붙는지 관찰합니다. nstat -az | grep -i ListenOverflows(또는 ListenDrops) 카운터가 부하와 함께 증가하면 넘침 확정입니다. accept 루프가 동기 DNS·블로킹 I/O 같은 초기 처리에 붙잡혀 있지 않은지도 함께 봅니다.

해결: net.core.somaxconn과 애플리케이션의 listen() backlog를 둘 다 상향합니다(둘 중 작은 값이 상한). Nginx는 listen ... backlog=, Node는 server.listen(port, backlog), JVM 서버는 acceptCount/backlog로 조정합니다. 근본은 accept를 지연시키는 느린 초기 처리를 걷어내 큐가 빨리 비도록 하는 것이고, 절대 규모가 문제라면 워커·인스턴스를 늘립니다.


💼
실무 맥락
현업 패턴

용량 계획과 임계값 설정

운영 서버에서 소켓 상태를 정기적으로 수집하면 서버 용량 계획의 근거가 됩니다.

로컬 터미널
#!/bin/bash
# /usr/local/bin/socket-metrics.sh
# Prometheus textfile collector 형식으로 메트릭 출력

OUTPUT_FILE="/var/lib/node_exporter/textfile_collector/socket_stats.prom"

ESTAB=$(ss -antp | grep -c ESTAB)
TIMEWAIT=$(ss -antp | grep -c TIME-WAIT)
CLOSEWAIT=$(ss -antp | grep -c CLOSE-WAIT)
LISTEN=$(ss -tlnp | tail -n +2 | wc -l)

cat > "$OUTPUT_FILE" << EOF
# HELP socket_established_total Current ESTABLISHED TCP connections
# TYPE socket_established_total gauge
socket_established_total $ESTAB

# HELP socket_timewait_total Current TIME_WAIT TCP sockets
# TYPE socket_timewait_total gauge
socket_timewait_total $TIMEWAIT

# HELP socket_closewait_total Current CLOSE_WAIT TCP sockets
# TYPE socket_closewait_total gauge
socket_closewait_total $CLOSEWAIT

# HELP socket_listen_total Current LISTEN ports
# TYPE socket_listen_total gauge
socket_listen_total $LISTEN
EOF

실무 점검 체크리스트

서버 신규 오픈 전 점검:

로컬 터미널
# 1. 의도한 포트만 LISTEN 중인지 확인 (불필요한 포트 차단)
ss -tlnp
# → 모든 LISTEN 포트가 서비스 목적에 부합하는지 리뷰

# 2. 0.0.0.0 vs 127.0.0.1 바인딩 적절성 확인
# → DB(3306), Redis(6379)는 127.0.0.1에만 바인딩되어야 함

# 3. CLOSE_WAIT 소켓 없음 확인
ss -antp | grep CLOSE-WAIT
# → 배포 직후 CLOSE_WAIT가 0이어야 정상

# 4. 동시 접속자 수 베이스라인 측정
ss -antp | grep -c ESTAB

장애 대응 시나리오별 확인 명령

장애 신고확인 명령
"서버 접속이 안 돼요"`ss -tlnp
"서버가 느려요"`ss -antp
"주기적으로 연결이 끊겨요"`ss -antp
"새벽마다 장애가 나요"ss -s로 TIME_WAIT 추이 모니터링
"특정 IP에서만 접속 안 돼요"`ss -antp

자격증 및 면접 포인트

  • RHCE / LFCS: ssnetstat 명령을 이용한 포트 점검은 필수 실기 항목
  • CKA (Kubernetes 관리자): Pod 간 통신 문제 디버깅에 ss를 사용
  • 백엔드 개발 면접: TIME_WAIT 원인과 tcp_tw_reuse 튜닝, CLOSE_WAIT 코드 버그 진단 능력
  • 시스템 설계 면접: ESTABLISHED 소켓 수로 서버 용량을 추정하고 스케일아웃 시점을 결정하는 근거

핵심 명령어 정리

명령설명
ss -tlnpLISTEN 중인 TCP 포트와 프로세스
ss -antp모든 TCP 소켓 상태
ss -s소켓 요약 통계
ss -antp | grep ESTAB | wc -l현재 ESTABLISHED 소켓 수
ss -antp | grep CLOSE-WAITCLOSE_WAIT 소켓 목록
netstat -tulnpLISTEN 포트와 프로세스 (레거시)
sysctl net.ipv4.tcp_tw_reuseTIME_WAIT 재사용 설정 확인
sysctl net.ipv4.tcp_fin_timeoutFIN_WAIT_2 타임아웃 확인

정리

  • LISTEN은 연결 대기, ESTABLISHED는 활성 연결, TIME_WAIT는 종료 후 대기, CLOSE_WAIT는 앱이 close()를 아직 호출하지 않은 상태입니다.
  • ss -antp는 모든 TCP 소켓 상태를 빠르게 확인하는 현대적 도구입니다.
  • ESTABLISHED 소켓 수가 서버의 실제 동시 접속자 수입니다.
  • TIME_WAIT 소켓 폭증은 tcp_tw_reuse=1 설정과 Keep-Alive/Connection Pool 사용으로 해결합니다.
  • CLOSE_WAIT 누수는 커널이 자동 해결하지 않으므로 반드시 코드에서 소켓 close()를 보장해야 합니다.

관련 모듈로 더 깊이:

다음 모듈에서는 ARP 프로토콜 동작 원리와 IP 충돌(중복 IP) 상황을 arping으로 진단하는 방법을 다룹니다.

지식 확인

퀴즈 — 8문제

Q1

배포 직후 클라이언트에서 'Connection refused' 오류가 발생합니다. 서버에서 `ss -antp | grep 8080`을 실행했더니 아무것도 출력되지 않았습니다. 이 결과가 의미하는 것은 무엇입니까?

Q2

TIME_WAIT 상태가 발생하는 이유는 무엇입니까?

Q3

장애 상황에서 서버의 소켓 상태를 빠르게 확인하려 합니다. `ss antp` 실행 시 포트 번호 대신 `http`, `ssh` 같은 서비스 이름이 출력되어 grep으로 필터가 잘 안 됩니다. 어떤 옵션을 추가해야 합니까?

Q4

CLOSE_WAIT 상태 소켓이 대량으로 쌓이는 주된 원인은 무엇입니까?

Q5

현재 서버의 ESTABLISHED 소켓 수를 빠르게 확인하는 명령은 무엇입니까?

Q6

ss 출력에서 소켓 상태가 'LISTEN'인 것과 'ESTABLISHED'인 것은 각각 무엇을 의미하나?

Q7

[심화] ss -ltn 출력에서 LISTEN 소켓의 Recv-Q와 Send-Q 열은 각각 무엇을 뜻하나?

Q8

[심화] 트래픽이 튀는 순간에만 일부 접속이 타임아웃되는데 서비스는 정상 LISTEN이고 CPU도 여유가 있습니다. accept 큐 넘침을 확정할 진단으로 가장 적절한 것은?

0 / 8 답변

🧪 실습으로 확인하기

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

초급

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

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

이것도 배워보세요