TCP vs UDP

4 분 소요

Transport 계층에 위치한 TCP와 UDP를 비교해보겠다.

UDP

UDP는 비연결형 트랜스포트이다.

UDP는 transport 계층에 위치하기 때문에 송신 측에서 앱 프로세스로부터 메시지를 얻어서 네트워크 계층에 직접 넘겨주거나, 수신 측에서 네트워크 계층으로부터 도착한 메시지를 앱 프로세스로 넘겨주는 일을 해야 한다. 따라서 기본적으로 다중화와 역다중화 서비스를 제공한다.

또한 간단한 체크섬 기능을 이용한 오류 검사 기능을 가진다. 체크섬은 세그먼트가 출발지로부터 목적지로 이동 했을 때 UDP 세그먼트 안의 비트에 대한 변경사항이 있는지 검사하는 것이다. UDP는 오류 검사를 제공하지만 오류를 회복하기 위한 어떤 일도 하지 않는다.

TCP

TCP는 두 프로세스가 서로 “핸드세이크”를 해야하므로 연결지향형 트랜스포트이다. 따라서 데이터 전송을 보장하는 파라미터들을 각자 설정하기 위한 사전 세그먼트들을 보내야한다. 그런데 왜 2-way handshake가 아닌 3-way일까? 이는 송신자 수신자 모두 서로의 데이터 전송이 상대방에게 제대로 도달하는 지 확인할 수 있기 때문이다. 2-way의 경우는 송신 측만 데이터 전송이 확실하다고 알 수 있지 수신측은 ACK가 송신측에 제대로 도착하는 지 알 수 없다.

TCP는 IP의 비신뢰적인 best-effort 서비스에서 신뢰적인 데이터 전달 서비스를 제공한다. 이는 프로세스가 자신의 수신 버퍼로부터 읽은 데이터 스트림이 손상되지 않았으며 손실이나 중복이 없다는 것과 순서가 유지된다는 것을 보장한다. 그리고 흐름제어와 혼잡제어를 제공한다.

신뢰적인 데이터 전달

TCP는 손실이 없다는 것을 어떻게 보장할까? TCP는 체크섬, 순서번호, ACK 패킷, 타임아웃, 재전송을 이용하여 신뢰적인 데이터 전달을 보장한다. ACK 패킷은 수신측에서 데이터를 받았을 때 송신측에 데이터를 제대로 받았다는 확인 메시지이다. 이를 송신측이 받게 되면 송신측이 데이터가 수신측에 제대로 전달되었다는 것을 알 수 있다. 만약 수신측이 보낸 패킷이 중간에 loss가 발생하는 경우 타임아웃을 이용하면 된다. 일정 시간동안 송신측에 ACK가 도착하지 않으면 송신측에서 다시 데이터를 전송하는 것이다.

또 다른 사례로 ACK 패킷이 손실된 경우이다. 이 경우에는 수신측은 데이터를 정확히 받았으나 송신측은 그 여부를 파악하지 못하여 타임아웃이 발생한다. 그렇게 송신측에서 데이터를 재전송하며 수신측은 다시 데이터를 받게된다. 이때 중복되는 데이터를 받았으니 곧바로 다시 가장 마지막의 ACK를 전송한다.

타임아웃 주기 예측

타임아웃 길이는 어떻게 결정할까? 타임아웃은 송수신자 사이의 왕복 시간을 이용하여 결정한다. 이는 이전 RTT 값들로 추정된 추정 RTT 값과 새로 측정된 샘플 RTT 값을 이용하여 추정된 RTT 값을 계속해서 갱신한다. 권장되는 가중치는 0.125이며 식은 아래와 같다.

EstimatedRtt = 0.875 x EstimatedRTT + 0.125 x SampleRTT

RTT 예측 외에도 RTT의 변화율을 측정하는 것도 매우 유용하다. 이는 최근 편차와 기존 편차를 이용하여 계산하면 된다.

DevRTT = 0.75 x DevRTT + 0.25 x abs(SampleRTT - EstimatedRTT)

이제 위의 두 값을 가지고 타임아웃을 계산하면 된다. 분명히 주기는 EstimatedRTT보다 크거나 같아야하지만 그렇다고 이 값보다 너무 크면 세그먼트를 잃었을 때 TCP는 세그먼트의 즉각적인 전송을 하지 않게 된다. 그러므로 타임아웃 값은 EstimatedRTT에 약간 여유 값을 더한 값으로 설정하는 것이 바람직하다.

TimeoutInterval = EsitimatedRTT + 4 x DevRTT

흐름제어

TCP 연결의 각 종단에서 호스트들은 연결에 대한 개별 수신 버퍼를 설정한다. 앱이 버퍼에 도착한 데이터를 빠르게 읽지 않으면서 송신자가 점점 더 많은 데이터를 빠르게 전송한다면 수신자의 버퍼는 금방 오버플로가 발생할 것이다. 이와 같은 송신자가 수신자의 버퍼 오버플로우를 방지하기 위해서 TCP는 흐름제어 서비스를 제공한다.

TCP는 송신자가 수신 윈도우라는 변수를 유지하여 흐름제어를 제공한다. 수신측은 최근 수신한 버퍼 크기와 최근에 읽은 버퍼 크기, 총 버퍼 크기를 이용하여 여유 공간인 rwnd를 계산한다. 그리고 수신측이 송신측에서 데이터를 받고 ack를 보낼 때마다 rwnd 크기를 송신측에 보낸다. 송신측은 앞서 말한 rwnd 크기, 최근 전송한 버퍼 크기, Acked를 받은 버퍼 크기를 가지고 있는다. 이를 활용하여 rwnd를 초과하지 않도록 송신을 진행한다.

그런데 이 방법에는 사소한 기술적인 문제가 있다. 만약 수신 버퍼가 rwnd=0으로 가득 찼다고 가정하자. 송신측에 rwnd=0이라고 알리고 수신측이 더이상 송신측에 전달하는 것이 없다고 가정하자. 이때 수신측이 버퍼를 비웠음에도 송신측에서 데이터를 전송하지 않기 때문에 송신측은 버퍼가 비어있는 줄 알 수 없다. 이는 수신 윈도우가 0일 때 계속해서 송신측이 1바이트 데이터로 세그먼트를 계속해서 전송하도록 요구하면 해결할 수 있다.

혼잡제어

송신측의 데이터는 라우터를 거쳐가며 수신측에 전달된다. 이때 특정 라우터에 부하가 심하게 가해지면 해당 라우터는 본인에게 온 모든 데이터를 처리할 수 없게 된다. 이로인해 송신측은 데이터를 재전송하게 되고 결국 혼잡만 가중시켜 오버플로우 혹은 데이터 손실이 발생된다. 따라서 이러한 네트워크 혼잡을 없애기 위해 송신측에서 데이터를 보내는 속도를 강제로 줄이게 되는데 이를 혼잡제어라고 한다.

흐름제어는 송수신측 사이의 전송 속도를 다루는 반면, 혼잡제어는 라우터를 포함한 더 넓은 관점에서 전송 문제를 다룬다.

혼잡제어의 기본적인 원리는 다음과 같다. 혼잡하지 않은 상태까지 전송률을 천천히 증가시키다가 혼잡해지는 상황을 마주하면 급격하게 전송률을 줄이는 방법이다. 그렇다면 어떻게 혼잡하다고 파악할 수 있을까? 타임아웃 혹은 중복된 ACK를 수신하는 경우이다.

더 자세히 TCP 혼잡제어 알고리즘을 살펴보자. 혼잡제어 알고리즘에는 중요한 세 가지 구성요소들을 갖는다.

Slow start

패킷을 하나씩 보내면서 시작하고, 패킷이 문제없이 도착하면 각각의 ACK 패킷마다 window size를 1씩 늘려준다. 즉, 한 주기가 지나면 window size는 지수적으로 증가한다. 그렇다면 언제까지 지수적으로 증가할까? 먼저 혼잡 상황이 발생하면 cwnd값을 1로 하고 새로운 슬로 스타트를 시작한다. 혼잡이 검출 되었던 시점의 윈도우 값의 반 까지만 지수적으로 증가한다. 이때 혼잡 회피모드로 진입한다.

Congestion avoidance

혼잡 회피 상태로 들어가는 시점에서 cwnd의 값은 대략 혼잡이 마지막에 발견된 시점에서의 값의 반으로 된다. 이때는 윈도우 사이즈가 완만하게 1씩 증가된다.

Fast recovery

빠른 회복은 혼잡 상태가 되었을 때 window size를 1이 아닌 해당 시점의 값의 절반으로 줄이고 선형증가시키는 방법이다.

UDP 사용 이유?

그렇다면 신뢰성이 높은 TCP 대신 UDP를 왜 사용할까? 아래와 같은 이유들 때문이다.

  • 무슨 데이터를 언제 보낼지에 대해 앱 레벨에서 더 정교한 제어가 가능
    실시간 앱에서는 종종 최소 전송률을 요구하고, 지나치게 지연되는 세그먼트 전송을 원하지 않으며, 조금의 데이터 손실은 허용할 수 있으므로 이 경우 UDP가 적합하다.
  • 연결 설정이 없음
    3-way handshake가 없는 UDP는 형식적인 예비동작 없이 전송하기 때문에 연결 설정을 위한 어떤 지연도 없다. 이것은 DNS가 왜 TCP보다는 UDP에서 동작하는지에 대한 일반적인 이유이다.
  • 연결 상태가 없음
    연결상태가 없기 때문에 서버가 UDP에서 동작할 때 좀 더 많은 클라이언트를 수용할 수 있다.
  • 작은 패킷 헤더 오버헤드
    TCP가 세그먼트마다 20바이트의 헤더 오베헤드를 갖는 반면에 UDP는 단지 8바이트의 오버헤드를 가진다.

댓글남기기