포트 고갈
고친 사람 github-actions[bot]
포트 고갈은 한 컴퓨터가 밖으로 새 연결을 더 못 맺게 되는 고장입니다. 연결을 맺을 때마다 운영체제가 빌려 주는 번호가 다 쓰여서 생깁니다. 짧은 연결을 아주 자주 맺고 끊는 서비스에서 주로 터집니다.
쉽고 빠른 이해
무슨 일이 생기나 — 다른 서버로 연결을 걸 때 운영체제가 붙여 줄 번호가 없습니다. 그래서 연결이 실패합니다. 같은 백엔드 서버를 초마다 수백 번씩 새로 부르는 서비스가 어느 순간 연결 오류를 쏟아냅니다.
왜 생기나 — 연결을 걸 때마다 내 쪽에도 번호(포트)가 하나 필요합니다. 그 번호는 수만 개로 정해져 있습니다. 끊은 연결의 번호도 바로 돌아오지 않습니다. 1분쯤 묶여 있습니다.
어떻게 터지나
- 같은 상대에게 연결을 새로 맺을 때마다 번호를 하나 빌립니다
- 다 쓰고 내 쪽이 먼저 끊으면 그 번호가 1분쯤 묶입니다
- 묶인 번호와 쓰는 번호가 준비된 번호 수에 닿으면 새 연결이 실패합니다
무엇이 나빠지나 — 상대 서버는 멀쩡한데 이쪽에서 거는 요청이 전부 실패합니다. 연결을 오래 쥐고 안 놓는 코드도 번호를 붙들어 같은 일을 만듭니다.
상세
이 절은 포트 고갈이 어떤 조건에서 터지는지 따라갑니다. 연결을 가리는 값 · 터지는 조건 · 보이는 모습 · 이웃 고장 · 푸는 방법 순서로 봅니다.
연결을 가리는 네 값
TCP(Transmission Control Protocol, 전송 제어 프로토콜)는 두 프로그램 사이에 연결을 맺고 데이터를 순서대로 나르는 규약입니다. 웹 요청이나 데이터베이스 접속 대부분이 이 위에서 오갑니다. 포트 고갈도 대개 TCP 연결에서 이야기합니다.
포트는 한 컴퓨터 안에서 어느 프로그램의 연결인지 가르는 번호입니다. 0부터 65535까지 있습니다. 컴퓨터를 가리키는 IP 주소(Internet Protocol address, 인터넷 프로토콜 주소)와 짝을 지어 씁니다.
TCP 연결 하나는 네 값으로 가려집니다. 내 IP 주소와 포트, 상대 IP 주소와 포트입니다. 이 네 값의 묶음을 4-튜플이라고 부릅니다.
같은 네 값을 가진 연결이 둘 있으면 들어온 데이터가 어느 연결 것인지 가를 수 없습니다. 그래서 운영체제는 4-튜플이 겹치는 연결을 만들지 않습니다.
거는 쪽이 빌리는 임시 포트
서버는 자기 포트를 미리 정해 둡니다. 웹 서버라면 443번에서 기다립니다. 거는 쪽은 번호를 고르지 않습니다.
연결을 걸면 운영체제가 빈 번호 하나를 골라 내 쪽 포트로 붙여 줍니다. 이렇게 잠깐 빌려 쓰고 돌려주는 번호를 임시 포트라고 합니다. 답이 돌아올 때 어느 프로그램에 넘길지 가르려면 거는 쪽에도 번호가 있어야 합니다.
임시 포트는 정해 둔 구간 안에서만 빌려 줍니다. IANA(Internet Assigned Numbers Authority, 인터넷 번호 할당 기관)가 권하는 구간은 49152번부터 65535번까지입니다. 리눅스는 기본으로 32768번부터 60999번까지를 씁니다. 운영체제마다 구간이 다르고 설정으로 바꿀 수 있습니다.
같은 상대 한 곳에 거는 연결만 보면 4-튜플에서 바뀌는 값은 내 임시 포트 하나뿐입니다. 내 주소와 상대 주소, 상대 포트는 고정입니다. 그래서 한 상대에게 동시에 둘 수 있는 연결 수가 임시 포트 구간 크기에 묶입니다. 포트 고갈은 이 구간이 바닥나는 일입니다.
받는 서버에서는 포트 고갈이 잘 안 납니다. 서버의 포트는 443번 하나여도 연결마다 상대 주소와 상대 포트가 달라 4-튜플이 겹치지 않기 때문입니다. 번호가 바닥나는 쪽은 거는 쪽입니다.
끊은 뒤에도 묶이는 번호
연결을 끊어도 번호가 바로 돌아오지는 않습니다. 연결을 먼저 끊은 쪽은 TIME_WAIT 라는 상태에서 잠깐 기다립니다. 늦게 도착한 옛 연결의 데이터가 같은 4-튜플로 새로 맺은 연결에 섞이지 않게 하려는 기다림입니다.
기다리는 동안 그 4-튜플은 쓸 수 없습니다. 리눅스는 이 시간을 60초로 고정해 두었습니다. 거는 쪽이 먼저 끊으면 그 임시 포트는 60초 동안 같은 상대에게 다시 못 씁니다.
받는 서버 쪽에 TIME_WAIT 가 많이 쌓여도 새 연결을 받는 데는 탈이 없습니다. 앞에서 본 대로 들어오는 연결마다 상대 칸이 다르기 때문입니다.
임시 포트 하나가 거치는 상태를 그리면 이렇습니다.
stateDiagram-v2
state "빈 번호" as 빈
state "연결에 쓰는 중" as 쓰는중
[*] --> 빈
빈 --> 쓰는중: 새 연결을 맺는다
쓰는중 --> TIME_WAIT: 내 쪽이 먼저 끊는다
쓰는중 --> 빈: 상대가 먼저 끊고 내가 닫는다
TIME_WAIT --> 빈: 60초가 지난다
그림에서 번호를 붙드는 칸은 둘입니다. 쓰는 중인 번호와 TIME_WAIT 에 묶인 번호를 합친 수가 구간 크기에 닿으면 새 연결에 붙일 번호가 없습니다.
터지는 조건
포트 고갈은 흔히 이 셋이 겹칠 때 터집니다.
- 한 출발 IP 주소에서 같은 상대 IP 주소·포트 하나로 연결이 몰립니다
- 쓴 번호가 바로 돌아오지 않고 쌓입니다. 내 쪽이 먼저 끊어 TIME_WAIT 에 묶이거나, 아예 안 놓습니다
- 새 연결을 자주 맺습니다. 같은 상대에게 1초에 수백 개를 맺습니다
셋째 조건의 문턱을 리눅스 기본값으로 셈해 봅니다. 연결을 모두 내 쪽이 먼저 끊는다고 잡으면, 1초에 새로 맺는 연결 수에 60초를 곱한 만큼 번호가 묶입니다.
ports = 60999 - 32768 + 1 # 28232
per_sec = ports / 60 # 470.5...
첫 줄은 리눅스 기본 구간에 든 임시 포트 개수입니다. 둘째 줄은 그 번호를 60초씩 돌려 쓸 때 1초에 맺을 수 있는 새 연결 수입니다. 같은 상대 한 곳으로 1초에 470개 남짓을 넘겨 맺으면 번호가 모자랍니다.
첫째 조건이 빠지면 한도가 넓어집니다. 상대가 여럿이면 4-튜플의 상대 칸이 달라지므로 같은 임시 포트를 상대마다 또 쓸 수 있습니다. 한도는 상대 한 곳마다 따로 걸립니다.
둘째 조건의 두 길 가운데 TIME_WAIT 는 앞에서 봤습니다. 다른 길은 코드가 연결을 닫지 않고 버리는 경우입니다. 그 번호는 쓰는 중으로 남습니다. 이런 커넥션 누수는 연결을 자주 맺지 않아도 구간을 천천히 채웁니다.
터졌을 때 보이는 모습
번호가 바닥나면 연결을 거는 호출 connect 가 바로 실패합니다. 망으로는 아무 신호도 안 나갑니다. 상대 서버는 이 실패를 모릅니다.
리눅스에서는 이때 EADDRNOTAVAIL 오류가 돌아옵니다. 요청한 주소를 붙일 수 없다는 뜻입니다. 문구는 Cannot assign requested address 입니다. 애플리케이션 로그에는 이 문구가 연결 실패 예외에 섞여 찍힙니다.
연결 목록을 찍어 보면 원인이 드러납니다. netstat 이나 ss 로 보면 같은 상대에게 간 TIME_WAIT 연결이 수만 줄 쌓여 있습니다.
tcp 0 0 10.0.0.5:41022 10.0.0.9:8080 TIME_WAIT
tcp 0 0 10.0.0.5:41024 10.0.0.9:8080 TIME_WAIT
tcp 0 0 10.0.0.5:41026 10.0.0.9:8080 TIME_WAIT
세 줄 모두 오른쪽 상대 칸이 10.0.0.9:8080 으로 같습니다. 왼쪽 내 칸에서는 포트 번호만 바뀝니다. 앞에서 본 「바뀌는 값은 임시 포트 하나뿐」이 목록에 그대로 찍힌 모습입니다.
자주 부딪히는 구성
터지는 조건의 첫째가 저절로 서는 구성이 있습니다. 상대가 한 곳으로 고정된 채 연결이 몰리는 곳입니다.
첫째는 서버가 다른 서버를 부르는 경우입니다. 애플리케이션 서버가 같은 데이터베이스나 같은 외부 API(Application Programming Interface, 애플리케이션 프로그래밍 인터페이스)를 요청마다 새 연결로 부르면 조건이 다 섭니다. 이때는 서버가 거는 쪽입니다.
둘째는 리버스 프록시입니다. 리버스 프록시는 클라이언트 요청을 받아 뒤쪽 서버에 대신 넘겨 주는 서버입니다. 요청마다 뒤쪽 서버에 새 연결을 맺고 끊으면 프록시가 거는 쪽이 되어 임시 포트를 씁니다. 뒤쪽 서버 주소가 하나면 한도는 그 한 곳에 걸립니다.
셋째는 부하 테스트 도구입니다. 테스트 장비 한 대가 서버 한 곳에 짧은 연결을 초마다 수천 개 맺습니다. 서버보다 테스트 장비가 먼저 번호를 다 씁니다.
NAT 장비에서의 포트 고갈
NAT(Network Address Translation, 네트워크 주소 변환)는 여러 컴퓨터가 공인 IP 주소 하나를 나눠 쓰게 해 주는 장비입니다. 공인 주소는 인터넷 어디서나 닿는 주소입니다. 안쪽 컴퓨터들은 사설 주소를 씁니다. 사설 주소는 바깥에서 직접 닿지 않는 안쪽 전용 주소입니다.
안쪽 컴퓨터가 밖으로 연결을 걸면 NAT 가 출발 주소를 자기 공인 주소로 바꿉니다. 출발 포트도 자기 번호 가운데 하나로 바꿔 붙입니다.
그래서 안쪽 컴퓨터가 몇 대든 밖에서 보면 거는 쪽은 NAT 하나입니다. 안쪽 컴퓨터들이 공인 주소 하나의 번호를 함께 나눠 씁니다.
flowchart TD
subgraph 안쪽["사설 주소를 쓰는 안쪽"]
A["서버 A"]
B["서버 B"]
C["서버 C"]
end
A --> N["NAT · 공인 주소 하나"]
B --> N
C --> N
N --> X["같은 외부 API"]
세 서버가 같은 외부 API 를 부르면 NAT 한 곳의 번호가 세 서버 몫을 다 받아 냅니다. 서버마다 따로 보면 번호가 넉넉해도 NAT 에서 먼저 바닥납니다. 이때는 서버 쪽 연결 목록을 봐도 원인이 안 보입니다.
헷갈리는 이웃 고장
이름이나 증상이 비슷한 고장이 둘 있습니다. 무엇이 모자라는지가 셋을 가릅니다.
EADDRINUSE 는 서버가 번호를 잡는 호출 bind 에서 납니다. 잡으려는 번호를 다른 연결이 이미 쓰고 있다는 뜻입니다. 화면에는 Address already in use 로 찍힙니다. 번호 하나가 겹친 것이지 구간이 바닥난 것이 아닙니다.
소켓은 프로그램이 연결 하나를 다룰 때 쥐는 손잡이입니다. 프로그램은 연결 하나마다 소켓을 하나 엽니다. 데이터도 그 소켓으로 보내고 받습니다.
파일 디스크립터는 운영체제가 프로그램에 열린 파일마다 붙여 주는 번호입니다. 소켓도 파일처럼 이 번호를 하나씩 씁니다. 그래서 연결이 많아지면 이 번호도 함께 불어납니다.
한 프로그램이 쥘 수 있는 파일 디스크립터 수에는 상한이 있습니다. 상한에 닿으면 EMFILE(Too many open files, 열린 파일이 너무 많음) 오류가 납니다. 포트가 남아 있어도 연결을 못 엽니다.
셋을 나란히 놓으면 이렇습니다.
| 고장 | 무엇이 모자라나 | 보이는 오류 |
|---|---|---|
| 포트 고갈 | 나가는 연결에 붙일 임시 포트 | EADDRNOTAVAIL |
| 이미 쓰는 포트 | 서버가 잡으려는 특정 번호 하나 | EADDRINUSE |
| 파일 디스크립터 상한 | 한 프로그램이 쥘 수 있는 번호(소켓도 하나씩 쓴다) | EMFILE |
푸는 방법
푸는 길은 두 갈래입니다. 번호를 덜 쓰게 하거나, 쓸 수 있는 번호를 늘립니다. 아래 표는 흔한 방법과 그 대가입니다.
| 방법 | 무엇이 바뀌나 | 대가 |
|---|---|---|
| 연결을 돌려 쓴다 · 커넥션 풀 · 지속 연결 | 새로 맺고 끊는 횟수가 준다 | 쉬는 연결도 양쪽 자원을 쥔다 |
| 임시 포트 구간을 넓힌다 · ip_local_port_range | 빌려 줄 번호가 는다 | 넓힌 구간의 번호를 쓰는 서버 프로그램과 부딪힐 수 있다 |
| 출발 주소나 상대 주소를 늘린다 | 4-튜플 조합이 는다 | 주소와 설정을 따로 관리해야 한다 |
| 먼저 끊는 쪽을 바꾼다 | TIME_WAIT 가 상대 쪽에 생긴다 | 상대가 그 기다림을 떠맡는다 |
| tcp_tw_reuse 를 켠다(리눅스) | TIME_WAIT 가 안 끝난 번호도 나가는 연결에 다시 쓴다 | 옛 데이터를 가리는 일을 기다림 대신 TCP 타임스탬프에 맡긴다 |
첫 줄이 원인을 없애는 방법입니다. 요청마다 맺고 끊던 연결을 열어 둔 채 여러 요청에 돌려 쓰면 번호가 거의 안 돕니다. 나머지 넷은 번호를 더 늘리거나 더 빨리 돌려받아 한도를 뒤로 미룹니다.
관련 항목
포트 고갈이 바닥내는 연결 식별 값
포트 · 포트 번호 · 임시 포트 · 4-튜플 · IP 주소 · 소켓
포트 고갈을 부르는 연결 종료와 상태
TIME_WAIT · CLOSE_WAIT · TCP 상태 · 4방향 핸드셰이크 · 커넥션 누수 · MSL
포트 고갈과 증상이 겹치는 오류
EADDRNOTAVAIL · EADDRINUSE · EMFILE · 파일 디스크립터 · 연결 거부 · 연결 타임아웃
포트 고갈이 자주 터지는 구성
리버스 프록시 · NAT · SNAT · NAT 게이트웨이 · 로드 밸런서 · 부하 테스트 · 마이크로서비스
포트 고갈을 막거나 늦추는 수단
커넥션 풀 · 지속 연결 · HTTP keep-alive · ip_local_port_range · tcp_tw_reuse · SO_REUSEADDR · SO_LINGER
포트 고갈을 들여다보는 명령
netstat · ss · lsof · tcpdump
포트 고갈이 속하는 상위 분류
다른 이름: port exhaustion · ephemeral port exhaustion · 임시 포트 고갈 · 소스 포트 고갈