TCP로 작은 크기의 패킷 전송 번역

원문

Winsock TCP로 작은 크기의 data segment를 전송

Summary

TCP를 사용하여 작은 패킷을 보내야 할 경우, 어플리케이션의 디자인은 매우 중요합니다. 지연된 응답의 상호작용(네이글 알고리즘)을 고려하지 않으면 Winsock 버퍼링이 성능에 큰 영향을 끼칩니다. 이 글에서는 몇 가지 케이스 스터디를 통해 작은 패킷을 효율적으로 전송하는 방법을 알아봅니다.

Background

Microsoft TCP 스택이 패킷을 받으면 200ms의 지연 타이머가 시작됩니다. 수신 측에서 송신 측으로 ACK가 전송되면 지연 타이머는 리셋되고, 다음 패킷을 받으면 또 다시 시작됩니다. 수신된 패킷의 ACK를 언제 전송할 것인가를 결정하기 위해 MS TCP스택은 아래의 기준을 사용합니다.
  • 지연 타이머가 만료되기 전에 다음 패킷을 수신하면 ACK 전송
  • 다음 패킷을 수신하거나 지연 타이머가 만료되기 전에 전송 할 다음 패킷이 있으면 그 패킷에 편승하여 ACK 전송
  • 지연 타이머가 만료되면 ACK 전송
작은 패킷들이 네트워크를 혼잡하게 만드는 것을 회피하기 위해, MS TCP스택은 네이글을 기본적으로 on 합니다. 네이글 알고리즘은 원격에서 이전 패킷의 전송에 대한 ACK를 수신하기 전까지 작은 패킷들을 모으고 전송을 지연시킵니다. 아래는 네이글 알고리즘의 두 가지 예외 상황입니다.
  • MTU(최대 전송 유닛)보다 더 큰 패킷이 모아졌을 때, ACK를 기다리지 않고 바로 전송합니다. 이더넷 네트워크에서 TCP/IP의 MTU는 1460 byte 입니다.
  • TCP_NODELAY 옵션이 활성화 되어 있으면 작은 패킷들을 원격으로 바로 전송합니다.
어플리케이션 레이어의 성능을 향상 시키기 위해, Winsock은 어플리케이션의 전송 버퍼의 데이터를 Winsock 커널 버퍼로 복사합니다. 그리고 나서 스택은 패킷을 언제 원격으로 전송할지 결정하기 위해 휴리스틱을 사용합니다. SO_SNDBUF 옵션을 사용하여 Winsock 커널 버퍼의 크기를 변경할 수 있습니다. 기본값은 8K 입니다. 필요하다면 Winsock은 SO_SNDBUF 사이즈보다 더 큰 사이즈를 버퍼링 할 수 있습니다. 대부분의 경우 어플리케이션이 인지할 수 있는 전송 완료는 어플리케이션의 버퍼가 Winsock 커널 버퍼에 복사되었을 때 이고, 실제로 네트워크에 전송 중인지는 알 수 없습니다. SO_SNDBUF 를 0으로 설정함으로써 Winsock 버퍼링을 껐을 때는 예외입니다.

Winsock은 전송 완료를 어플리케이션에 통지하기 위해 아래의 룰을 따릅니다. 
  • SO_SNDBUF에 남는 버퍼가 있다면 어플리케이션 버퍼에서 복사하고, 어플리케이션에 전송 완료를 통보합니다.
  • SO_SNDBUF에 남는 버퍼가 없지만, 아직 보내지 않은 이전 패킷이 하나만 쌓여있다면 어플리케이션 버퍼에서 복사하고 어플리케이션에 전송 완료를 통보합니다.
  • SO_SNDBUF에 남는 버퍼가 없지만, 아직 보내지 않은 이전 패킷이 하나 이상 쌓여있다면 일단 어플리케이션 버퍼에서 복사하고 어플리케이션에는 전송 완료를 나중에 통보합니다.
Case Study 1

Overview

Winsock TCP 클라이언트가 TCP 서버에게 DB에 저장하기 위한 10000개의 레코드를 보내야 합니다. 레코드의 크기는 20 ~ 100 바이트 입니다. 어플리케이션 로직을 단순화 하기 위한 디자인은 아래와 같습니다.
  • 클라이언트는 블로킹 전송만 하고, 서버는 블로킹 수신만 합니다.
  • 클라이언트 소켓은 SO_SNDBUF를 0으로 설정하여, 매번 데이터 세그먼트를 하나씩 전송합니다.
  • 서버는 루프 안에서 recv를 호출합니다. 수신 버퍼를 200 byte로 배정하여 각각의 레코드가 하나의 recv 호출로 수신이 가능하도록 합니다.
성능

테스트를 통해 클라이언트는 1초에 5개의 레코드만 서버로 전송할 수 있다는 사실을 알수 있습니다. 10000개의 레코드(976KB, (10000x100)/1024)를 전송하는데 30분의 시간이 걸립니다.

분석

클라이언트 TCP_NODELAY가 off이기 때문에, 네이글 알고리즘이 ACK가 도착하기 전까지 다음 패킷을 원격으로 전송하지 못하게 합니다. 하지만 SO_SNDBUF를 0으로 설정해서 Winsock 버퍼링을 off 했습니다. 따라서 10000번의 send 호출과 ACK는 개별적으로 수행됩니다. TCP서버 스택에 아래의 현상이 발생해서 ACK는 매번 200ms 지연 됩니다.
  • 서버가 패킷을 받을 때마다 200ms 지연 타이머가 시작됩니다.
  • 서버는 클라이언트로 전송할 패킷이 없기 때문에, 수신한 패킷에 대한 ACK를 편승해서 전송 할 수 없습니다.
  • 서버의 지연 타이머가 만료되면 ACK가 전송됩니다.
개선 방안

위 디자인에는 2개의 문제점이 있습니다. 첫번째로 지연 타이머 문제 입니다. 클라이언트는 2개의 패킷을 200ms안에 서버로 전송 할 수 있어야 합니다. 네이글 알고리즘 기본값이 on이기 때문에, 클라이언트는 SO_SNDBUF를 0으로 하지 말고 Winsock 버퍼링을 사용해야 합니다. TCP 스택이 MTU보다 큰 패킷을 모으면, full-size 패킷이 ACK를 기다리지 않고 한번에 원격으로 전송됩니다.
두번째로 이 디자인은 매번 작은 크기의 패킷마다 send를 호출합니다. 작은 크기의 패킷을 전송하는 것은 매우 비효율적 입니다. 이 경우에는 80개의 레코드를 모아서 send 호출을 할 수 있습니다. 서버에게 전송될 전체 레코드 개수를 알려주기 위해, 클라이언트는 레코드 개수가 포함된 고정 크기 헤더 전송으로 통신을 시작할 수 있습니다.


코드 리뷰 시스템 Crucible Programming

Crucible은 Jira로 유명한 Atlassian에서 만든 코드 리뷰 시스템 입니다.
http://www.atlassian.com/software/crucible/

Code Collaborator에 비해 기능은 적지만 가격이 저렴합니다.

간단하게 리뷰를 올리는 프로세스를 보여드리겠습니다.

리뷰를 올리는 방식은 Post-Review가 있고 Pre-Review가 있습니다.
  • Post-Review : 저장소에 Commit 된 Changelist 를 기반으로 리뷰를 진행
  • Pre-Review : 소스를 Commit 하기전에 리뷰를 올려서 진행
먼저 Post-Review 프로세스 입니다.

대쉬보드에서 Review 탭을 클릭하면 "Create New Review"가 보입니다. 클릭.

Browse Changesets를 클릭

본인이 Commit한 Changelist가 보입니다. 적절한 항목을 선택하고 "Edit Details"를 클릭.
이렇게 바로 Changelist가 나오게 하려면 Crucible의 계정과 소스저장소의 계정이 동일해야 합니다.


리뷰어를 지정하고 "Start Review"를 클릭합니다.


여기서 부터는 Reviewer의 시선입니다. 변경된 파일들이 좌측에 트리구조로 보이는데, 하나씩 클릭해서 보면 됩니다.
이것은 없던 소스가 삽입된 경우.

이것은 기존 소스가 변경된 경우 입니다.

라인을 클릭하면 코멘트를 달 수 있습니다.

반드시 고쳐야하는 심각한 오류는 Defect를 체크하고 심각도, 분류 항목을 선택해 줍니다.

"Complete"를 클릭해서 Reviewee에게 보냅니다.

Reviewee는 코멘트를 확인하고 수정할것 수정한 후 "Summarize"를 클릭하여 리뷰를 정리합니다.


"Close Review"를 클릭하면 리뷰가 종료됩니다.

다음은 Pre-Review 입니다. Pre-Review를 받기 위해서는 소스저장소에서 patch를 만들어야 합니다. 퍼포스를 예로 들면 아래와 같이 명령줄에 입력하면 현재 pending list를 patch.txt로 만들어 줍니다.

p4 diff -dcu > patch.txt

Post-Review에서 Browse Changesets 선택했던 화면에서 Pre-Commit 선택하고 미리 만들어둔 patch.txt를 업로드 하면 됩니다.

이후는 Post-Review와 같습니다.

그들이 말하지않는 23가지 등 서평

★★★★★ (5개)

나쁜 사마리아인과 비슷한 이야기로 신자유주의의 허상에 대해 이야기 하는 책입니다. 나쁜 사마리아인보다 쉽게 설명되어 있어서 이해하기 좋았습니다. 읽는 재미도 있고 여러모로 추천할만한 책.


★★★★ (4개)
일본 스님이 쓰신 책인데, 잡념이 많을때, 쉽게 화가 날때, 이럴때 스스로를 어떻게 컨트롤 해야 할 것인가를 알려주는 책입니다. 무의식적으로 혹은 수동적으로 여러가지 행동을 동시에 하지 말고(음악 들으면서, 책 보면서, 옆사람 통화 엿들으면서... ), 하나의 행동에 집중해서 느끼고 생각하고 행동하라는 내용이 와닿았습니다.

★★ (2개)
저에겐 너무 어려웠습니다. 중간까지 읽다가 포기. 한국사람에게는 너무 생소한 미국역사상 정치적/사회적인 사건들을 별 설명없이 인용하는 경우가 많습니다. 미국 정치, 역사에 대한 지식이 좀 있어야 이해를 할듯합니다.

★★★★★ (5개)
예수의 생애를 좀 더 객관적인 시각에서 바라보아 재해석을 시도하는 책입니다. 사건에 대한 설명과 의견, 그것을 뒷받침하는 설명들이 상당히 논리적이라 많은 동감을 했고 또한 감동도 했습니다. 너무 재미있게 읽어서 김규항씨의 책들을 더 찾아보게 되는 계기가 되었습니다.

★★★★ (4개)
김규항씨의 블로그에 올렸거나 신문 혹은 잡지에 기고했던 글들을 모아서 발행한 책입니다. 동감가는 이야기도 많고 재미도 있습니다만 너무 짧은 글들의 모음이라 몰입은 잘 안되더군요.

Protocol Buffers를 패킷으로 활용해 보자 Programming

Protocol Buffers(이하 PB)를 패킷으로 활용해 보기위한 예제입니다.

패킷으로서 활용하려면 패킷이 뭉쳐서 올수 있으므로  메세지의 길이를 알아낼수 있다거나 끝을 표시해주는 기능이 필요합니다. 하지만 아래 링크의 글을 보시면,
http://code.google.com/apis/protocolbuffers/docs/techniques.html
하나의 스트림에 다수개의 메세지를 쓸때, 메세지의 끝이나 시작을 추적하는 것은 직접 구현해야 합니다. PB에는 메세지의 끝을 알아내는 기능이 없습니다. 이것을 해결하기 위한 가장 쉬운 방법은 메세지를 비트에 쓰기전에 그것의 사이즈를 쓰는것 입니다. 나중에 읽어낼때는 사이즈를 먼저 읽고 그 사이즈 만큼의 메세지의 본체를 읽어내면 됩니다.

헤더를 자체적으로 만들어서 사용하라고 권고합니다. 그래서 아래와 같이 헤더를 먼저 쓰고 그 다음에 메세지를 쓰는 방식으로 구현했습니다.


메세지가 미완성인지 체크해주는 기능은 있으므로 잘려서 오는 것은 걱정없습니다.
bool isCompleted = message.ParseFromCodedStream(&stream);
먼저 simplemessage.proto 파일 입니다.
package simple;

enum MessageType {
    LOGIN = 0;
    CHAT = 1;
    MOVE = 2;
}

message Login {
    required int32 id = 1;
    required string name = 2;
}

message Chat {
    required string name = 1;
    required int32 dst_id = 2;
    required string message = 3;
}

message Move {
    required int32 id = 1;
 
    message Position {
        required float x = 1;
        required float y = 2;
    }
    repeated Position track = 2;
}

이전 포스트 참조해서 h/cc파일을 생성합니다. 아래는 생성된 파일을 이용한 소스 파일 입니다.
#include <string>
#include <google/protobuf/io/zero_copy_stream_impl_lite.h>
#include <google/protobuf/text_format.h>
#include "simplemessage.pb.h"

using namespace std;
using namespace google;

// 메세지 사이에 끼워넣을 헤더 구조체
struct MessageHeader
{
   protobuf::uint32 size;
  simple::MessageType type;
};
const int MessageHeaderSize = sizeof(MessageHeader);

class PacketHandler
{
public:
   void Handle(const simple::Login& message) const
   {
    PrintMessage(message);
   }
  void Handle(const simple::Chat& message) const
   {
     PrintMessage(message);
   }
  void Handle(const simple::Move& message) const
   {
      PrintMessage(message);
  }

protected:
   // 메세지의 내용을 텍스트 형식으로 출력
  void PrintMessage(const protobuf::Message& message) const
   {
      string textFormatStr;
     protobuf::TextFormat::PrintToString(message, &textFormatStr);
     printf("%s\n", textFormatStr.c_str());
  }
};

// 패킷을 파싱해서 그것을 인자로 적절한 메서드를 호출해줌
void PacketProcess(protobuf::io::CodedInputStream& input_stream, const PacketHandler& handler)
{
  MessageHeader messageHeader;
   // 헤더를 읽어냄
  while( input_stream.ReadRaw(&messageHeader, MessageHeaderSize) )
   {
// 직접 억세스 할수 있는 버퍼 포인터와 남은 길이를 알아냄
    const void* payload_ptr = NULL;
     int remainSize = 0;
    input_stream.GetDirectBufferPointer(&payload_ptr, &remainSize);
    if (remainSize < (signed)messageHeader.size)
       break;

     // 메세지 본체를 읽어내기 위한 스트림을 생성
    protobuf::io::ArrayInputStream payload_array_stream(payload_ptr, messageHeader.size);
     protobuf::io::CodedInputStream payload_input_stream(&payload_array_stream);

     // 메세지 본체 사이즈 만큼 포인터 전진
     input_stream.Skip(messageHeader.size);

// 메세지 종류별로 역직렬화해서 적절한 메서드를 호출해줌
     switch(messageHeader.type)
      {
     case simple::LOGIN:
       {
          simple::Login message;
         if (false == message.ParseFromCodedStream(&payload_input_stream))
             break;
         handler.Handle(message);
        }
      break;
     case simple::CHAT:
        {
          simple::Chat message;
          if (false == message.ParseFromCodedStream(&payload_input_stream))
            break;
          handler.Handle(message);
       }
        break;
     case simple::MOVE:
       {
          simple::Move message;
         if (false == message.ParseFromCodedStream(&payload_input_stream))
            break;
         handler.Handle(message);
       }
        break;
   }
   }
}

// 메세지를 출력용 스트림에 쓴다
void WriteMessageToStream(
simple
::MessageType msgType,
const protobuf::Message& message,
protobuf::io::CodedOutputStream& stream)
{
  MessageHeader messageHeader;
   messageHeader.size = message.ByteSize();
  messageHeader.type = msgType;
   stream.WriteRaw(&messageHeader, sizeof(MessageHeader));
  message.SerializeToCodedStream(&stream);
}

int main(int argc, char* argv[])
{
  // 메세지에 값 세팅
   simple::Login login;
  login.set_name("alice");
   login.set_id(1);

  simple::Chat chat;
   chat.set_name("rabbit");
  chat.set_dst_id(2);
   chat.set_message("How are you doing?");

  simple::Move move;
   move.set_id(3);
  simple::Move::Position* pos0 = move.add_track();
   pos0->set_x(10.01f);
  pos0->set_y(10.02f);
  simple::Move::Position* pos1 = move.add_track();
   pos1->set_x(20.01f);
  pos1->set_y(20.02f);
   simple::Move::Position* pos2 = move.add_track();
  pos2->set_x(30.01f);
   pos2->set_y(30.02f);

  // 필요한 버퍼의 사이즈를 알아내서 버퍼를 할당
   int bufSize = 0;
  bufSize += MessageHeaderSize + login.ByteSize();
   bufSize += MessageHeaderSize + chat.ByteSize();
  bufSize += MessageHeaderSize + move.ByteSize();
   protobuf::uint8* outputBuf = new protobuf::uint8[bufSize];

  // 메세지를 출력할 스트림 생성
   protobuf::io::ArrayOutputStream output_array_stream(outputBuf, bufSize);
  protobuf::io::CodedOutputStream output_coded_stream(&output_array_stream);

   // 메세지를 스트림에 쓴다
  WriteMessageToStream(simple::LOGIN, login, output_coded_stream);
   WriteMessageToStream(simple::CHAT, chat, output_coded_stream);
  WriteMessageToStream(simple::MOVE, move, output_coded_stream);

   // 패킷 핸들러와 입력용 스트림 생성
  PacketHandler handler;
   protobuf::io::ArrayInputStream input_array_stream(outputBuf, bufSize);
  protobuf::io::CodedInputStream input_coded_stream(&input_array_stream);

   // 패킷 분석
  PacketProcess(input_coded_stream, handler);

   // 버퍼 해제
  delete [] outputBuf;
   outputBuf = NULL;
  return 0;
}
실행 결과 입니다.



Good-bye Frankfurt 기타

대략 1년반정도의 독일생활을 정리하고 돌아왔습니다. 사실 돌아온건 2달쯤 전인데, 더 늦기전에 글을 쓰지 않으면 아예 잊어버릴것 같아서 지금이라도 올립니다.

Frankfurt 라는 도시에 대한 1년반동안의 감상이랄까 그런 글입니다. 일단 전반적으로 만족스러웠습니다. 처음엔 독일인에 대한 선입견 같은게 있었죠. 무뚝뚝하다거나 불친절, 완벽주의, 2차대전 등등 하지만 대부분은 사실과 달랐습니다. 사람들은 대다수가 친절했고 인종차별같은건 단 한번도 당해본적 없습니다. 나름 국제도시라서 다양한 인종들이 많습니다. 백인이 가장 많고, 터키계가 그다음, 나머지 흑인, 아시아계가 소수있죠. 제 시선에는 특별히 다른 인종들을 배척하지 않고 어울려서 잘 살아가고 있었습니다. 북미쪽에서는 인종때문이라기보다는 영어사용에 미숙하면 무시당하는 경우가 종종있다고 하던데, 여기는 어차피 영어모국어권도 아니니 그런 것도 없습니다. 젊은 사람들은 대부분 영어사용에 익숙하고 나이드신 분들은 거의 못하시더군요. 영어만으로 일상 생활에 지장이 없었습니다. 다만 아기를 유치원에 보낸다거나, 집을 계약한다거나 하는 좀 더 디테일한 상황에서는 독일어가 좀 필요한듯 했습니다. 이런 경우에는 회사 인사팀에서 도움을 주었으므로 또한 문제가 없었습니다.


두번째로 이야기하고 싶은게 자연환경인데요, 제가 사는 동네에도 정말 멋진 공원이 2개나 있었는데, 하나는 조깅도 할수 있고 고기도 구워 먹을수 있는 전형적인 공원이었고 나머지 하나는 숲길이 길게 펼쳐져있고 호수도 있어서 산책하기 아주 좋은 그런 곳이었습니다. 평일에는 아침에 조깅하러 가고 주말에 날씨 좋은날 도시락 싸서 가면 하루가 훌쩍가죠. 주말에 가도 사람들이 그리 많지 않았습니다. 멋진 공원 근처에 사는게 희망사항이었는데, 제가 살던 동네(Enkheim)는 그런면에서 만족스러웠습니다.


대중교통에 대해 이야기 해보면 Frankfurt는 전반적으로 대중교통이 잘 되어 있어서 자가용없이도 생활이 가능했습니다. 대신 가격은 상당히 비싼 편이라 서울이 보통 편도 900원정도 하는 거리면 거기서는 4000원 정도였습니다. 통근하는 직장인들은 월정기권을 끊어서 이용하는 듯 했습니다. 약간 할인 되기는 하지만 그래도 비싸죠. 좋은 점이라면 그닥 붐비지 않는다는 점과 장애인과 유모차에 대한 배려가 많다는 점이었습니다. 유모차를 가지고 어디를 가도 불편하지 않게 이동이 가능했습니다. 단점하나 추가하자면 지하철은 한국에 비해 지저분하고 냄새도 좀 나서 그닥 좋지 않았습니다. 어딜가도 서울 지하철만큼 깨끗한데가 없죠.

빼놓을수 없는 Frankfurt의 장점은 유럽여행 다니기 정말 좋은 위치라는 것이죠. 기차로 3시간이면 스위스, 4시간이면 파리, 공항이야 워낙 유명하고 비행편도 많죠. 비용때문에 자주 다니지는 못해도 기차표나 비행기표 모두 미리 끊으면 상당히 저렴하게 구입할수 있는 표가 있습니다. 몇달전부터 미리 계획을 짜서 움직이면 저렴하게 여행을 다닐수있습니다. 

단점은 음식이 별로라는 점. 전반적으로 짜고 팍팍해서 거의 대부분의 음식이 입에 맞지 않았습니다. 다행히 독일교민을 상대로한 인터넷 쇼핑몰 같은게 있어서 라면, 쌀, 김, 참치 등등 주문해서 집에서는 거의 한식을 먹었죠. 이거 없었으면 어떻게 살았을까 생각만 해도 아찔하네요. 물가도 비쌌습니다. 품목별로 차이가 있기는 하지만 전반적으로 서울보다 1.5배 가량 비싸다는 느낌을 받았습니다. 대표적으로 비싼 것은 대중교통, 학용품류, 집세 였습니다. 은행도 불편한 항목중에 하나네요. 오늘 입금을 해도 돈은 이틀쯤후에 이체된다거나, 주소가 없으면 통장 개설자체가 불가능하다거나, 계좌에 돈이 충분치 않으면 수수료를 매달 떼어간다거나 하는 등의 한국과 다른게 유독 은행 일처리에 많았습니다. 일요일에 모든 가게가 문을 닫아서 쇼핑이 불가능한것은 처음에는 불편했지만 적응이 되니 토요일에 미리미리 다 사놓게 되더군요.

장점, 단점이 있지만 그래도 여러모로 살기 좋은 곳이었고 저와 가족들에게는 좋은 기억으로 남은 Frankfurt 였습니다.


1 2 3 4 5 6 7 8 9 10 다음