사전 TIME_WAIT
개념

TIME_WAIT

gabury1고친 사람 github-actions[bot]

TIME_WAIT 는 연결을 먼저 끊은 쪽이 끊은 뒤에도 잠깐 기다리는 상태입니다. 기다리는 동안 운영체제는 끊긴 연결의 기록을 지우지 않고 남겨 둡니다. 늦게 도착한 옛 연결의 데이터가 새 연결에 섞이지 않게 하려는 것입니다. 짧은 연결을 아주 자주 맺는 서버에서는 이 기록이 쌓여 새 연결을 못 맺기도 합니다.

쉽고 빠른 이해

무슨 일을 하나 — 끊은 연결의 기록을 잠깐 더 남겨 둡니다. 서버에서 연결 목록을 찍으면 끝난 연결이 TIME_WAIT 라는 이름으로 수백 개씩 보입니다.

왜 이렇게 하나 — 끊는 순간에도 아직 도착하지 않은 데이터가 망에 떠다닐 수 있습니다. 기록을 바로 지우고 같은 주소와 포트로 새 연결을 맺으면 그 늦은 데이터가 새 연결로 들어갑니다. 마지막 답이 사라지면 상대가 끊자는 말을 다시 보냅니다. 그때 받아 줄 기록이 남아 있어야 다시 답할 수 있습니다.

어떻게 도나

  1. 한쪽이 먼저 끊자고 합니다. 상대도 끊자고 답합니다
  2. 먼저 끊자고 한 쪽이 마지막 답을 보내고 기다리기 시작합니다
  3. 정해진 시간이 지나면 운영체제가 기록을 지웁니다

대가 — 기다리는 동안 그 연결이 쓰던 주소와 포트 짝을 다시 못 씁니다. 같은 서버의 같은 포트로 짧은 연결을 초마다 오백 번 넘게 맺으면 쓸 포트가 바닥납니다.

상세

이사를 나간 집을 떠올려 봅시다. 집을 비운 뒤에도 옛 주소로 편지가 한동안 계속 옵니다. 그 집을 곧바로 다음 사람에게 내주면 옛 사람 앞으로 온 편지를 새 사람이 받습니다. 그래서 몇 주는 비워 두고 늦은 편지가 다 올 때까지 기다립니다.

TIME_WAIT 는 끊긴 연결을 이렇게 잠깐 비워 두는 상태입니다. 이 절은 먼저 연결을 끊는 순서를 따라가며 TIME_WAIT 가 어느 쪽에 생기는지 봅니다. 이어서 왜 기다리는지를 두 까닭으로 봅니다. 그 까닭에 맞춰 얼마나 기다리는지는 그다음에 봅니다. 끝으로 이 상태가 쌓이면 무엇이 막히고 어떻게 줄이는지를 봅니다.

연결을 끊는 순서

TCP(Transmission Control Protocol, 전송 제어 프로토콜)는 두 프로그램 사이에 연결을 맺고 데이터를 빠짐없이 순서대로 나르는 규약입니다. 웹 요청과 데이터베이스 접속 대부분이 이 위에서 오갑니다. TIME_WAIT 는 TCP 연결이 거치는 상태 가운데 하나입니다.

TCP 는 연결을 끊을 때도 신호를 주고받습니다. 한쪽이 더 보낼 것이 없다는 신호 FIN(finish, 끝)을 보냅니다. 받은 쪽은 잘 받았다는 신호 ACK(acknowledgment, 확인 응답)로 답합니다.

FIN 은 한 방향만 닫습니다. 그래서 양쪽이 저마다 FIN 을 보내고 저마다 ACK 를 받아야 연결이 다 끊깁니다. 신호가 네 번 오가서 이 과정을 4방향 핸드셰이크라고 부릅니다.

아래 그림은 A 가 먼저 끊을 때 네 신호가 오가는 순서입니다.

sequenceDiagram
    participant A as A · 먼저 끊는 쪽
    participant B as B · 나중에 끊는 쪽
    A->>B: FIN
    B-->>A: ACK
    Note over B: B 의 프로그램이 닫기를 기다린다
    B->>A: FIN
    A-->>B: ACK
    Note over A: TIME_WAIT · 정해진 시간 동안 기다린다
    Note over B: 연결 기록을 바로 지운다

마지막 ACK 를 보낸 A 가 TIME_WAIT 에 들어갑니다. B 는 그 ACK 를 받는 즉시 연결을 지웁니다. TIME_WAIT 는 늘 먼저 FIN 을 보낸 쪽에 생깁니다. 서버든 클라이언트든 먼저 끊는 쪽이 이 기다림을 떠맡습니다.

마지막 ACK 가 사라질 때

기다리는 까닭은 둘입니다. 첫째는 마지막 ACK 가 도중에 사라지는 경우입니다. 망은 신호를 잃어버리기도 합니다.

A 가 보낸 마지막 ACK 가 사라지면 B 는 자기 FIN 이 안 닿았다고 여깁니다. 그래서 FIN 을 다시 보냅니다.

A 가 이미 연결 기록을 지웠다면 이 FIN 을 알아보지 못합니다. TCP 는 모르는 연결의 신호를 받으면 RST(reset, 초기화)로 답합니다. RST 는 「그런 연결은 없다」며 상대 연결을 강제로 끊는 신호입니다. 그러면 B 의 프로그램은 잘 끝난 연결을 오류로 받습니다.

TIME_WAIT 에 있는 A 는 기록이 남아 있어 다시 온 FIN 에 ACK 를 한 번 더 보낼 수 있습니다. 아래 그림이 이 경우입니다.

sequenceDiagram
    participant A as A · TIME_WAIT
    participant B as B
    A-xB: ACK · 도중에 사라진다
    B->>A: FIN · 답이 없어 다시 보낸다
    A-->>B: ACK · 기록이 남아 있어 다시 답한다
    Note over B: 연결을 잘 끝낸다

늦게 도착하는 옛 세그먼트

둘째 까닭은 늦게 도착하는 데이터입니다. 이것을 보려면 TCP 가 데이터를 어떻게 나르는지와 연결을 어떻게 가리는지부터 알아야 합니다.

TCP 는 보낼 데이터를 잘게 잘라 보냅니다. 이 한 덩어리를 세그먼트라고 합니다. 세그먼트는 망 안에서 길을 돌거나 장비 안에 머물며 늦게 도착하기도 합니다.

TCP 연결 하나는 네 값으로 가려집니다. 보내는 쪽 IP 주소(Internet Protocol address, 인터넷 프로토콜 주소)와 포트, 받는 쪽 IP 주소와 포트입니다. 포트는 한 컴퓨터 안에서 어느 프로그램의 연결인지 가르는 번호입니다. 이 네 값의 묶음을 4-튜플이라고 부릅니다.

연결이 끊긴 뒤에도 그 연결의 세그먼트 일부가 망 어딘가에서 늦게 떠돌 수 있습니다. 그사이 같은 4-튜플로 새 연결이 맺어지면 곤란해집니다. 늦게 온 옛 세그먼트가 새 연결의 데이터처럼 받아들여질 수 있기 때문입니다.

TIME_WAIT 는 기다리는 동안 이 4-튜플을 붙들어 둡니다. 기본으로는 같은 네 값으로 새 연결을 못 맺게 합니다. 옛 세그먼트가 망에서 다 사라질 만큼 기다린 뒤에 4-튜플을 풉니다.

기다리는 시간

TIME_WAIT 가 얼마나 기다리는지는 세그먼트가 망에 얼마나 오래 머물 수 있나로 정합니다. 앞의 두 까닭이 모두 이 시간에 걸려 있습니다.

MSL(Maximum Segment Lifetime, 최대 세그먼트 수명)은 세그먼트 하나가 망 안에서 떠돌 수 있는 가장 긴 시간을 어림한 값입니다. 이 값이 있어야 「이만큼 기다리면 늦은 세그먼트도 다 사라졌다」고 말할 수 있습니다.

TIME_WAIT 는 MSL 의 두 배를 기다립니다. 이 길이를 흔히 2MSL 이라고 부릅니다.

두 배는 첫째 까닭의 왕복을 덮으려는 것입니다. A 의 마지막 ACK 가 B 로 가는 데 MSL 까지 걸립니다. B 가 다시 보낸 FIN 이 A 로 돌아오는 데 또 MSL 까지 걸립니다.

TCP 가 잡아 둔 MSL 은 2분입니다. 이대로면 TIME_WAIT 는 4분을 기다립니다. 리눅스는 MSL 과 따로 TIME_WAIT 길이를 60초로 고정해 두었습니다. 운영체제마다 이 길이가 다릅니다.

60초는 MSL 2분보다도 짧습니다. 2분은 넉넉하게 잡은 어림값입니다. 실제 망에서 세그먼트가 1분 넘게 떠도는 일은 드뭅니다. 60초만 기다려도 대개 탈이 없다고 보는 것입니다.

커널이 쥔 기록

TIME_WAIT 에 든 연결은 프로그램이 이미 놓은 연결입니다. 프로그램은 소켓을 닫고 다음 일로 넘어갔습니다. 소켓은 프로그램이 연결 하나를 다룰 때 쥐는 손잡이입니다.

기다리는 일은 커널이 맡습니다. 커널은 운영체제의 중심부입니다. 네트워크 연결도 커널이 관리합니다.

TIME_WAIT 연결은 어느 프로그램에도 속하지 않습니다. 프로그램을 재시작해도 이 기록은 남습니다.

netstat이나 ss 로 연결 목록을 찍으면 이 상태가 보입니다. 아래는 서버 10.0.0.5 의 8080 포트에서 먼저 끊은 연결 한 줄입니다.

tcp   0   0 10.0.0.5:8080   10.0.0.9:51690   TIME_WAIT

맨 끝 칸이 상태입니다. 앞의 두 주소 칸이 이 연결의 4-튜플입니다. 기다리는 동안 이 네 값이 묶여 있습니다.

쌓이면 모자라는 임시 포트

TIME_WAIT 는 정상 절차라 몇 개 있는 것은 문제가 아닙니다. 탈이 나는 것은 같은 상대에게 짧은 연결을 아주 자주 맺고 먼저 끊는 쪽입니다. 대개 다른 서버를 부르는 클라이언트 쪽입니다.

클라이언트가 연결을 맺을 때 자기 쪽 포트는 운영체제가 빈 번호에서 하나 골라 줍니다. 이렇게 잠깐 쓰고 돌려주는 포트를 임시 포트라고 합니다. 리눅스는 기본으로 32768번부터 60999번까지를 임시 포트로 씁니다.

상대가 한 서버의 한 포트로 고정이면 4-튜플에서 바뀔 수 있는 값은 내 임시 포트 하나뿐입니다. 임시 포트마다 TIME_WAIT 가 60초씩 붙들리면 1초에 맺을 수 있는 새 연결 수에 한도가 생깁니다. 아래는 그 한도를 셈한 것입니다.

Python
ports = 60999 - 32768 + 1  # 28232
wait = 60
ports / wait               # 470.53...

같은 서버의 같은 포트로는 1초에 470개 남짓이 끝입니다. 그보다 빨리 맺으면 빈 임시 포트가 없어 연결이 실패합니다. 리눅스에서는 이때 EADDRNOTAVAIL 오류가 돌아옵니다. 오류 문구는 Cannot assign requested address 입니다.

서버에 쌓일 때

서버가 먼저 끊는 경우도 흔합니다. 응답을 보낸 뒤 연결을 닫는 HTTP(HyperText Transfer Protocol, 하이퍼텍스트 전송 프로토콜) 서버가 그렇습니다. 이때 TIME_WAIT 는 서버에 쌓입니다.

서버 쪽은 포트가 모자라지 않습니다. 서버의 포트는 하나입니다. 그래도 클라이언트마다 주소와 포트가 달라 4-튜플이 모두 다릅니다. 기록 하나가 차지하는 메모리도 작습니다.

서버에서 부딪히는 것은 재시작입니다. 서버는 연결을 받기 전에 먼저 자기 포트를 잡습니다. 포트를 잡는 호출이 bind 입니다.

bind 는 4-튜플을 보지 않습니다. 포트를 잡을 때는 아직 상대가 없기 때문입니다. bind 는 내 주소와 포트만 봅니다. 그 포트를 쓰는 연결 기록이 하나라도 남아 있으면 기본으로 막습니다.

서버를 내렸다 바로 올리면 여기에 걸립니다. 그 포트를 쓰던 옛 연결들이 아직 TIME_WAIT 에 남아 있어 bind 가 EADDRINUSE 오류로 실패합니다.

소켓에 SO_REUSEADDR 옵션을 켜면 이 확인이 느슨해집니다. 남은 것이 TIME_WAIT 기록뿐이면 같은 포트를 다시 잡게 해 줍니다. 서버 프로그램이 대개 이 옵션을 켜는 까닭입니다.

줄이는 방법

흔히 쓰는 방법은 넷입니다. 앞 둘은 내 쪽에 TIME_WAIT 가 덜 생기게 합니다. 뒤 둘은 생긴 기다림을 줄이거나 건너뜁니다. 아래 표는 넷과 그 대가입니다.

방법 무엇이 바뀌나 대가
연결을 돌려 쓴다 · 지속 연결 · 커넥션 풀 끊는 횟수 자체가 준다 쉬는 연결도 자원을 쥔다
먼저 끊는 쪽을 바꾼다 TIME_WAIT 가 상대 쪽에 생긴다 상대가 그 기다림을 떠맡는다
리눅스의 tcp_tw_reuse 를 켠다 옛 연결이 TCP 타임스탬프를 썼으면 기다림이 안 끝난 4-튜플도 새로 나가는 연결에 다시 쓴다 리눅스에만 있고 나가는 연결에만 듣는다
SO_LINGER 를 0초로 두고 닫는다 FIN 대신 RST 로 끊어 TIME_WAIT 를 건너뛴다 보내던 데이터를 버린다. TIME_WAIT 가 막던 두 문제가 되살아난다

앞 두 줄은 맺고 끊는 방식만 바꿉니다. TIME_WAIT 가 지키는 두 가지는 그대로 남습니다. 뒤 두 줄은 기다림 자체를 건드립니다. 마지막 줄은 기다림과 함께 그 기다림이 지키던 것까지 버립니다.

tcp_tw_reuse 가 기대는 TCP 타임스탬프는 세그먼트마다 보낸 시각을 적어 두는 TCP 옵션입니다. 새 연결의 세그먼트는 옛 연결보다 늦은 시각을 답니다. 늦게 온 옛 세그먼트는 시각이 뒤처져 있어 받는 쪽이 가려 버릴 수 있습니다.

CLOSE_WAIT 와 가르기

이름이 비슷한 CLOSE_WAIT 는 반대편에 생깁니다. 상대의 FIN 에 ACK 로 답한 뒤 들어가는 상태입니다. 이 상태는 내 프로그램이 소켓을 닫기를 기다립니다. 첫 그림에서 B 가 머물던 상태입니다.

두 상태는 끝나는 방식이 다릅니다. TIME_WAIT 는 시간이 지나면 커널이 알아서 지웁니다. CLOSE_WAIT 는 프로그램이 소켓을 닫아야만 끝납니다. 그래서 CLOSE_WAIT 가 계속 늘면 대개 소켓을 안 닫는 코드가 있다는 뜻입니다.

관련 항목

TIME_WAIT 와 함께 TCP 연결이 거치는 상태

TCP 상태 · CLOSE_WAIT · FIN_WAIT_1 · FIN_WAIT_2 · LAST_ACK · CLOSING · ESTABLISHED · LISTEN · SYN_SENT

TIME_WAIT 를 낳는 연결 종료 절차와 신호

4방향 핸드셰이크 · 3방향 핸드셰이크 · FIN · ACK · RST · 세그먼트 · 순서 번호

TIME_WAIT 가 붙드는 연결 식별 값

4-튜플 · IP 주소 · 포트 · 포트 번호 · 임시 포트

TIME_WAIT 의 길이를 정하는 값

MSL · 2MSL · TCP_TIMEWAIT_LEN · TTL

TIME_WAIT 가 쌓여 나는 오류

포트 고갈 · EADDRNOTAVAIL · EADDRINUSE

TIME_WAIT 를 줄이거나 건너뛰는 수단

지속 연결 · 커넥션 풀 · HTTP keep-alive · TCP 타임스탬프 · SO_REUSEADDR · SO_REUSEPORT · SO_LINGER · tcp_tw_reuse · tcp_max_tw_buckets · ip_local_port_range

TIME_WAIT 를 들여다보는 명령

netstat · ss · lsof · tcpdump · 와이어샤크

TIME_WAIT 가 속하는 상위 분류

TCP · 전송 계층 · 네트워크 · 소켓 · 커널 · 리눅스

다른 이름: TIME-WAIT · time_wait · TIME_WAIT 상태 · 타임 웨이트