[기술컬럼] 고용량 서버 구현을 위한 원칙

게임 개발 과정에 참여해본 사람이라면 굳이 게임 서버 개발자가 아니더라도 “게임 서버의 scalability” 라는 표현을 접해봤을 것이다. 일반적으로 Scalability 는 시스템, 네트워크, 프로세스가 더 많은 작업량을 처리할 수 있게 하는 특성을 의미하거나 혹은 작업량이 증가하더라도 이를 소화할 수 있는 확장성을 의미한다.

그 때문에, Scalability 라는 것은 단순히 서버를 늘렸을 때 서버가 어떻게 잘 동작하는지에 대한 질문이라기 보다 늘어나는 유저를 어떻게 수용할 수 있는가에 대한 질문에 가깝다고 할 수 있다. 그리고 당연하겠지만, 늘어나는 유저를 처리하기 위한 방법으로는 1) 서버당 처리 용량을 늘리는 방법과 2) 서버를 추가함으로써 전체 시스템의 처리 용량을 늘리는 방법이 있을 수 있겠다.

서버당 처리 용량을 늘리기 위해서는 단순하게 더 고사양의 기계로 교체하는 방법도 있겠지만, 이 방법은 단순 포팅만으로 성능향상을 얻어낼 수 있고 기존 코드 중에 아무것도 바꾸지 않아도 된다는 장점이 있는 반면, 고사양으로 갈수록 장비 가격이 기하급수적으로 비싸진다는 문제점과 결국 고사양의 끝은 존재한다는 현실적인 문제를 겪게 된다. 또한, CPU 나 메모리가 부족해서 생기는 문제가 아닌 것들은 해결이 어렵다는 단점도 있다.

5월_대표님 컬럼_image1.png

그림1: CPU 의 성능 증가에 따른 가격 변화. 붉은색 선이 linear 한 선인데, 각 히스토그램으 끝을 이은 파란색 선이 이보다 더 빠른 속도로 증가하는 것을 알 수 있다.

따라서 서버당 처리 용량을 개선하기 위해서는 서버 프로그램의 개선이 병행되어야 되는데, 이런 개선은 1) 비효율성을 줄이고, 2) 동시성을 최대화하는 방식으로 이루어져야 된다. 사실 이 두 가지 원칙은 비단 단일 서버의 처리 용량을 개선하는 것 외에도 분산 시스템을 포함해서 어떤 시스템을 구현하더라도 적용되는 원칙이다.

비효율성이라는 것은 불필요하게 CPU 시간을 낭비하는 것을 의미한다. 그럼 어떤 것들을 비효율적이라고 할 수 있을까? 여기에는 동일한 작업을 처리하는데 더 오래 걸리는 방법을 쓰는 경우와 (즉, 다시 말해 성능이 좋지 않은 알고리즘을 쓰는 경우) 아니면 다른 작업의 완료를 기다리기 위해 polling 을 하는 경우 (즉 busy waiting 을 하는 경우) 로 나눌 수 있다. 전자의 경우 알고리즘의 개선으로 해결할 수 있고 후자의 경우 비동기식 처리 방법으로 변경함으로써 비효율성을 줄일 수 있다.

동시성이라는 것은 둘 이상의 작업이 동시에 진행되는 것을 의미한다. 이때 동시에 진행된다는 개념은 병렬적으로 처리가 되든 아니면 시분할 방식으로 처리가 되든 관계가 없다. 그리고 동시성을 구현하기 위해서는 전통적인 선점형 (preemptive) 방식의 thread 나 상대적으로 최근에 도입되기 시작한 fiber 나 coroutine 같은 협조적 (cooperative) thread 를 쓸 수도 있고, 아니면 process 를 기본 단위로 쓸 수도 있다. 이 때 process 는 동일 장비에서 동작하는 local process 일 수도 있고, 다른 장비에서 동작하는 remote process 일 수도 있다.

그러나 여기서 주의해야될 것은 위에 언급한 동시성 구현단위를 쓴다고 실제로 동시성이 개선되는 것은 아니라는 것이다. 컴퓨터 운영체제 교재에 소개가 되는 “식사하는 철학자” 문제는 이를 단적으로 잘 반영한다. 해당 문제를 간략히 요약하면 철학자들 사이에 포크가 하나씩 놓이게 되고, 한 철학자는 양옆의 포크를 모두 사용해서 식사를 해야된다. 이 때 언젠가는 모든 철학자가 포크를 하나씩 들고 옆 사람이 포크를 놓기를 기다리는 상황이 발생할 수 있다.

5월_대표님 컬럼_image2

그림 2: 식사하는 철학자들. 각 철학자 사이에 포크가 하나씩 놓여있고, 철학자는 식사를 하기 위해서 오른쪽과 왼쪽 포크를 모두 써야된다. 이 때 식사를 하다가 모두 포크 하나씩을 들고 옆 사람이 포크를 놓기를 기다리는 데드락 상황이 발생하게 된다. (출처: By Benjamin D. Esham / Wikimedia Commons, CC BY-SA 3.0, https://commons.wikimedia.org/w/index.php?curid=56559)

여기서는 각 철학자가 밥을 먹는 동시성의 단위가 된다. 그러나 이 식사하는 철학자 문제에는 더 많은 철학자를 집어 넣는다고 앞에서 언급한 데드락 문제가 해결되지 않는다. 즉 동시성 단위를 추가하더라도 반드시 더 많은 일을 처리할 수 있게 되는 것이 아닌 것이다. 따라서 동시성이라는 것은 둘 이상의 작업을 동시에 진행하지만, “의미있게” 진행하는 것으로 이해하는 것이 옳다.

따라서 더 나은 동시성을 위해서는 여러 작업들 간의 간섭과 의존성을 최소화해서 서로 blocking 되는 것을 피하는 것이 바람직하다. 이를 위해서 1) 각 작업은 가급적 비동기로 처리 되어야 하며 2) 작업들간에 직렬화되는 지점을 줄이는 것이 중요하다.

그런데 우리가 멀티 쓰레딩에 사용하는 lock 구조는 작업들간의 직렬화를 최소화하는 것과는 거리가 멀다. Lock 의 경우 “둘 사이에 문제가 생길 것 같으니 일단 이 지점에서 lock 이라는 것을 들고 시작할 때까지 기다려” 같은 방식으로 동작하기 때문이다.

Lock 처럼 동시에 여러 작업을 처리하더라도 이것이 순차적으로 처리되게 보장하는 것을 동시성 제어 (concurrency control) 이라고 하는데, lock 과 같은 방법은 그 중에서도 비관적 동시성 제어 (pessimistic concurrency control) 에 해당한다. 이 방법은 기본적으로 동시성 단위간 경합이 있을 것을 전제하고 동시성 작업들이 공유하는 데이터에 대해서 배타적 권한을 획득하는 방식으로 동작한다. 앞에서 예를 든 프로그래밍에서의 lock 이나 전통적인 RDBMS 에서의 transaction 이 이와 같이 동작한다.

그러나 그 반대의 경우를 가정하는 방식도 있다. 경합은 드물 것이고, 혹시 공용 데이터를 읽고 쓰는 작업을 하더라도 혹시 그 사이에 그 데이터가 다른 작업에 의해서 갱신되었다면 이를 rollback 하고 재시도하는 방식인데, 이를 낙관적 동시성 제어 (optimistic concurrency control) 이라고 한다.

앞에 설명한 낙관적 제어 방법과 비관적 제어 방법은 경합에 대한 전제가 서로 다름을 알 수 있다. 따라서 두 방법이 절대적으로 어떤 것이 옳다는 말을 하기는 어렵다. 경합이 많은 환경에서 낙관적 제어 방법을 도입할 경우 무의미하게 계속해서 rollback 을 하고 재시도하는 것이 많아질 수도 있고, 경합이 적은 환경에서의 비관적 제어 방법은 전체적인 Throughput 을 떨어뜨릴 수 있기 때문이다.

이번 글에서는 고용량 서버 구현을 위해서 비효율성을 줄이고 동시성을 늘려야 된다는 점을 강조했다. 그리고 동시성 제어에 대해 기존에 많이 쓰이던 비관적 동시성 제어 외에 낙관적 동시성 제어에 대한 개념적인 설명을 했다. 다음에 기회가 될 때에는 낙관적 동시성 제어를 구현하는 방법에 대해서 알아보도록 하겠다.

아이펀팩토리 문대경 CEO

답글 남기기

댓글을 게시하려면 다음의 방법 중 하나를 사용하여 로그인 하세요:

WordPress.com 로고

WordPress.com의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Google+ photo

Google+의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Twitter 사진

Twitter의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Facebook 사진

Facebook의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

%s에 연결하는 중