TCP 재전송과 타임아웃
아래 내용은 DevOps와 SE를 위한 리눅스 커널 이야기 9장 TCP 재전송과 타임아웃을 읽고 정리한 내용입니다.
TCP는 그 특성상 자신이 보낸 데이터에 대해서 상대방이 받았다는 의미의 응답 패킷을 다시 받아야 통신이 정상적으로 이루어졌다고 생각한다. 그래서 만약 자신이 보낸 데이터에 대한 응답 패킷을 받지 못하면 패킷이 유실되었다고 판단하고 보냈던 패킷을 다시 한번 보낸다. 이 과정을 TCP 재전송이라고 한다.
RTO란
패킷 유실 후에 재전송이 일어나고 재전송에 대한 ACK를 얼마나 기다려야 하는지에 대한 값을 RTO(Retransmission Timeout)라고 부른다. RTO 안에 ACK를 받지 못하면 보내는 쪽에서 재전송을 진행한다.
- RTO에는 일반적인
RTO
와initRTO
두 가지가 존재 - 일반적인 RTO는 RTT(RoundTripTime, 두 종단 간 패킷 전송에 필요한 시간)를 기준으로 설정된다. 예를 들어, 두 종단 간 패킷 전송에 필요한 시간이 1초라면, 최소한 1초는 기다려야 내가 보낸 패킷이 손실되었는지 아닌지를 판단할 수 있다.
- InitRTO는 두 종단 간 최초의 연결을 시작할 때, 즉 TCP Handshake가 일어나는 첫 번째 SYN 패킷에 대한 RTO를 의미한다. 맨 처음 연결을 맺을 때는 두 종단 간 RTT와 같은 패킷 전송의 소요 시간을 전혀 알 수 없기 때문에 임의로 설정한 값으로 RTO를 계산한다. 이 때의 RTO를 InitRTO라고 하며 리눅스에서는 소스 코드에 1초로 구현해 놓았다. 즉 SYN 패킷에 대한 RTO는 특별히 1초로 설정된다고 볼 수 있다.
[ec2-user@ip-172-31-35-50 ~]$ ss -i
Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
u_str ESTAB 0 0 * 17951 * 17952
# ...(생략)
tcp ESTAB 0 0 172.31.35.50:ssh 221.146.0.112:50079
cubic wscale:6,7 rto:216 rtt:12.896/8.354 ato:40 mss:1448 pmtu:9001 rcvmss:1368 advmss:8949 cwnd:10 bytes_sent:4885 bytes_acked:4885 bytes_received:4325 segs_out:47 segs_in:60 data_segs_out:41 data_segs_in:25 send 8.98Mbps lastsnd:24 lastrcv:24 lastack:20 pacing_rate 18Mbps delivery_rate 14.8Mbps delivered:42 app_limited busy:380ms rcv_space:56575 rcv_ssthresh:56575 minrtt:6.191
# rto는 216ms
# rtt는 12.896ms, 편차: 8.354ms
RTO값은 초기값을 기준으로 2배씩 증가한다. 예를 들어 처음 재전송이 일어나면 216ms, 그 다음 재전송 요청까지 432ms순으로 2배씩 계속해서 증가한다. 그렇다고 재전송 횟수 제한 없이 그 수가 계속해서 커지는 것은 아니다.
재전송을 결정하는 파라미터
[root@ip-172-31-35-50 ~]# sysctl -a | grep -i retries
net.ipv4.tcp_orphan_retries = 0
net.ipv4.tcp_retries1 = 3
net.ipv4.tcp_retries2 = 15
net.ipv4.tcp_syn_retries = 6
net.ipv4.tcp_synack_retries = 5
...(생략)
net.ipv4.tcp_syn_retries
- TCP 재전송은 이미 연결되어 있는 세션에서도 일어나지만 연결을 시도하는 과정에서도 일어나며 해당 파라미터는 SYN에 대한 재시도 횟수를 결정
net.ipv4.tcp_synack_retries
- 상대편이 보낸 SYN에 대한 응답으로 보내는 SYN+ACK의 재전송 횟수를 정의
net.ipv4.tcp_orphan_retries
- orphan socket이라 불리는 상태의 소켓들에 대한 재전송 횟수를 결정
orphan socket
netstat 명령으로 소켓의 연결 상태들을 살펴보면 ESTABLISHED 상태의 소켓들은 우측에 속한 PID와 프로세스 이름이 보이지만 FIN_WAIT, TIME_WAIT 등의 소켓들은 커널에 귀속되었기 때문에 PID와 프로세스 이름이 보이지 않는 것을 확인할 수 있다.
이렇게 특정 프로세스에 할당되지 않고 커널에 귀속되어 정리되기를 기다리는 소켓 중에서도 FIN_WAIT 상태의 소켓을 orphan socket이라고 한다.
서버가 클라이언트에 FIN 패킷을 전송하고 아주 짧은 시간에 FIN_WAIT1 상태에서 FIN_WAIT2 상태로 빠지고 TIME_WAIT 상태로 이어진다. FIN 전송과 그에 대한 ACK를 받는 과정이 굉장히 빠르게 이루어지기 때문이다.
본문에서는 tcp_orphan_retries() 함수의 소스 코드도 확인하는데 해당 내용은 아래와 같다.
{
...
if (retries == 0 && alive)
retries = 8;
return retries;
}
위 소스코드에서 net.ipv4.tcp_orphan_retries를 0으로 설정하거나 alive 값이 1이라면 if문의 결과는 true가 되고 retries 값은 0이 아닌 8로 바뀌어서 반환된다.
만약 FIN_WAIT1 상태에서 지정된 재전송 횟수까지 모두 보내고 나면 해당 소켓은 FIN_WAIT2, TIME_WAIT 상태로 변경되지 않고 이미 죽은 소켓으로 판단하여 소켓을 아예 회수해 버린다. 그렇기 때문에 net.ipv4.tcp_orphan_retries 값이 너무 작으면 FIN 패킷이 유실된 상태의 FIN_WAIT1 소켓이 너무 빨리 정리될 수 있으며, 상대편에 닫혀야 되는 소켓이 닫히지 않는 결과를 초래할 수도 있다.
최소한 TIME_WAIT이 유지되는 시간인 60초가 될 수 있도록 7정도의 값을 설정하는 것이 좋으며 그래야 최소한 TIME_WAIT가 남아있는 만큼의 효과를 유지할 수 있다. net.ipv4.tcp_orphan_retries
값을 7로 설정함으로써 orphan socket을 너무 오래 시스템에 남아 있지 않도록 하면서도 TIME_WAIT 상태의 기본 지속 시간과 유사한 시간 동안 연결 종료를 위한 충분한 재전송 기회를 가질 수 있게 됩니다.
TIME_WAIT 상태는 TCP 연결이 닫힌 후에도 일정 시간 동안 유지되는 상태로 이 상태는 재전송된 패킷이 올 수 있을 때 이를 처리하기 위해 유지되며 일반적으로 2 x MSL(Maximum Segment Lifetime) 만큼의 시간동안 유지, MSL 값은 일반적으로 30초 정도이므로 약 60초가 TIME_WAIT의 기본 지속 시간이 된다.
net.ipv4_tcp_retries1
- IP 레이어에 네트워크가 잘못 되었는지 확인하도록 사인을 보내는 기준
- soft threshold
net.ipv4_tcp_retries2
- 더 이상 통신을 할 수 없다고 판단하는 기준
- hard threshold
- 결과적으로 이 값에 정의된 횟수만큼을 넘겨야 실제 연결이 끊김
tcpretrans
- 재전송이 의심되는 서버에서 tcpdump를 추출할 시 너무 많은 패킷이 잡히므로 tcpretrans 스크립트를 사용하여 패킷 분석
- 1초에 한 번씩 깨어나서 ftrace를 통해 수집한 커널 함수 정보를 바탕으로 재전송이 일어났는지 아닌지를 파악한 후,
/proc/net/tcp
의 내용을 파싱하여 어떤 세션에서 재전송이 일어났는지 출력 - RTO_MIN 값이 200ms이며 스크립트에 설정된 1초의 인터벌은 트래픽이 많은 서버라면 재전송되는 패킷을 놓칠 수도 있어 좀 더 정확한 추적이 필요하다면 interval 값을 기존 1에서 0.2(200ms)로 낮추는 방법도 권장
- https://github.com/brendangregg/perf-tools/blob/master/net/tcpretrans
RTO_MIN 값 변경하기
[ec2-user@ip-172-31-35-50 ~]$ ss -i
Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
u_str ESTAB 0 0 * 17951 * 17952
# ...(생략)
tcp ESTAB 0 0 172.31.35.50:ssh 221.146.0.112:50079
cubic wscale:6,7 rto:216 rtt:12.896/8.354 ato:40 mss:1448 pmtu:9001 rcvmss:1368 advmss:8949 cwnd:10 bytes_sent:4885 bytes_acked:4885 bytes_received:4325 segs_out:47 segs_in:60 data_segs_out:41 data_segs_in:25 send 8.98Mbps lastsnd:24 lastrcv:24 lastack:20 pacing_rate 18Mbps delivery_rate 14.8Mbps delivered:42 app_limited busy:380ms rcv_space:56575 rcv_ssthresh:56575 minrtt:6.191
# rto는 216ms
# rtt는 12.896ms, 편차: 8.354ms
- 주고 받는 데에 12.25ms가 걸리는 두 종단 사이에서 50ms 후에도 응답을 받지 못한다면 이미 유실되 었다고 봐야 하는데 216ms로 설정된 rto 값은 너무 큼, 216ms 동안 기다리는 것은 오히려 더 낭비일 수도 있음
[root@ip-172-31-35-50 ~]$ ip route
default via 172.31.32.1 dev eth0 # check
169.254.169.254 dev eth0
172.31.32.0/20 dev eth0 proto kernel scope link src 172.31.35.50
위 명령어 결과 default via 172.31.32.1 dev eth0
내용은 찾은 외부와의 통신을 위한 모든 패킷은 eth0이라는 네트워크 디바이스의 172.31.32.1 게이트웨이를 통해서 나간다는 의미
[root@ip-172-31-35-50 ~]$ ip route change default via 172.31.32.1 dev eth0 rto_min 100ms
[root@ip-172-31-35-50 ~]$ ss -i
...(생략)
cubic wscale:6,7 rto:136 rtt:9.799/3.63 ato:40 mss:1448 pmtu:9001 rcvmss:1368 advmss:8949 cwnd:10 bytes_sent:34789 bytes_retrans:92 bytes_acked:34661 bytes_received:11385 segs_out:293 segs_in:467 data_segs_out:283 data_segs_in:220 send 11.8Mbps pacing_rate 23.6Mbps delivery_rate 4.36Mbps delivered:283 app_limited busy:3324ms unacked:1 retrans:0/1 dsack_dups:1 rcv_space:56575 rcv_ssthresh:56575 minrtt:6.189
- rto_min이 100ms 내려가면서 rto 값도 함께 감소하였다. 즉, 136ms 동안 응답을 받지 못하면 재전송한다.
- 본문에서는 rto_min 값이 어느 정도가 적당한지에 대한 답은 없다고 함
- 외부에 노출된 웹 서버에는 다양한 고객들이 접근하기 때문에 기본값으로 정해진 200ms를 따르는 것이 좋다.
- 내부에서 통신하는 서버에서는 200ms라는 값이 길게 느껴짐
- 내부 통신의 경우 rtt는 매우 짧아 좀 더 빠른 재전송이 필요한지 확인하여 rto_min 값을 그에 상응하는 수준으로 낮춰서 빨리 보내는 것이 서비스의 품질을 높일 수 있는 좋은 방법
- rto_min 값이 너무 낮다면 잦은 재전송이 일어날 수도 있기 때문에 신중하게 적당한 값을 설정
애플리케이션 타임아웃
Connection Time out
- TCP Handshake 과정에서 재전송이 일어날 경우 발생
Read Time out
- 맺어져 있는 세션을 통해서 데이터를 요청하는 과정에서 발생
SYN
, SYN+ACK
패킷은 종 단에 대한 정보가 없기 때문에 RTO를 계산하기 위한 RTT 값을 구할 수가 없다. 그렇기 때문에 기본적으로 1초로 설정되어 있지만 SYN과 SYN+ACK를 주고 받은 후에는 종단에 대한 정보가 생기기 때문에 해당 패킷에 대한 RTT 값을 측정할 수 있게 되고 이때부터는 RTO가 계산된다. 그래서 Connection Timeout은 SYN, SYN+ACK의 유실에서 발생한다.
최소한 한 번의 재전송은 견딜 수 있도록 애플리케이션의 타임 아웃 중 Connection Timeout은 3초, Read Timeout은 300ms 이상으로 설정하는 것이 좋다.