LabHub
배우기 러닝패스 코스

ネットワークトラブルシューティング

ss出力の読み方とバインドアドレスの罠

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

한 줄 요약

ss -ltnp 한 줄이면 "듣고 있는가"와 "누구에게 듣고 있는가"가 동시에 나온다. 후자를 놓치는 것이 원격 접속 장애의 절반이다.

概念マップ: 루프백에만・connection refused・바인딩 주소・Recv-Q 와 Send-Q 는 상태에 따라 뜻이 바뀐다.

왜 이게 필요했나

가장 흔한 배포 사고 하나를 보자.

LISTEN  0  4096  127.0.0.1:8080  0.0.0.0:*  users:(("api",pid=1,fd=7))

이 프로세스는 루프백에만 듣고 있다. 같은 호스트에서 curl localhost:8080 은 성공한다. 그런데 다른 호스트나 파드 IP 로 오는 연결은 커널이 RST 로 거절한다. 즉 connection refused 가 난다.

여기서 진단이 갈린다. 파드 IP 로는 refused 인데 파드 안에서 localhost 로는 성공한다면 원인은 네트워크 정책이 아니라 바인딩 주소다. 반대로 파드 IP 로도 timeout 이라면 그때부터 NetworkPolicy 와 CNI 를 본다. 이 한 번의 갈림이 조사 범위를 절반으로 줄인다.

프레임워크별로 실수하기 쉬운 지점이 정해져 있다.

프레임워크 안전한 표기 사고 나는 표기
Express app.listen(8080) app.listen(8080, '127.0.0.1')
Go net/http ":8080" "localhost:8080"
Gunicorn --bind 0.0.0.0:8000 --bind 127.0.0.1:8000
Rails -b 0.0.0.0 기본값(로컬만)
PostgreSQL listen_addresses = '*' 기본값 localhost

어떻게 동작하나

ss 의 옵션은 손에 꼽는다.

ss -ltnp                      # 리슨 중인 TCP + 프로세스
ss -tulpn                     # TCP/UDP 리슨 전체
ss -s                         # 소켓 총계 (누수 판별)
ss -tan state established     # 확립된 연결만
ss -tanp state syn-sent       # 나갔는데 응답 없는 연결
ss -tnio                      # rto/rtt/cwnd 같은 타이머까지

옵션 뜻: -t TCP, -u UDP, -l 리슨만, -a 전부, -n 이름 해석 안 함, -p 프로세스.

Recv-Q 와 Send-Q 는 상태에 따라 뜻이 바뀐다. 이게 시험에 잘 나온다.

소켓 상태 Recv-Q Send-Q
LISTEN accept 대기 중인 완료 큐 길이 백로그 최대치
ESTABLISHED 아직 읽히지 않은 수신 데이터 아직 확인되지 않은 송신 데이터

LISTEN 소켓의 Recv-Q 가 Send-Q 에 붙어 있으면 accept 큐가 넘치고 있다는 뜻이다. 이때 nstat -az | grep -E 'ListenOverflows|ListenDrops' 가 증가한다면 네트워크가 아니라 애플리케이션 처리량이 원인이다. 방화벽을 아무리 뒤져도 나오지 않는다.

TCP 상태별 운영 판정도 정리해 두자.

/proc/net/tcp 를 직접 읽는 방법도 알아 두면 좋다. 도구가 없는 최소 컨테이너에서 마지막 수단이 된다. 로컬 주소는 리틀엔디언 16진수이고 포트도 16진수다(8080 = 1F90), 상태 컬럼의 0A 가 LISTEN 이다.

현장에서 만나는 모습

refused 를 방화벽 탓으로 돌리는 오진. 실무 방화벽은 거의 언제나 DROP 정책이라 막힌 포트는 timeout 이 된다. RST 를 돌려주는 REJECT 규칙을 일부러 쓴 경우가 아니면, refused 는 "거기까지 갔고 아무도 안 듣더라" 는 뜻이다.

쿠버네티스 Endpoints 가 비어 있을 때. 서비스 IP 로 붙으면 즉시 refused 가 난다. kubectl get endpoints <svc><none> 이면 셀렉터가 안 맞거나 readinessProbe 가 실패 중인 것이다. 네트워크가 아니라 라벨 문제다.

ss 한 줄로 좁히는 순서

네트워크 문제를 볼 때 netstat 대신 ss 를 쓰면 필요한 것만 빠르게 뽑을 수 있다. 자주 쓰는 형태를 정해 두면 조사 시간이 크게 준다.

ss -tlnp                       # 듣고 있는 TCP 와 그 프로세스
ss -tanp state established '( dport = :5432 or sport = :5432 )'
ss -s                          # 상태별 요약
ss -tin                        # rtt, cwnd, 재전송 — 성능을 볼 때

듣는 소켓에서 Recv-Q/Send-Q 의 뜻이 다르다. 연결된 소켓에서는 각각 읽지 않은 데이터와 보내지 못한 데이터지만, 듣는 소켓에서는 현재 accept 큐 길이와 그 상한이다. 두 숫자가 붙어 있으면 애플리케이션이 연결을 못 받아 가는 중이고, 그때 커널은 완료된 연결을 버린다.

nstat -az | grep -E 'ListenDrops|ListenOverflows|TCPBacklogDrop'

어디에 바인딩했는지 본다. 127.0.0.1:8080 은 밖에서 못 들어온다. 컨테이너 안에서 루프백에 바인딩해 놓고 "포트가 안 열린다" 고 하는 경우가 가장 흔하다. 0.0.0.0 이나 * 로 나와야 밖에서 닿는다.

연결이 어느 상태에서 멈췄는지가 원인을 가른다.

상태
SYN-SENT 가 쌓임 상대가 응답하지 않는다(방화벽이 조용히 버림)
SYN-RECV 가 쌓임 우리 쪽 accept 큐나 SYN 큐 문제
CLOSE-WAIT 가 쌓임 우리 애플리케이션이 close() 를 안 한다
FIN-WAIT-2 가 쌓임 상대가 닫지 않는다
TIME-WAIT 가 많음 정상. 60초 뒤 사라진다

CLOSE-WAIT 는 커널이 아니라 우리 코드의 버그를 가리키는 유일한 상태라 특히 중요하다. 시간이 지나도 사라지지 않는다.

거절과 무응답을 구별한다. Connection refused 는 상대 호스트까지 갔는데 그 포트가 안 열린 것이고, 타임아웃은 패킷이 어딘가에서 버려진 것이다. 앞의 것은 서비스 문제, 뒤의 것은 경로나 방화벽 문제다.

다음 실습에서 할 것

서버를 두 개 띄우되 하나는 0.0.0.0, 하나는 127.0.0.1 에 바인딩한다. 그리고 자기 IP 로 두 포트를 두드려 어느 쪽이 왜 실패하는지를 직접 만든다. /proc/net/tcp 도 손으로 읽는다.