[Computer Network] - TCP & Timers(RTT, RTO)와 QUIC

오현석·2024년 12월 2일

Computer Network

목록 보기
24/25

⏰ TCP Timers

TCP에서 사용하는 타이머의 종류는 다음과 같이 4가지의 종류가 있다.

  1. 재전송을 위해 필요한 타이머 - Retransmission Timer (RTO timer)

  2. Window size가 0인 Ack를 받았을 때 구동시키는 타이머 - Persistence timer

  3. TCP 연결이 끊어진 상황을 알기 위한 타이머 - Keepalive timer

  4. TCP 연결을 종료할 때 모종의 시간을 기다리기 위한 타이머 - Time-waited timer

Round Trip timer(RTT) - Retransmission

TCP 연결 중 Segment나 Ack가 인터넷 상에서 사라졌다고 해보자. 이 상태가 되면, Sender는 자신이 보낸 Segment에 대한 Ack를 Receiver로부터 받지 못했으므로 해당 Segment가 잘 도착했는 지에 대해 확인할 수 없다.
그래서 Receiver가 못 받았다고 생각하여 Segment를 다시 보내야 한다. 이때, 구동시키는 타이머가 바로 Retransmission timer이다.

이 Retransmission timer는 Sender가 Segment를 보낼 때부터 구동되어, Ack가 정상적으로 돌아오면 타이머를 없애지만 Ack가 돌아오지 않는다면 Timeout 이벤트를 발생시킨다. 이걸 RTO가 timeout 되었다고 한다. RTO가 timeout 되었다는 말은 Sender가 다시 Segment를 재전송해야 한다는 의미이므로 Sender는 다시 한번 같은 Segment를 전송하게 된다.

그래도 또 Ack가 돌아오지 않아 RTO timeout이 발생했다면? 계속해서 Sender는 다시 재전송 해줘야 하는걸까?

그건 아니다. 몇 번 재전송을 보내도 Ack가 돌아오지 않는다면 Sender는 해당 TCP 연결에 문제가 있다고 판단하여 스스로 TCP 연결을 끊게 된다. (여기서 재전송 횟수는 일정 수준의 횟수로 제한하는 것이 일반적이다. 예를 들어, Linux에서는 16번으로 제한해둔다.)

RTT

그렇다면 이 Retransmission timer가 설정하는 시간은 어떻게 설정할까? 이 시간을 얼마로 설정해야 하는걸까?
시간을 너무 짧게 설정한다면 Receiver로부터 Ack가 도착하기 전에 Timeout되어 중복된 Segment를 재전송하게 될 것이고, 시간을 너무 길게 설정한다면 Ack나 Segment가 사라졌을 때 다시 Segment를 보내주는 작업이 늦게 시행되어 효율적이지 않게 된다.

단순히 Propagation Delay * 2보다 크게 하면 될까? 그런데 인터넷은 Propagation Delay를 미리 알 수가 없는데? Destination이 멀리 있는 지, 가까이 있는 지 알 수 없고, 경로 상황이 트래픽 양에 따라 매번 달라질 수 있을 뿐더러 심지어는 경로 자체가 달라질 수 있기 때문에 이걸 미리 안다고 해도 매번 값이 달라질 것이다.

이러한 이유로 인해 적절한 RTO 값을 찾기 위한 방법이 따로 존재한다.

먼저 처음에는 Destination이 어디에 있는 지를 모르니까, 매우 멀다고 가정하고 시간값을 크게 설정하여 데이터를 보낸다. 가장 처음 보내는 데이터는 SYN Segment가 되겠다.

TCP 연결을 위해 Sender가 SYN Segment를 보내고 이에 대한 Ack를 SYN+ACK로 받는다. 그렇다면 Sender는 이 Ack가 돌아오는 시간을 보고 적절한 시간값을 계산하게 된다. 이 측정한 시간을 Measured RTT, RTTm이라고 한다.

하지만, 앞서 말한 것처럼 TCP는 인터넷을 사용하기 때문에 이 RTTm이 앞으로 보내는 데이터들에 있어서 일관되지 않는다. 그래서 데이터를 보내고 돌아오는 Ack의 시간차에 대해 계속해서 확인하며 이 RTT값을 적절하게 업데이트시킨다. 이 업데이트 되는 값을 Smoothed RTT, RTTs라고 한다. 즉, RTTs는 들쭉날쭉한 RTTm값에 대한 평균치를 찾아가는 값이다. Sender는 이 RTTs값(과거 데이터로 추정한 현재 타임아웃 값)과 RTTm값(실제로 이번에 받은 시간 차이)를 이용하여 다음 RTTm을 포함할 수 있는 RTTs값을 예측해서 새로운 RTTs값을 결정하게 된다.

초기 RTTs값은 RTTm과 같다.

RTTs = RTTm

이후, 두번째 Segment 전송과 Ack 수신부터는 RTTs = (1-a) RTTs + a RTTm 으로 업데이트 된다.
여기서의 a값은 정하기 나름이지만, 일반적으로는 1/8로 많이 설정한다.

또한, 이 RTTs라는 RTTm의 평균치와 같은 값이 있다면 이에 대한 표준 편차도 구할 수 있다. 이 표준 편차는 RTTd라고 하고 초기 값은 RTTd = RTTm /2, 이후 값은 RTTd = (1-b) RTTd + b |RTTs - RTTm| 이 된다.
여기서의 b값은 정하기 나름이지만, 일반적으로는 1/4로 많이 설정한다.

최종적으로 실제 타이머의 타임아웃값으로 적용되는 RTO값은 RTO = RTTs + 4RTTd 로 결정된다.

RTTs = 계속해서 업데이트 되어가는 값으로 지금까지의 데이터를 누적하여 얻은 최적화된 시간값
RTTm = 지금 Segment를 보낸 시간과 이에 대한 Ack를 받은 시간의 차이
RTTd = RTTs라는 평균으로부터의 RTTm의 표준편차
RTO = 실제 Retransmission timer에서 사용하는 Timeout 값

Examples

초기에는 Receiver의 위치를 모르기 때문에 충분히 멀리 있다고 가정하고 RTO 값을 크게 해서 보낸다. 처음 보낸 SYN에 대한 RTTmdl 1.50s니까 이를 반영해서 RTTs, RTTd를 도출하고 이걸로 RTO값을 만들어낸다.

이후 시행에서의 RTTm은 2.50s니까 다시 RTTs와 RTTd를 도출해내고 RTO값을 업데이트 시킨다.

Problem

그러나, 이런 문제 상황이 발생할 수 있다. Sender가 Segment를 보냈는데, 이 Segment가 인터넷 단에서 사라졌다. 이렇게 되면 RTO가 timeout되고 Sender가 Segment를 다시 보내게 된다. 이 상황에서 Ack가 들어왔다면, Sender는 이 Ack가 언제 보낸 Segment에 의한 것인지를 알 수 있을까?

Sender는 먼저 보낸 Segment가 사라졌는 지를 알 지 못한다. 이 Segment는 잘 도착했는데 Ack가 오는데 시간이 너무 걸려서 Timeout이 발생했을 수도 있기 때문이다. 나중에 보낸 Segment에 의한 Ack라고 확신할 수는 없다. 이럴 경우에는 Sender는 RTTm값을 어떻게 처리해야 할까? 앞선 Segment에 의한 것이라고 생각해야 할까? 아니면 최근에 보낸 Segment에 의한 것이라고 생각해야 할까?

이런 상황을 해결하기 위해서 나온 접근 방법이 Karn's algorithm이다. 이 방식은 위에서 말한 상황처럼 RTTm을 파악하기 곤란한 상황, 즉 RTO가 timeout되고 Segment를 재전송했을 때 돌아온 Ack로는 RTTm을 계산하여 RTTs나 RTTd를 업데이트 하지 않는다. 다른 값들은 그냥 그대로 두고 RTO값만 2배로 늘려 Segment를 재전송한다.

Ack가 들어온 이후로는 다시 정상적으로 RTTm을 계산해서 RTTs, RTTd, RTO를 업데이트 해간다.

Keepalive timer

이때, Receiver는 Sender가 일방적으로 TCP 연결을 끊은 것이기 때문에 이 연결이 끊어졌는 지를 알 방법이 없다. 인터넷은 Connectionless 방식으로 구동되기 때문이다.

Sender만 TCP 연결을 끊고 Receiver만 TCP 연결을 이어가고 있다면, 이 상황은 Half-open Connection Problem이라고 한다 했다. 이를 해결하기 위해서 Sender는 Receiver에게 주기적으로 더미 데이터를 보낸다.

TCP - Connection Oriented 에서 Half-Open Connection Problem에 대한 내용을 찾을 수 있다.

Receiver는 Sender부터 더미 데이터가 계속 들어오고 있다면 TCP 연결에 문제가 없다는 것을 알지만, Sender가 이 연결을 끊었다면 더미 데이터가 들어오지 않을 것이고, 이러한 이벤트를 Receiver가 눈치채고 Receiver도 연결을 끊어야 한다. 이때 사용되는 타이머가 Keepalive timer이다.

Keepalive timer는 Receiver에 Segment나 더미 데이터가 들어왔을 때부터 타이머를 구동시키고, 이 타이머가 timeout 되었다면, Sender가 TCP 연결을 끊었다고 생각해 Receiver도 TCP 연결을 끊어 종료시킨다.

Persistence timer

TCP로 데이터를 주고 받던 도중, Receiver가 자신의 Buffer에 남은 자리가 없어 Sender에게 Window size가 0이라는 Ack를 보내주었을 때를 가정해보자. 이때, Receiver Buffer에 자리가 생겨 Window size를 설정하여 Sender에게 Window size가 얼마라는 Ack를 보냈을 때, 이 Ack가 사라진다면 Sender는 Receiver로부터 Window size가 비어있다는 Ack를 받지 못하여 데이터를 더 보낼 수 없고, Receiver는 Ack를 보냈는데 Sender의 응답이 오지 않는 Deadlock 상황에 빠지게 된다.

이를 해결하기 위해 Sender는 Receiver로부터 Window size가 0이라는 Ack를 받은 시점부터 일정 시간 간격으로 Receiver에게 여전히 Window sizer가 0이냐고 물어보는 작업이 필요하다. 이때, 이 Sender가 보내는 Request의 시간 간격을 정해주는 것이 Persistence timer이다. 이 timer가 timeout 될 때마다 Sender는 Receiver에게 Window size가 여전히 0이냐는 Request를 보낸다.
이 Request Segment를 Probe Segment라고 한다.

Time-waited timer

Time-waited timer는 TCP Connection을 Terminate 시킬 때 동작하는 타이머이다. FIN에 대한 ACK를 받았을 때에 FIN이 손실된 상황에 의해 동일한 FIN을 보내줘야 하는 상황, FIN보다 먼저 보낸 패킷이 아직 다 들어오지 못했을 경우를 대비하는 등의 이유로 약간의 시간을 기다린다. 이때, Time-waited timer를 구동시켜 해당 시간만큼을 기다리게 된다. 이 시간은 보통 MSL(Maximum Segment Lifetime)의 2배로 설정된다.

Timestamp option

Timestamp option은 IP에서도 있었던 옵션이다. 내용은 비슷한데, TCP에서는 주로 RTT를 계산할 때 쓰이거나 Sequence Number가 중복되는 경우 이 SN을 구별하기 위해 사용한다.

먼저 다음과 같이 Segment를 보낸 시간을 Sender에서 기억하고 있는 것이 아니라 그냥 Segment에 Timestamp를 적어서 보내면, Receiver는 이를 받아 Ack를 보낼 것이다. 이때, Ack를 보내면서 해당하는 Segment에 적혀있던 Timestamp값을 Timestamp echo reply 필드에 담아 보내준다. Sender는 이를 받아 현재 Sender에서의 시간값인 Timestamp와 Ack 내에 있는 Timestamp echo reply값을 비교하여 RTT를 계산한다.

여기서 한 가지 의문점이 생긴다. Segment에 이렇게 Timestamp 값이 들어간다면, RTT를 계산할 때 Karn's algorithm을 굳이 안 써도 Sender가 이 Ack가 어디에서 기인된 것인지를 알 수 있으니까 RTT를 계산할 수 있는게 아닐까? 왜 Karn's algorithm과 같은 방법을 써서 번거롭게 할까?

사실 이 Timestamp 옵션은 성능을 떨어뜨릴 수 있기 때문에 평소에는 잘 사용하지 않는다. 그래서 위성통신과 같이 거리가 먼 통신의 경우에 특수하게 가끔 사용되기 때문에 일반적인 경우에서는 Karn's algorithm을 사용하는 것이다.

또한, 중복되는 Sequence number를 구별하기 위해 사용한다. 물론 Sequence number는 32bit로 표현되어 매우 크지만, 혹시 이 비트를 모두 사용했다면 다시 처음 0으로 Sequence number가 돌아오기 때문에 Sequence number를 구별해야 할 일이 생길 수 있다.

이를 PAWS라고 하는데, Sequence number가 겹친다는 것은 2^32 bit를 모두 사용하여 0부터 2^32 - 1 만큼의 번호를 다 쓰고 난 후에 다시 0이 나오는 이유로 발생한다. 이렇게 되면 이 0이 가장 처음 받은 0인지 아니면 두 번째로 들어온 0인지를 알 수 없기 때문에, 이 Sequence number를 따로 구별할 수 있어야 한다.

가장 쉬운 방법으로는 Segment의 id에 timestamp값을 포함하는 것이다. 예를 들어, Sequence number가 12001인 Segment가 두 개 있다고 하더라도 400:12001과 700:12001 이렇게 표현하면 이 12001이라는 Sequence number를 가지는 Segment가 언제 받은 Segment인지 구별할 수 있다.

QUIC (Quick UDP Internet Connections)

시간이 지나면서 TCP의 성능을 더 올리고 싶어졌다. 예를 들어, 기존 TCP를 사용하는 HTTPS 프로토콜에서는 TCP의 3-way handshaking과 TLS 키 교환, 즉 공개키 암호화 알고리즘을 이용해서 초기에 연결이 이루어지기 때문에 데이터의 왕복 과정인 Round Trip(RT)가 너무 많았다. 또한, TCP는 Receiver가 데이터를 순서대로 받아야 하기 때문에 누락된 Segment가 있다면 이 Segment를 받아 처리할 때까지 뒤에 오는 Segment들이 모두 대기해야 하는 Head of line blocking 문제가 생겼다.

이런 RT가 많아지고 HOL Blocking이 발생하면 사용자가 느끼는 웹 사이트에 대한 만족도가 떨어지게 되고, 이는 곧 서비스의 매출 하락으로 이어질 가능성이 매우 높다. 이러한 문제가 생겨 업데이트를 진행하려 했으나, TCP는 OS 커널 깊이 내장되어 있기 때문에 이 프로토콜을 업데이트 하려면 모든 장치들과 네트워크 장비들이 변경되어야 한다.

이런 문제를 해결하기 위해서 구글에서는 QUIC라는 프로토콜을 제안했다. TCP 역할을 Transport layer에서 하지 않고 UDP를 사용하여 이런 Control 기능들을 Application layer에서 동작하도록 바꾼 것이다. 이렇게 구현하니 Handshaking의 횟수가 줄어들게 되고, 데이터를 Stream 단위로 분리 전송이 가능하기 때문에 한 스트림에서 문제가 생겨도 다른 스트림에는 영향이 가지 않게 되었다. 또한, Application 수준에서 프로토콜을 구현했기 때문에 프로토콜의 수정과 배포가 빠르고 용이해졌다. QUIC는 HTTP/3 표준을 기반으로 한다.

실제로 크롬이나 유튜브 등에 적용되어 페이지 로드 시간과 영상 버퍼링 시간이 크게 줄어들게 되었다. 특정 ISP나 방화벽에서는 UDP 트래픽에 대해서는 비정상 트래픽으로 간주하고 차단하거나 일부 라우팅 장비들은 UDP 트래픽에 대한 QoS를 제공하지 않는 경우도 존재한다.

참고 사이트 : QUIC 프로토콜 | 구글 또 너야? (scalalang2)

profile
다함께 성장하는 개발자 세상을 꿈꾸는 MLOps 엔지니어입니다😁 작성 당시 제 생각의 흐름을 독자 모두가 공감하고 이해할 수 있게 적으려고 노력합니다. 조언이나 질문은 언제든 환영입니다!

0개의 댓글