Multi-core의 Concurrency를 위해선 멀티스레딩 뿐인가?

답은 아니다라고 생각한다 — 실제 현실도 그런 것 같고. 명확히 정리된 생각은 아니지만, 어제~오늘 생각했던 얘기를 풀어보겠다. 응용에 따라선 다른 방식으로 하드웨어(특히 CPU)의 concurrency를 끌어다 쓸 수 있을 것 같으니.1

멀티코어 / 멀티 CPU 머신이 시대의 흐름인 것은 거의 확실하다. 적어도 앞으로 수 년 정도 범위에서 CPU 기술의 방향은,

  • 메모리와의 통신 효율성 재고 — 현재 최대의 병목은 CPU – 메모리 통신이다
  • 캐쉬 용량 증대 — 앞에서 말한 효율성의 개선
  • CPU 코어 수 증가 — CPU 클럭을 올리는게 힘들어지지만 당분간은 트랜지스터 수는 늘릴 수 있으니 이 쪽일 것이다

일 거라고 생각한다. 이 중에 마지막 예측 때문에 허브 서터 같은 “대가” 들이 concurrency를 활용하기 위한 방안들을 설명하거나, 강조하고 있다. 그러나 한 프로세스 2 의 성능 향상 범위에서 보면 이건 정확하다. 적어도 CPU 발전 방향이 코어 수 증가로 쏠려있는 상황에서는3 단일 프로그램의 1개 실행 속도가 올라가는데는 concurrency를 이용하지 않고는 한계에 직면한다. 뭐 “단일 프로그램"의 얘기다.

범위를 좀 더 확대해서 생각해보자.

1명의 사용자가 아니라 여러 명에게 서비스를 제공하는 “사업자” 측면을 생각해보자. 성능을 끌어올리기 위해 — 혹은 많은 사용자에게 서비스를 제공하기 위해 — 선택할 프로그래밍 모델은 세 가지 정도라고 생각된다.

  1. 멀티스레딩 or concurrent 응용을 만들어낸다. 게임 쪽에서 MMORPG 서버를 만들 때 이런 방식을 취한다.
  2. 같은 서비스를 제공하는 프로세스를 여럿 사용한다. 필요에 따라선 여러 개의 서버에 나눠 띄운다.
  3. 같은 서비스를 제공하는 여러 개의 프로세스 - 여러 프로세스가 한 묶음이 되게 - 를 만들고, 이들간의 통신 시스템을 만든다. 역시 필요에 따라선 여러 개의 서버에 나눠 띄운다.

아마 많은 경우에 프로그래밍 복잡도는 1 > 3 > 2가 아닐까. 반대로 scalability는 3 > 2 > 1 순서.

물론, 하나의 프로세스 실행을 가속할 방법은 1 밖에 없지만, 사업자 측면에서 보면 1~3 모두가 scalable한(확대 제공 가능한) 서비스를 만들어낼 수 있는 방법이다.

게임 서버 처럼 프로세스보다 작은 단위 — 예를 들어 게임 내의 NPC와 게이머의 상호작용, 게이머간의 상호작용 — 에서 데이터 공유가 매우 많다면, 프로세스를 1개로 제한하는 수 밖에 없지만4, 그렇지 않은 상황에선 2/3 중에 적당한 것을 골라야 하지 않을까.

현존하는 초대형 웹 서비스들 — 웹 포탈이나 WordPress.com 같은 대형 호스팅 서비스들 — 은 3과 비슷한 구조를 가지고 있다. Load–balancer, 응용 서버 그리고 분산 DB로 이어지는 _잘 정의된 분할_을 가지고 있다. 물론 이런 대형 서비스들은 그것만으로 부족해서 구글의 분산 파일 시스템5 같은 도메인에 정말 잘 어울리는 응용을 개발해버리기도 하지만 :)

게임 서버의 경우에도 2, 3 과 같은 구현체들이 적지 않다. Sun의 Dark Star도 3과 유사한 구조를 가지고 있다 — 알려진 성능 평가는 “안습” 한 마디 빼고는 해줄 말이 없지만.6 카트라이더나 많은 수의 MMOG — 사용자 간의 상호작용이 일정 수(30인 이하 수준)으로 유지되는 게임들 — 의 경우에는 3과 같은 구조를 만들어내기 쉽다. Front-end의 load-balancer와 뒷 단의 DB, 그리고 중간이 분산된 게임 서버들 구성을 하는 경우들이 있다.

이런 분산된 개별 게임 서버는 간단하게 싱글 스레드/프로세스를 채용할 수 있게 된다. N모사 (내가 다니는 곳이 아닌 :p )에서 퇴사한 모군의 경우엔 싱글 스레드로 서버를 바꿨더니,

누구나 수정할 수 있게 되었어요!

라고 좋아하더라. 비슷하게 또 다른 개발자 한 분도, “굳이 멀티스레딩 쓸 이유있나요” 라는 뉘앙스의 얘기를.

사실 맞는 얘기다. 이런 구조에서 얻을 수 있는 병렬성이 있다 = 코어 수가 늘어갈 때도 분명히 이익이 있다. CPU 코어 수 만큼 게임 프로세스를 띄워버리면 되니까. 그럼 CPU를 낭비할 일도 없이(혹은 적게) 서버를 운영할 수 있게 된다.

그리고 위에서 후배 모군이 얘기한 것처럼 단일 스레드 프로그래밍은 쉽다. 굉장히 예측가능하고 인간의 뇌가 이해하기 쉽다. 다만 프로세스 간 통신이 힘들다는 근본적인 문제 때문에 만들어야하는 응용에 쓸 수 있냐없냐하는 문제가 있다. 오류가 발생해도 단순히 일부 게임들만 죽고(?) 끝나고, 개별 메시지를 처리할 때마다 오류처리를 아주 힘들게 할 이유가 없다 — 정 안되면 일부 게임을 포기하고 죽여버리면 되니까. 심지어 서버를 죽인다는 메모리 릭7이 좀 있어도 일부 게임만 죽이면서 프로세스를 다시 띄우는 방식으로 해결할 수 있다.

그래서 MMORPG 서버처럼 무시무시하게 coupling된 시스템이 아닌 이상 잘 쪼갤 궁리부터 열심히 해야하지 않을까. 잘 쪼개고, 쉽게 (단일 스레드로) 가는게 개발자 건강에 유익하지 않을까 하고 잡 생각하는 밤이다.

ps. 완전히 여담이지만. 1의 구조로 가기엔 지금 우리가 가진게 너무 없다. STM이나 묵시적인 locking algorithm, 메시지 전송 같은 좀 더 편안하고, 안전하게 쓸 수 있는 primitive 혹은 개념들이 제공되기 시작했지만 멀티스레딩-concurrent programming이란 것은 저수준 시스템 프로그래밍에 가깝다;

OS에서 제공하지 않는 기능만으로 하기엔 한계가 있고, OS에서 제공하는걸 쓰자니 굉장히 저수준이 되어가고.8


  1. 엄밀한 의미로 말하면 concurrent-programming이 아닐 수도 있다. 모든 구성요소가 동시에 돌지 않아도 되는 내용위주로 글을 썼다. ↩︎

  2. 컴퓨터 공학의 OS 개론에서 말하는 process를 의미한다. OS에서 구분하여 자원을 관리해주고, 독립적인 메모리 영역을 갖는 단위를 의미한다. ↩︎

  3. 2003년에 2.4 Ghz CPU를 보편적으로 쓸 수 있었는데, 지금 사용하고 있는 intel Core2Duo CPU의 클럭이 겨우2.66Ghz다. 물론 캐쉬메모리의 증가, 명령어 파이프라인의 개선, 명령 실행 방식 변경(메모리 접근 재정렬), 명령의 병렬 수행 등등으로 “의미면에서의 실행속도"는 확실히 빨라지긴 했다. ↩︎

  4. 혹은 가능하다면 통신량이 적을 곳 끼리 잘라내는 정도의 분할. 그러면 3에 조금 가까워진다. 상호작용이 많다는 문제 때문에 아무리 MMORPG 서버를 잘 만들어도, 한 서버(논리적인 의미에서)에서 처리하는 인원 수에는 한계가 있다. 가장 많은 인원을 처리한다는 Lineage II 서버도 이상적인 네트웍 상황에서 1개 서버에서 8천명 정도가 한계일 것이다. 물론 단일 서버 처리량으론 절대 작지 않다. ↩︎

  5. 검색 자체가 결과만 재빨리 나오면 되고, 굉장한 저장용량을 필요로 하지만 저장 공간 자체는 append-only 에 가깝게 쓴다는 특징들을 잘 이용해서 단순한 RAID로라면 하루 종일 디스크 교체만 해도 (전체 서버군이) 유지되기 힘든 문제를 해결했다. ↩︎

  6. 서버간 병렬성으로 성능을 올리는 것은 둘째치고, 단일 서버 안에서는 당장 상용에서 쓰기 힘든 수준이라니. ↩︎

  7. 대부분의 경우 서버는 오래 떠 있을걸 가정하기 때문에, 아주 조금씩 새도 결국엔 물리 메모리를 다 채우고, 스왑핑을 하고, thrashing으로 들어가면서 CPU를 못 쓰게되고, 요청 속도를 처리 속도가 못 따라가게 되면서 서버는 사망. ↩︎

  8. 물론 boost::ASIO 팩키지나 ACE 프레임웍 처럼 각 OS의 저수준 기능을 잘 wrapping해서 제공하기도 하지만 그렇게 잘 되는 경우도 별로 흔하진 않은 것 같다. 당장 떠오르는게 저거랑 boost::thread 정도 뿐이니 Java도 epoll system call 같이 각 OS의 최적화된 I/O multiplexing을 쓰게된게 1.6 부터였던 것 같으니. ↩︎