Congestion (혼잡)
네트워크에서의 '혼잡' 은 너무 많은 source host (sender) 가 너무 많은 데이터를 너무 빠르게 전송해서 네트워크가 이를 처리할 수 없는 상황을 의미한다.
즉, 네트워크 트래픽이 몰려있는 상황, 고속도로에 차가 몰려서 차가 막히는 상황과 같다.
데이터가 몰리면 라우터 버퍼에는 패킷이 계속 쌓이니 queueing 딜레이가 증가하니 데이터 전송 시간이 증가할 수 있고, 심한 경우 라우터의 버퍼가 넘쳐서 패킷 Loss가 발생할 수 있다.
이때 혼잡 제어와 흐름 제어를 구분하면
흐름 제어는 1:1 상황(TCP, UDP)에서 너무 많은 데이터를 보내는 것을 제어하여 receiver 의 버퍼가 넘치지 않게 조절하는 것을 말하고
혼잡 제어는 다수의 sender가 데이터를 너무 많이, 빠르게 보내는 상황을 제어하여 네트워크 경로 중간에 있는 라우터의 버퍼에서 문제가 발생하지 않도록 조절하는 것을 말한다.
Congestion Control
사실 혼잡 제어에 대해 TCP는 특별하게 조치를 취하는 것이 없다.
즉, 혼잡이 발생한 위치로부터 어떤 explicit 피드백을 받아서, 그에 대해 즉각적인 조치를 취한다거나 하지 않는다.
그저 세그먼트가 느리게 전달되거나 Loss가 발생하면 그에 대해 재전송하고, 중간에 혼잡이 발생하지 않았을까 하고 추측해서 delay, loss 정보를 토대로 sending rate를 줄인다.
물론 explicit 피드백을 받아서 처리하는 것도 가능하다. (TCP 헤더에도 이를 위한 부분이 구현되어 있다.)
하지만 대부분의 경우 사용하지 않기 때문에 TCP는 excplicit Congestion Control은 하지 않는다고 본다.
만약 라우터에서 explicit하게 알린다면 위와 같은 그림의 형태로 알린다.
널리 쓰이지는 않지만, explicit하게 처리하는 방법도 뒤에 정리한다.
대표적인 방법으로는 TCP ECN (Excplicit Congestion Notification), ATM (Asynchronous Transfer Mode, 데이터링크 계층에 속하는 기술), DECbit protocols (Digital Ecument Corp라는 회사에서 사용하던 프로토콜) 와 같은 방법이 있다.
TCP Congestion Control
AIMD
이제 TCP에서 사용하는 혼잡 제어 방법을 살펴보자.
TCP에서 혼잡을 detect 하는 건 sender 쪽이다.
데이터를 보냈는데, 잘 받았다는 응답이 없거나 느리다면 혼잡을 추측할 수 있기 때문이다.
반면 라우터는 자신을 거쳐가는 패킷이 congestion 때문에 delay, loss가 되면 즉시 알 수 있기 때문에 혼잡을 제일 빠르게 알 수 있는 장치다.
그래서 만약 라우터가 혼잡 정보를 알려준다면 좋겠지만, 현재 TCP는 이 방법을 사용하지 않는다.
이 방법은 라우터가 추가적인 기능을 수행해야하므로 부담이 늘기 때문이다.
따라서 sender는 혼잡을 추측하게 되면, 그때부터 자신이 데이터를 보내는 비율을 조절해서 혼잡을 피하고자한다.
TCP가 사용하는 방법은 간단하게 아래와 같다.
1. 만약 패킷 Loss가 발생하지 않는다면, 전송비율을 계속해서 증가시킨다.
2. 만약 패킷 Loss가 발생한다면, 전송 비율을 감소시킨다.
이 과정을 계속 반복하면서 데이터 경로상 최적의 bandwidth를 찾는다.
이때 사용할 수 있는 알고리즘이 여러가지가 있는데, 대표적인 알고리즘이 AIMD(Additive Increase Multiplicative Decrease) 이다.
이름 그대로, 증가시킬때는 매 RTT마다 1 MSS씩 전송 비율을 증가시키다가, 만약 Loss 이벤트가 발생하면 전송 비율을 반으로 줄이는 간단한 방법을 사용한다.
구체적으로는,
만약 세번째 Duplicated ACK를 받는다면 Loss가 발생했다고 간주하고 전송률을 반으로 줄인 뒤 재전송한다. (fast retransmit) 이때는 반만 줄여도 되는 이유가, 일단 중복된 ACK가 돌아왔다는 것 자체는 네트워크가 막혀있지 않다는 걸 의미하므로 굳이 1부터 다시 쌓을 필요가 없기 때문이다. (TCP Reno 버전부터 구현)
만약 timeout에 의해 Loss가 발생했음을 알아차린다면, 1 MSS 까지 전송률을 줄였다가 다시 천천히 증가시킨다. (TCP Tahoe 버전부터 구현)
이 방법을 사용했을 때, 전송률 그래프는 위와 같다.
이 과정을 통해 문제가 발생하지 않으면서 최대한 세그먼트를 보낼 수 있는 최적의 비율(대역폭)을 찾는다.
(이를 톱날처럼 생겼다고 해서 AIMD sawtooth 라고도 한다.)
Congestion 은 다수의 TCP Sender가 연관된 문제인만큼 AIMD는 모든 TCP를 사용하는 호스트가 사용하는 알고리즘이다. 그래서 혼잡이 발생하면 나만 줄이는게 아니라 모든 TCP를 사용하는 sender가 보내는 비율을 줄인다.
하지만 당연하게도 모든 TCP 호스트가 동시에 동일한 Congestion Control 알고리즘의 동일 부분을 실행하고 있을 수 없다.
각각의 host가 느끼기에 뭔가 congestion이 발생한 것 같다 싶으면 알아서 AIMD와 같은 알고리즘을 적용할 것이기에 각각이 적용되는 시점은 들쭉날쭉 할 것이다.
AIMD는 이렇게 각각의 호스트가 congestion 을 제어하는 시점이 제각각일 때 사용하기 좋은 방법으로 알려져있다.
각각의 호스트가 자신의 상황에맞게 AIMD를 썼을 때 결과적으로 모든 TCP host에 대해 혼잡 제어가 잘 되었다고 한다.
Detail
이제 TCP가 사용하는 혼잡 제어 방법을 더 자세하게 살펴보자.
사실 TCP의 혼잡 제어는 흐름 제어와 똑같이, TCP의 센더와 리시버가 사용하는 버퍼의 빈 공간에 좌우된다.
이때 혼잡 제어와 관련하여 부를 때 그 윈도우를 cwnd (congestion window) 라고 부를 뿐이다.
Sender의 cwnd는 전에 살펴봤던 윈도우와 그 형태가 동일하다.
녹색은 이미 ACK를 수신해서 기억할 필요가 없는 부분
노란색은 전송했으나 ACK를 받지 못했고, 재전송을 해야할 수도 있기 때문에 보관하는 부분
파란색은 어플리케이션이 데이터를 보내주지 않았거나, 데이터는 보냈는데 아직 세그먼트로 만들지 않은 부분이다.
이 그림을 토대로 볼 때, TCP sender는 전송량을 다음과 같이 제한한다.
사실 당연한 이야기이긴 하다. cwnd의 크기보다 위 수식의 값은 같거나 작을 수 밖에 없다.
추가로 보낼 수 있는 부분은 파란색 영역까지가 최대이기 떄문이다.
TCP는 응답이 오는 딜레이와 loss 여부에 따라 cwnd의 크기를 동적으로 조절한다.
(TCP는 운영체제의 영역이기 때문에 실제로는 운영체제가 조절한다.)
그렇다면 혼잡 상황에서 TCP가 데이터를 전송하는 비율은 아래와 같이 대략적으로 계산할 수 있다.
1 RTT에 cwnd bytes 크기의 데이터만큼은 전송할 수 있다는 의미이다.
TCP는 일종의 신사협정으로 모든 호스트가 따르므로, 혼잡이 발생했을 때, 각 TCP 호스트는 1 RTT에 이 만큼의 데이터를 주고 받을 수 있다.
따라서 이 값은 일종의 성능을 나타내는 지표이다.
Slow Start
TCP의 또다른 혼잡 제어 알고리즘인 slow start를 살펴보자.
(혼잡 제어를 하기 위한 기법은 AIMD, slow start, congestion avoidance 와 같이 여러가지가 있다. 이것들을 종합해서 사용하는 것이 TCP의 혼잡 제어 기법이다.)
이 기법은 이름 그대로 처음 연결을 설정했을 때, cwnd를 1 MSS로 설정해두었다가 매 RTT마다 cwnd 크기를 2배씩 증가시키는 기법이다. (전송 단위가 세그먼트이므로 제일 작은 값은 1 MSS 이다. 세그먼트는 잘라서 보낼 수 없다!)
cwnd가 2배가 되었다는 말은, 최대 in-flight segment 수가 2배로 증가한다는 것을 의미한다.
이를 그림으로 나타내면 위와 같다.
처음에는 cwnd의 크기가 1MSS이므로 1개의 세그먼트를 보낸다.
모두에 대해 ACK가 도착하면 그 다음 RTT에는 cwnd의 크기를 2배로 늘려 2MSS가 되고,
2개의 세그먼트를 1RTT에 보낸다.
2개에 대해 ACK가 도착하면, cwnd의 크기가 다시 2배 증가하여 4개의 세그먼트를 보낼 수 있게 된다.
정확하게 설명하면 ACK를 수신할 때마다 cwnd의 크기를 1씩 증가시키기 때문에
1RTT가 지나면 결과적으로 cwnd의 크기가 2배로 증가한다.
그래서 4개 세그먼트를 보내는 부분의 그림을 자세히보면 ACK가 도착할 때마다 2개씩 보내는 것을 볼 수 있다.
2MSS를 보냈을 때, 첫번째 ACK를 받으면 cwnd 의 크기는 3이 되고, 그 두번째 ACK를 받으면 cwnd의 크기는 4가 되는 방식이다.
얼핏보면 뭔가 느린 것 같지만 이름에 속으면 안된다.
이 방식은 매 시간이 지날 때마다 전송량을 2배씩 증가시키기 때문에 '기하급수적으로' 그 전송 속도가 증가한다.
slow start 방식도 결국 원하는 것은 최적의 전송 비율(최적의 대역폭)을 가능한 빠르게 찾는 것이 목표다.
이렇게 증가하다보면 언젠가는 문제가 생기면 AIMD에서 그랬던 것처럼 비율을 줄일 것이다.
이제 비율을 줄이는 기법인 congestion avoidance를 다음으로 살펴보자.
Congestion Avoidance
Congestion Avoidance도 TCP의 혼잡 제어 알고리즘 중 하나이다.
이름 그대로 혼잡을 피하는 것이 목표이기 때문에 congestion avoidance는 천천히 증가한다.
이 그림은 초기에 slow start 방식으로 기하급수적으로 증가하다가 ssthresh 라는 기준점을 넘어서면 congestion avoidance 방식으로 천천히 증가하는 과정을 보여준다.
가로축은 RTT, 세로축은 sender의 congestion window (mss) 크기이다.
ssthresh 이후부터는 기하급수적으로 증가하는게 아니라 linear하게 증가한다.
그 이유는 그림에서 X 표시 친 곳이 문제가 생기는 지점이라고 판단하고 있기 때문에 천천히 증가시키는 것이다.
이때 증가하는 방법은 AIMD의 AI 방식을 따라서 증가시킨다.
이때 X 지점에서 정말 패킷 Loss와 같은 혼잡 상황으로 추측되는 현상이 발생했다면
TCP Tahoe 버전에서는 cwnd를 1부터 다시 증가시키고, TCP Reno 버전에서는 3 duplicated ACK를 받았다면 cwnd를 1/2로 감소시킨 부분부터 다시 증가시킨다.
이때 ssthresh 는 congestion 이 발생한 그 cwnd 값의 1/2로 설정된다.
그렇다면 TCP Tahoe 버전에서는 다시 slow start 부터 시작해서 급격하게 증가시키다가 AI 방식으로 천천히 증가시킬 것이고, TCP Reno에서는 시작점이 ssthresh 부분이므로 처음부터 AI 방식으로 천천히 증가시킬 것이다.
TCP Congestion Control FSM
이제 위에서 설명한 내용을 FSM으로 표현해보자.
먼저 처음에는 slow start 상태부터 시작한다.
처음 cwnd 값은 1MSS 이고, ssthresh 값은 64kb 라고 하자.
dupACK 가 3번이면 재전송을 해야하므로 그 카운트 변수도 초기화해둔다.
만약 duplicate ACK 이벤트가 발생하면, dup ACK 카운트를 증가시킨다.
만약 새로운 ACK가 왔다면 cwnd 값을 1 증가시킨다. (slow start 상태이므로)
duplicate ACK 카운트값은 초기화하고, 보낼 수 있다면 새로운 세그먼트를 전송한다.
만약 timeout 이 발생했다면 ssthresh 값을 cwnd값의 절반으로 줄이고, cwnd는 1MSS로 초기화한다. (3 duplicate ACK가 아니므로 1로 세팅하는 것이다.)
그리고 duplicate ACK 카운트도 초기화한다.
이때 계속 ACK를 받아서 cwnd값이 ssthresh 값 이상이 되었다면 congestion avoidance 상태로 천이한다.
만약 이 상태에서 duplicate ACK를 받으면 카운트만 증가시킨다.
새로운 ACK를 받으면 cwnd는 1 RTT에 1 MSS 씩 증가하도록 설정하면 된다.
그래서 1 RTT에 1MSS 씩 증가하도록 하게 하기 위해서 MSS * (MSS / cwnd) 를 곱한다.
만약 현재 cwnd의 값이 4라면, slow start 에서 4개를 한꺼번에 보내고 ACK를 4개를 받을 것이다.
정상적인 상황이라면 각 ACK마다 cwnd 값을 1씩 증가시켜서 cwnd 값은 8이 될 것이다.
congestion avoidance 에서는 ACK를 받을 때마다 1/4 씩 증가시켜서 모든 ACK를 다 받으면 (1RTT 지남) 그때 cwnd 값을 1 증가시키는 것이다.
만약 duplicated ACK를 받으면 카운트를 증가시킨다.
만약 timeout 이 발생하면 그때는 slow start 상태로 천이한 뒤, 다시 1MSS부터 시작해서 증가시킨다.
다른 값을 세팅하는 것은 slow start 때와 같은 action이다.
만약 dupACK 카운트가 3이 되어서 fast recovery 상태로 간다고 해보자.
이때는 ssthresh 값이 cwnd/2로 감소하고, cwnd 값은 ssthresh + 3 이 된다.
일단 ACK를 3개를 받기는 했기 때문이다.
이때 여전히 duplicate ACK가 들어온다면 cwnd는 증가시키면서 같은 상태에 머무르며 세그먼트를 재전송할 것이다.
cwnd를 증가시키는 이유는, 비록 duplicate ACK 여도 일단 ACK가 온다는 것은 네트워크 상황이 나쁘지 않다고 생각할 수 있기 때문에 증가시킨다.
(그렇다면 왜 slow start, congestion avoidance 상태에서는 바로 cwnd 값을 안늘리고 dupACK 가 3개 왔을 때 그제서야 늘리는 걸까?)
만약 이 상황에서 새로운 ACK가 잘 들어와서 Loss가 해결이 되었다면 cwnd 값은 ssthresh 값으로 돌아가고, dupACK 카운트는 초기화되며, ssthreash 값으로 cwnd를 설정하여 다시 congestion avoidance 단계에서부터 천천히 linear하게 증가한다.
이때 cwnd 값을 다시 ssthresh로 설정하는 이유는, congestion avoidance가 cwnd == ssthresh 일 때 발생하는 것으로 보고 맞춰주기 위함이다.
만약 fast recovery 상태에서 재전송을 했음에도 불구하고 timeout이 발생했다면 slow start 로 돌아가서 다시 1 MSS부터 시작하여 재전송한다.
TCP CUBIC
네트워크 혼잡은 언제든지 일어날 수 있는 상황이다.
네트워크 혼잡이 절대 발생하지 않게 만드는 것은 비용이 너무 많이 든다. (대역폭을 그냥 무진장 키운다든가..)
따라서 현실적으로 네트워크 혼잡이 발생할 수 있는 현 상황에서 최선의 방법을 찾는 것이 좋은데, 이론적으로 생각했을 때 제일 좋은 방법은 혼잡이 발생하게 되는 상황 직전까지 네트워크를 최대한 활용하는 것이 좋다.
혼잡을 피한다고 너무 보수적으로 데이터를 조금씩 보내지 않고, 그렇다고 너무 데이터를 많이 보내서 혼잡도 경험하지 않는 그 중간점을 찾고자 하는 것이 혼잡 제어의 핵심이다.
그래서 그 방법으로 AIMD를 살펴보았었다.
처음에는 slow start로 공격적으로 증가시키다가 스레드홀드를 만나면 그때부터는 AIMD방식으로 천천히 증가시키고, 혼잡이 발생한 것으로 예상되면 윈도우 크기를 반으로 줄였다가 다시 증가시키는 방법이다.
CUBIC은 AIMD 에서 증가를 시킬 때 너무 느리게 증가하는 것 같은데, 더 빠르게 증가시킬 수 없을지 고민하는 데에서 시작되었다.
CUBIC은 말 그대로 '3차' 라는 뜻이다.
적절한 대역폭을 찾기 위해 linear하게 느리게 탐색했다가 확 떨어지고, 느리게 올라가다가 확 떨어지면서 찾는게 아니라 cubic 3차 곡선의 형태로 빠르게 올라갔다가 찾아보자는 것이다.
이 아이디어는 congestion 을 유발하는 병목 링크가 그렇게 변하지 않을 것이라는 가정에서 시작한다.
즉, 그림에서 Wmax 위치에 가면 항상 loss가 발생했고, 그게 거의 변하지 않을 것이라는 가정에서 시작한다.
이 가정이 성립한다면 어디에서 문제가 생기는지를 아는데 굳이 느리게 linear하게 갈 이유가 없다고 생각할 수 있다.
따라서 CUBIC의 아이디어는 이렇게 Wmax에 도달할 때까지 빠르게 올라갔다가 Wmax 근처에 도착하면 속도를 늦추는, 마치 3차함수 곡선과 비슷한 모양으로 탐색하자는 아이디어를 제시한다.
기존 TCP와 다르게 TCP CUBIC은 Wmax 도착하는 시점을 K 라고 할 때, K 이전에는 급격하게 증가하다가 K에 가까워지면 점점 느리게 증가하도록 한다.
이런 CUBIC방식은 리눅스에서 기본적으로 채택하고 있다.
TCP와 Bottleneck Link
위에서 지금까지 살펴본 AIMD, CUBIC 모두 결국은 네트워크 혼잡이 발생하는 bottleeneck link를 빠르게 찾기 위한 알고리즘이다.
만약 그림과 같은 상황의 네트워크가 존재한다고 해보자.
빨간색 표시한 부분이 Bottleneck Link이다.
그렇다면 TCP가 혼잡을 피하기 위해서는 그림에 표시한 저 link를 문제 없이 지날 수 있는 최적의 bandwidth를 빠르게 찾는 것이다.
이 bottleneck link를 기준으로 해석을 해보자는 것이다.
만약 source 에서 destination으로 갈 때 어차피 저 link를 지날 수 밖에 없다고 한다면,
지금 상황에서는 보내는 비율을 아무리 늘려봤자 성능 향상에 도움이 안된다.
라우터의 큐에는 계속 패킷이 대기하고 있을 것이기 때문이다.
따라서 sending rate를 늘리면 측정되는 RTT만 계속 증가할 것이다.
그래서 이때는 목표를 조금 다르게 잡는다.
이 병목 링크 하나만 생각을 했을 때, 이 병목 링크가 넘치지 않을 정도로, 하지만 거의 가득차도록 유지해서 데이터를 보내자는 것이다.
이게 할 수 있는 성능상 최선인 것이다.
그래서 구글에서 이걸 찾기 위해 BBR(Bottleneck Bandwidth and Round-Trip propagation time) 이라는 delay 기반의 혼잡 제어 아이디어를 제안했다.
(그리고 실제로 구글의 백본 서버, 콘텐츠 네트워크에는 적용되어있다.)
이 아이디어는 RTT가 결국 혼잡 정도를 나타내는 척도가 아닐까 라는 생각에서 출발했다.
혼잡 정도가 심해질 수록 체감 RTT도 증가할 것이니 합리적인 생각임을 알 수 있다.
BBR은 다음과 같이 동작한다.
먼저 RTT min 을 계산한다.
즉, 두 컴퓨터 사이의 RTT를 측정하는데, 측정된 값 중 제일 작은 값을 계산한다.
이 상황은 혼잡이 없는, 가장 바람직한 상황에서의 RTT이다.
이 RTT min 값을 이용하면, 혼잡이 없을 때 우리가 달성할 수 있는 throughput을 알 수 있다.
그 값은 cwnd / RTT min 으로 구한다. (가장 빠른 RTT 시간에서 한번에 보낼 수 있는 MSS 수)
내가 마지막으로 보낸 RTT 시간동안 보낸 바이트 수를, 그때 측정했던 RTT로 나누면 현재 측정한 스루풋이 된다.
이 값과 최적의 스루풋을 비교했을 때, 그 정도가 매우 가깝다면 네트워크 상황이 좋다고 추측할 수 있다.
그래서 이 때는 전송률을 조금씩 늘린다.
그런데 만약 측정한 값이 최적의 상황에서 계산한 값보다 훨씬 더 작은 값이 나왔다면 네트워크가 혼잡한 상황이라는 것을 알 수 있고, 이때는 보내는 비율을 선제적으로 linear하게 줄인다.
즉, 요약하면 매번 RTT를 측정하고, 이 측정한 RTT를 최상의 상황에서 측정했던 RTT와 비교하면서 혼잡 정도를 파악해서 선제적으로 전송량을 조절하자는 아이디어다.
이렇게 선제적으로 조절함으로써 Loss가 발생하는 최악의 상황은 피하면서 데이터를 최대한 많이 보내고자 한다.
ECN (Explicit Congestion Notification)
지금까지 정리한 혼잡 제어방식은 모두 TCP가 채택하고 있는 implicit 혼잡 제어 방식이다.
혼잡 상황을 정확하게 알고있는 라우터가 알려주는 정보가 아니라, end-end 시스템이 나름대로 네트워크 상황을 추측해서 추측한 정보를 기반으로 혼잡을 제어한다.
이번에는 자주 사용되지는 않지만 라우터가 직접 혼잡 정도를 알려주는 ECN 방식을 간단하게 정리해본다.
TCP 세그먼트는 IP의 데이터로서 들어가므로, 3계층 장비인 라우터는 세그먼트를 건들 수 없다.
따라서 라우터는 혼잡상황이 발생한 경우, 이 정보를 IP 헤더에 마킹해서 목적지로 보내준다.
이때 사용하는 IP 헤더의 필드가 ToS (Time of Service) 라는 2bit 필드가 있다.
이 필드를 마킹해서 라우터가 목적지 호스트에게 혼잡상황이 발생했다는 것을 명시적으로 알려줄 수 있다.
(소스에게 보내지는 않는다. 그것은 라우터가 기존에 하는 일 (소스-> 목적지로 전달) 외에 추가로 하는 일이므로 오버헤드이기 때문이다.)
목적지 host는 IP헤더에서 ToS 필드가 마킹된 것을 보고, 지금 이 패킷이 혼잡상태의 라우터를 거쳐서 왔다는 것을 알 수 있고, 이 내용을 ACK를 보내면서 최종적으로 Source host에게 알려준다. 이때는 TCP 세그먼트의 C, E 플래그를 통해 혼잡 상황을 알려줄 수 있다.
따라서 Explicit Congestion Notification을 하기 위해서는 3계층과 4계층 모두의 도움이 있어야 한다.
IP 헤더에는 ECN 이라는 필드가 있는데, 이 필드를 10 으로 세팅을 하면, 만약에 혼잡을 경험했을 때 11로 세팅해달라, 마킹해달라는 의미이다. 이 필드는 10 또는 01로 마킹할 수도 있다.
만약 이 필드가 11로 마킹이되면 리시버는 이를 보고 혼잡을 경험한 라우터에 의해 마킹된 채 도착했다는 것을 알 수 있고, TCP 센더에게 이를 알릴 때는 ACK에 ECE (Explicit Congestion Experience) bit를 세팅해서 보내면 된다.
하지만 계속 말했듯, 이 방식은 잘 쓰이지 않고, TCP는 간접적으로 이를 확인하는 방법을 쓰고 있다.
라우터에게 부담이 되기 때문이다.
TCP fairness
마지막으로 혼잡과 관련된 주제로서 TCP fairness, 공정성에 대해 정리한다.
만약 TCP 링크의 대역폭을 n 개의 TCP connection 들이 나누어 사용한다면, 과연 이를 똑같이 공평하게 나누어 사용하는가에 대한 이야기이다.
그래서 만약 그림과 같이 2개의 TCP connection 이 R의 capacity를 갖는 병목 라우터를 동시에 지날 때,
각각 R/2 씩 사용하는지, 만약 k개의 연결이라면 R / K 개씩 공평하게 사용하는지 생각해본다.
TCP fairness를 달성하고자 한다면, 이상적으로는 R / K 개 씩 공평하게 사용하는 것이 바람직할 것이다.
한번 단순하게 2개의 TCP connection이 R 대역폭의 같은 링크를 나눠서 사용한다고 생각해보자.
만약 둘이 공평하게 사용하려면 컨넥션이 2개니까 R/2 만큼 나눠서 사용하는 것이 바람직할 것이다. (그래프의 회색 점선)
그래프에서 X축은 connection 1 의 throughput, y 축은 connection 2 의 throughput 이라고 하면, 두 throughput이 같아지는 지점이 회색 점선 지점일 것이다.
따라서 그 점선 지점대로 bandwidth를 나눠서 사용하면 이것을 fair하다고 말할 수 있을 것이다.
그래서 한번 AIMD만 사용한다고 가정해보자. (congestion avoidance에 진입한 상황)
그럼 초기에는 이 빨간색 점에서 시작한다고 했을 때 (두 connection이 fair하지 않게 대역폭을 사용하고 있는 상황)
현재 단계에서는 AIMD 단계이므로 Connection 1 도, Conneciton 2도 모두 cwnd 크기를 늘리면서 점점 많은 대역폭을 사용할 것이다.
그러면 회색 점선과 평행하게 throughput 의 합이 증가한다.
그러다가 파란색 선 (두 connection이 합쳐서 최대 사용 가능한 임계치) 을 넘기는 순간, 둘다 cwnd의 크기가 반으로 줄어들 것이다.
이때 감소하는 방법은 문제가 발생한 화살표 끝 지점과 원점을 연결한 뒤, 그 중간 지점까지 감소하도록 하면 된다.
Connection 2 입장에서도 절반으로 감소, Connection 1 입장에서도 절반으로 감소한 상태다.
다시 회색 점선과 평행하게 점점 증가하다가 임계치를 넘으면 절반으로 감소한다.
이렇게 감소하는 과정을 계속 반복하면 결국에는 회색 점선에 점점 가까워지게 된다.
물론 이를 위해서는 두 connection의 RTT 가 동일하고, 세션의 개수 역시 똑같아야 하며, 각 세션이 항상 동일한 단계에서 동작하고 있다는 극단적인 가정을 해야 한다.
아무튼 이런 가정을 한다면 TCP는 fair하다고 이야기할 수 있다.
그렇다면 모든 인터넷 어플리케이션은 fair 하도록 만드는 것이 가능할까?
이건 불가능하다.
일단, 만약 TCP의 혼잡 제어에 의한 throughput 제약을 받고 싶지 않다면 UDP를 사용하면 된다.
혼잡 상황이 발생했더라도 이기적으로 데이터를 많이 보내고 싶다면 말이다.
그래서 멀티미디어 어플리케이션 중에는 이런 이유로 UDP를 사용하는 경우도 꽤 많다.
하지만 물론 이렇게 했을 땐 신뢰성 보장이 안되니 이를 감내하거나 어플리케이션 계층에서 직접 구현해야한다.
그런데 사실 오디오, 비디오를 일정한 rate로 전송하면서 약간의 패킷 손실을 감내해도 된다면 굳이 신뢰성을 보장받을 필요가 없으니 그냥 UDP를 그대로 사용하는 경우도 있다.
아니면 TCP는 그대로 쓰면서도 혼잡 제어의 영향은 받고싶지 않을 수도 있다.
그럴 때는 우리가 직접 TCP를 짜면 된다.
기존 TCP를 사용하는 다른 어플리케이션과 호환이 되도록 나머지 부분은 그에 맞춰 TCP를 설계하되, 혼잡 제어부분만 빼는 것이다.
그러면 다른 어플리케이션과 통신하면서도 혼잡제어의 영향을 받지 않고 통신할 수도 있다.
이제 이렇게 되면 이 프로토콜은 TCP라고 부르기 어렵지만 아무튼 TCP를 사용하는 어플리케이션과 혼잡제어 영향 없이 통신할 수 있다.
예를 들면, 어떤 어플리케이션의 서버와 클라이언트를 직접 짜는데, 이떄 TCP에 해당하는 부분을 혼잡 제어 빼고 그대로 작성하는 것이다.
그러면 다른 TCP와 연동할 때 문제가 되는 부분이 있을지 몰라도, 내가 짠 클라이언트와 내가 짠 서버가 통신하는데에는 아무 문제가 없고, 혼잡 제어의 영향도 받지 않으니 공격적으로 데이터를 주고받도록 구현할 수도 있다.
이게 가능한 건 어차피 3계층에서는 세그먼트를 패킷의 데이터로 여기기 때문에 어떤 형식의 데이터가 들어가는지는 따지지 않기 때문에 가능한 것이다.
물론 TCP라는 복잡한 프로토콜과 비슷하게 동작하는 프로토콜을, 안정적이면서 성능을 내도록 직접 구현하는 것은 쉽지 않다.
아니면 표준 TCP를 사용하면서 다른 꼼수를 쓸 수도 있다.
바로 어플리케이션이 통신할 때 여러개의 connection (parallel connection) 을 여는 것이다.
만약 기존에 9개의 연결이 있는 R 대역폭의 링크가 있을 때, 하나의 connection으로 새로운 앱이 참가한다면 이 앱은 R/10의 대역폭을 사용할 수 있지만, 만약 11개의 connection 으로 참가한다면 거의 R/2 의 대역폭을 혼자 사용할 수 있다.
이런 꼼수가 가능한 이유는 HTTP는 stateless라서 서버가 자신에게 연결된 동일 클라이언트의 connection 개수를 카운트하지 않기 때문에 가능하다.
이걸 금지하려면 서버에 추가적인 오버헤드를 가하면 막을수는 있다.
(교수님이 하신 말씀, 현업에 나가면 TCP, UDP를 기계적으로 구분해서 사용하려고 하지 말고, 융통성을 갖고 TCP를 쓰되 성능을 높일 수 있는 방법을 고민해서 쳐낼 수 있다면 쳐내는 것도 고려하고.. 아니면 UDP로 가되 필요한 부분은 직접 구현하는 등 융통성을 가지라고 하신다.)
'CS > 컴퓨터 네트워크' 카테고리의 다른 글
[컴퓨터 네트워크] 25. Network Layer (1) : 개요 (0) | 2024.06.02 |
---|---|
[컴퓨터 네트워크] 24. Transport Layer (10) : TCP 기능의 진화 (1) | 2024.05.31 |
[컴퓨터 네트워크] 22. Transport Layer (8) : TCP 연결 설정 (3-way handshake) (0) | 2024.05.28 |
[컴퓨터 네트워크] 21. Transport Layer (7) : TCP Flow Control (흐름 제어) (0) | 2024.05.28 |
[컴퓨터 네트워크] 20. Transport Layer (6) : TCP (0) | 2024.05.27 |