본문 바로가기

프로그래밍/알고리즘

대량의 데이터 처리를 위한 프로세싱 모델


서버 개발자들이 한번쯤 고민하게 되는 모델이다.

그런데, 이 모델의 고민은 IOCP기반의 개발자 보다는 UNIX기반 개발자들이 더 많은 고민을 하게 되는 모델이기도 하다.

나는 가끔 윈도우 기반으로 개발하는 것은 참으로 축복이라고 생각한다.

많은 개발자 들이 MS를 욕하지만, 내가 보기에는 MS가 개발자들의 고민을 아주 많이 해소해 주는 존재 임은 분명해 보인다.

그럼 이제 옆의 구조의 의미를 말해보자.

LINUX에서는 대량의 데이터 통신을 위해 Edge Triggered기반의 epoll을 제공하고 있다.
그런데, epoll이 IOCP처럼 많은 것을 해주지는 못한다. 그러다 보니, 그 만큼 많은 처리가 개발자의 몫이 된다.

개발자 라면 이런 고민을 할것이다.
   "클라이언트의 연결이 많은 상태에서 어떻게 데이터를 처리하는게 좀더 효율적일까?"

이 고민의 이슈를 정리해보면
   1. 클라이언트에서 처리해야 할 데이터를 서버로 많이 전송한다.
   2. 그런데, 접속된 클라이언트는 많다.
   3. 그러다 보니 처리해야할 데이터가 아주아주 많이 지고, 데이터 처리의 효율성이 떨어지면서 속도가 떨어진다.
   4. 쓰레드를 써서 해결해야 겠는데.. 그러면 좋아 질까?
   5. 그런데 어떻게 만드는게 효율적이지?
대략 이러한 순서로 고민을 하게 될것이다.

이러한 고민을을 해결하기 위해 많은 플랫폼들이 있지만.. 나는 다음과 같은 방법을 제시해 본다.

구조는 크게 3부분으로 만들어 진다.
  1. 네트워크에서 발생된 데이터를 패킷 처리 큐에 PUSH BACK 하는 메인(쓰레드)
  2. 데이터 처리 큐에서 패킷을 분배 하는 쓰레드
  3. 실제 패킷을 처리하는 WORK 쓰레드

그럼 해당 3부분을 기반으로 처리 흐름을 설명해 보겠다.
   1. 클라이언트(S)에서 발생된 패킷을 처리 큐에 PUSH BACK 한다. (1)
   2. 패킷이 저장될때 마다 event를 발생시켜 패킷 분배 쓰레드를 깨운다. (1)
   3. 패킷 분배 쓰레드는 event를 받고 깨어나서,
      클라이언트(S) 별로 저장된 패킷 큐에서 처리할 패킷을 POP FRONT 한다 (2)
   4. 이때, 처리 중인 클라이언트 (S)는 분배 RULE에서 제외 시킨다 (2)
      : 이를 통해 동일 클라이언트에서 발생된 데이터를 동시에 처리하지 못하도록 하여 데이터 직렬화 한다.
   5. 선택된 클라이언트 (S)는 다음에 선택되지 않도록 비활성화 시키고 패킷을 처리할수 있도록 
      작업 쓰레드로 전송한다. (2)
   6. 작업 쓰레드에서 해당 처리를 마치면 클라이언트 (S) 패킷 큐를 활성화 시켜 다음 패킷을 보낼수 있도록 한다.

좀 설명해 복잡해 보이나 규칙은 간단하다.

struct dataBuffer {
   char *buffer;
   int size;
};
typedef std::deque<dataBuffer> dataBuffer_t;

struct dataEntry {
   int fd; // fd
   bool enable; // fd 활성화 여부
   dataBuffer_t q; // 처리할 패킷 큐
   LOCK_T lock;
};
typedef std::deque<dataEntry> dataEntry_t;

struct dataSingleton {
   dataEntry_t fds;
   int index; // 다음 처리 예정인 dataEntry의 위치
   COND_T cond;
   LOCK_T lock;
};

대략 자료의 원형이 될듯 하다.

간략화 하면..
   1. 작업할 패킷(dataBuffer)의 선택
      1.1. (dataEntry.enable == true) && (dataEntry.q.size() > 0) 인 dataEntry를 찾는다.
      1.2. dataEntry.enable = false 로 설정하고 WORK 쓰레드로 전송한다.

  2. WORK 쓰레드의 처리를 마친후
     2.1. dataEntry.q.pop_front()하여 최상위 패킷을 제거한다.
     2.2. dataEntry.enable = true로 설정한다.

  3. 클라이언트에서 패킷이 발생되는 경우
     3.1. dataEntry.q에 발생된 데이터를 저장한다.
     4.2. dataSingleton.cond를 설정하여 분배 쓰레드를 깨운다.

  4. 클라이언트의 접속이 해제될때
     4.1. dataSingleton.fds에서 dataEntry를 찾는다.
     4.2. 만약 dataEntry.enable == false 인 경우 dataEntry.fd = -1로 설정
     4.3. dataEntry.enable == true인 경우 dataSingleton.fds에서 dataEntry 제거

  5. 클라이언트가 접속할때
     5.1. dataSingleton.fds에 새로운 dataEntry를 추가한다.

이렇게 하면...
   1. 연결된 모든 클라이언트의 데이터 처리 패킷수는 동일화 된다.
      : 즉, 클라이언트당 똑같은 한개의 패킷씩 처리하게된다.
      -> 이슈) 처리할 패킷수가 누적되어 과도한 메모리를 사용할수 있다. 이때 처리 패킷큐의 최대 수 제한 필요.

   2. 특정 클라이언트의 처리 패킷수가 폭주하는 상황에서도 안정적인 처리를 할수 있다.
      -> 이슈) 폭주하는 클라이언트 발생시 예외 처리 필요

물론, 이외에도 해결해야 할 과제도 많이 있다.

나머지는 To be continue....