어쩌다보니, 오늘 처음으로…(정말로 처음으로!!!) TCP_CORK라는 옵션에 대해서 찾아보게 되었습니다.(아니 이게 무엇이오 여러분!!!)
TCP_CORK라는 옵션을 설명하기 전에 먼저 TCP_NODELAY라는 옵션이 있습니다. 원래 데이터 전송의 효율성을 취하기 위해서 기본적으로 TCP 전송에 Nagle이라는 알고리즘이 적용되어 있습니다. 대용량 파일을 보낼 때는 유리하지만, 짧은 길이의 데이터를 보낼때는 사실 유용하지 않습니다. 네트웍 관련 서비스를 만들다 보면, 뭔가 응답이, 아무이유 없이, 아주 늦게 가는 케이스를 만나게 되는 경우가 있는데, 실제로, Nagle 알고리즘의 영향을 받아서 늦게 가는 경우가 종종 있습니다.
그러면 먼저 Nagle 알고리즘에 대해서 알아보면, 그냥 어느정도 데이터가 쌓일 때 까지 패킷을 보내지 않고 기다려 놓다가… 일정 사이즈가 되면 보내겠다라는 알고리즘입니다. 동네 버스가, 사람이 적을 때는 출발하지 않고, 몇명 와야 출발하는 것 처럼…(판교역 앞에 계시면 이런 경우를 많이 보시게 됩니다.)
이렇게 모아서 보내면 효율은 좋지만, 먼저 버스에 탄 사람은 인원 수가 모일때까지 가지 못하고, 기다려야 하는 단점이 있습니다. TCP 전송에서도 이게 그대로 발생합니다. 작은 패킷을 보내면 다른 패킷이 추가되어서 특정 사이즈가 되기 전까지는 전달이 되지 않습니다.(정확히는 send timeout 이 되면 전송됩니다.)
예전에 서비스를 운영하다보면, 마지막 2바이트가 몇초 뒤에 전송되어서 항상 문제가 되는 경우가 발생한 적이 있는데, 결국 해당 서비스는 TCP_NODELAY를 적용함으로써 해결했습니다.(또다른 문제의 시작일수도?)
앞에 Nagle을 이렇게 설명한 것을 잘생각해보면 DELAY가 생기는 거고, TCP_NODELAY는 바로 이 Nagle 알고리즘을 끄는 것입니다. 즉 패킷이 들어오면 바로바로 전송하는 거죠. 사실 여기까지가 제가 아는 Nagle 알고리즘이었습니다. 그런데 갑자기 TCP_CORK 가 딱!!!, TCP_CORK 는 Nagle과 유사한 알고리즘(?) 입니다.
https://stackoverflow.com/questions/22124098/is-there-any-significant-difference-between-tcp-cork-and-tcp-nodelay-in-this-use 해당 링크가 잘 설명이 되어 있는데, 요약하면 Nagle은 TCP_CORK의 약화버전이고, Nagle은 ACK를 체크하지만, TCP_CORK는 사이즈만 본다? 라는 뭔가 설명이 있는데…(그렇습니다. 저는 영어가…)
여기서 ACK는 TCP에서도 패킷을 보내고 나면 거기에 대한 ACK를 받게 됩니다. 혼잡제어나, 재전송이나… 그리고 사이즈라… 저 사이즈는 뭘까요. 패킷을 모아보내는 사이즈면 설정가능하지 않을까 하고 찾아보면, 따른 설정은 안보입니다. 네트워크 좀 아시는 분들은 아 그거 단위로 보내겠다고 쉽게 생각하시겠지만… 전 몰라요~~~
그럼 이제 커널 소스를 보면서 간단하게 생각해보도록 하겠습니다. net/ipv4/tcp_output.c 파일을 보면 tcp_write_xmit 라는 함수가 있습니다. 여기서는 다시 tcp_mss_split_point 를 호출합니다.
...... limit = mss_now; if (tso_segs > 1 && !tcp_urg_mode(tp)) limit = tcp_mss_split_point(sk, skb, mss_now, min_t(unsigned int, cwnd_quota, max_segs), nonagle); ......
tcp_mss_split_point 를 보면 needed 가 버퍼에 존재하는 패킷의 사이즈로 예측이 됩니다. 소켓 버퍼에 있는 사이즈와 window 사이즈 중에 적은게 선택이 됩니다. 그리고 max_len 이 needed 보다 작으면 max_len 이 전송이 되고, 중요한 부분은 partial 은 구하는 것입니다. 모듈러 하는 변수명이 보이시나요? 아까 말한 그 사이즈는 바로 mss_now 인 것입니다. 여기서 partial은 원래 네트웍에서 패킷을 MSS 단위로 보내기 때문에, 모듈러 mss_now 하면, mss가 1024일 때 우리가 600만 보낸다면, 424바이트가 MSS에 모자라기 때문에 partial 은 424 바이트가 됩니다. 코드를 보면 tcp_nagle_check 하고나서 true면 nagle을 적용해야 하는 상황일테니… needed – partial 만큼의 사이즈를 리턴합니다. 즉 MSS 단위로 패킷을 보내도록 짤라준거죠.
static unsigned int tcp_mss_split_point(const struct sock *sk, const struct sk_buff *skb, unsigned int mss_now, unsigned int max_segs, int nonagle) { const struct tcp_sock *tp = tcp_sk(sk); u32 partial, needed, window, max_len; window = tcp_wnd_end(tp) - TCP_SKB_CB(skb)->seq; max_len = mss_now * max_segs; if (likely(max_len len, window); if (max_len packets_out 이 0보다 커야 합니다.(보내는게 있다는 뜻으로...) 그리고 Nagle 알고리즘에서는 마지막으로 minshall 체크라는 걸 합니다. TCP_CORK는 별 다른게 없는데, 아까 Nagle과의 체크에서 ACK를 확인한다는 걸 기억하시나요? static bool tcp_nagle_check(bool partial, const struct tcp_sock *tp, int nonagle) { return partial && ((nonagle & TCP_NAGLE_CORK) || (!nonagle && tp->packets_out && tcp_minshall_check(tp))); }
minshall 이라는 분이 계시더군요(먼산…) 이 알고리즘은 그냥 보낸 패킷의 시퀀스와 ACK 받은 패킷의 시퀀스를 비교하기만 합니다. 아래 before 와 after는 그냥 before는 앞에 파라매터가 작으면 true, after는 앞에 파라매터가 크면 true 입니다. 저기서 snd_sml은 보낸 패킷의 시퀀스, snd_una는 ACK 받은 패킷의 시퀀스입니다. “!after(tp->snd_sml, tp->snd_nxt)” 이 코드는 시퀀스가 오버플로우 난 걸 확인하는 걸로 보입니다. 하여튼!!!, 즉 여기서 중요한 것은 ack를 다 받았다면 tp->snd_sml 과 tp->snd_una는 같은 값일 것이므로 false가 리턴됩니다. 즉 tcp_minshall_check가 true는 현재 ack 받아야할 패킷이 더 있다라는 뜻이고 false는 현재 모든 패킷의 ack를 받았다가 됩니다.
static inline bool before(__u32 seq1, __u32 seq2) { return (__s32)(seq1-seq2) snd_sml, tp->snd_una) && !after(tp->snd_sml, tp->snd_nxt); }
그럼 요약을 하면 TCP_CORK는 이것저것 확인안하고 켜지면 무조건 MSS 단위로만 보내겠다가 됩니다. 그런데 Nagle은 전부 ACK를 받았다면 5 byte만 보낸다고 하더라도… ACK를 모두 받았으므로 tcp_minshall_check 가 false 가 되어서, 패킷이 보내집니다. 요약하면 mss가 1024이고 4100 바이트를 보낸다면 partial = 4100 % 1024 = 4, TCP_CORK에서는 마지막 4바이트는 전송이 되지 않습니다. 언제까지? timeout 이 발생할때까지…, 그러나 Nagle은… ACK를 받을 패킷이 남아있다면 마지막 4바이트가 전송이 안되지만, ACK를 모두 받았다면… 마지막 4바이트도 전송이 되게 됩니다. 이게 두 가지 옵션의 차이이고, Nagle이, TCP_CORK 보다 조금 약한 제약이라는 의미입니다.