본문 바로가기

TechLog

Tornado-test-c10m: 대규모 TCP / WebSocket 동시 연결 테스트에 대한 기록

이 글의 내용을 요약하면, "대량의 TCP / WebSocket 동시 연결(최소 65K 이상, 가능하다면 10M)을 처리하기 위한 환경을 구성하고 Tornado를 사용해 이를 테스트하는 간단한 프로그램을 만들어 보았다”이다. 일반적으로 재미있을만한 주제가 아니므로, 관심이 없는 내용이라면 뒤로 가기 버튼을 눌러 재빨리 빠져나가도록 하자.

이 작업을 시작한 이유는, 그 동안 개인 프로젝트(Fountain)를 진행하면서 Scale-up에 대해 생겼던 궁금점을 해결하기 위함이었다. 대량의 웹 트래픽 처리에 관심이 있는 사람이라면 C10K problem(=10,000개 이상의 네트워크 동시 연결Concurrent connection을 지원하는 서버를 구축하는 문제)에 대해 들어본 적이 있겠지만, 사실 유닉스 계열 운영체제에서 이 문제는 시스템에서 사용 가능한 file descriptor 숫자만 늘려주면 된다. 예전에야 컴퓨터의 리소스가 한정되어 있어 극단의 최적화가 필요했겠지만, 어쨌든 요즘 나오는 PC라면 최대 fd 수만 늘려줌으로써 (성능이야 어떻든) 10K 이상의 소켓 연결을 처리할 수 있다.

나는 TCP / WebSocket 연결을 지원하는 리얼타임 서버를 만들어야 했었고, Tornado로 이걸 구현하기로 했다. fd 숫자를 늘리고, local port range를 조정하고, 간단한 서버 / 클라이언트 코드를 작성해서 50,000개의 연결이 제대로 생성되고, 메시지를 주고받는 것을 확인하였다. 이게 작년의 이야기. (C10K tuning에 관련된 내용은 구글에 검색해 보면 많이 있으므로 그 내용을 참고하시라)

그리고 시간은 흘러서 얼마 전 모 회사에서 인터뷰를 할 일이 있었는데, 리얼타임 서버에 대한 이야기가 나왔다. 이 시점까지 나는 리얼타임 서버가 50K 이상의 연결을 처리할 일이 있을까에 대한 생각 자체를 해보지 않았다. 예를 들어보자면, 안드로이드를 지원하는 Push notification 서버 혹은 Messaging server 같은 경우 실제 트래픽은 상대적으로 적지만 대량의 연결을 계속 유지해야 하는 경우가 있다. 이런 유형의 서버가 있을 때 Scale-up을 어떻게 할지에 대한 질문을 받은 것이었다. inbound traffic을 모니터링하면서 상대적으로 가벼운 인스턴스를 계속 spawn하면 되지 않겠느냐 했지만 그것은 올바른 대답이 아니었다(…)

계속 인터뷰는 이어져서 내 개인 프로젝트에 대한 이야기가 나왔을 때, 내 서버가 얼마나 많은 동시 연결을 처리할 수 있냐는 질문에 ’5만개 연결까지는 테스트해봤지만 아직 그 이상 scale-up에 대해서는 아직 고민해보지 않았는데요’고 대답하니 ‘5만개요? (시무룩)’이라는 반응이 돌아왔다. 여기서 좀 당황했는데, 이때까지 나는 5만 개 이상의 동시 연결을 처리하는 서버를 구현할 일이 있을까라는 생각 자체를 해 본 적이 없기 때문이었다. 아니 이제 5만 개 동시 연결을 처리하는 걸로도 부족한 시대가 되었단 말인가!

새삼 궁금해져서 집에 돌아와 관련 내용을 검색해보니 이런 아티클을 찾을 수 있었다:
Whatsapp은 어떻게 5억 명의 사용자, 11,000개의 cpu 코어, 초당 7천만개의 메시지 처리까지 성장하였나 (영문) http://highscalability.com/blog/2014/3/31/how-whatsapp-grew-to-nearly-500-million-users-11000-cores-an.html

위 글을 읽어보면 알겠지만, 해당 글이 쓰인 시점에서 Whatsapp은 150대의 챗 서버로 1억 5천만 개의 동시 연결을 처리하고 있었다. 챗 서버 한 대당 1백만 개의 동시 연결을 처리하고, peak time에는 150만개 연결을 처리하기도 했다고.

이걸 읽고 나서 처음 든 생각은, ‘그래, 그런 서버를 구현한다고 치고, 테스트는 어떻게 하지?’ 였다. OS가 개별 소켓을 식별하는 방법을 알고 있는 사람이라면 금방 이해하겠지만, 소켓은 보통 local address와 remote address 두 값의 쌍으로 식별되며 (address는 192.168.0.10:8080처럼 ip:port 형태로 표현된다), 이 두 값이 중복되는 소켓은 존재할 수가 없다. 만약 한 개의 호스트 내에서 8080 포트를 listen하는 서버를 실행하고, 클라이언트를 실행해서 여러 개의 소켓을 생성해 이 포트에 연결하면 다음과 같은 주소를 갖는 소켓들이 생성된다:

local addressremote address
192.168.0.10:20001
192.168.0.10:8080
192.168.0.10:20002192.168.0.10:8080
192.168.0.10:20003
192.168.0.10:8080
192.168.0.10:20004
192.168.0.10:8080

이게 뭐가 문제냐면, 포트 번호는 0~65,535까지로 한정되어 있기 때문에, 이 범위의 포트 번호를 전부 사용하고 나면 더 이상 local address를 위한 포트 번호를 할당할 수 없게 된다. 그러므로 이상적인 상황을 가정할 때 로컬에서 실행한 서버에 연결할 수 있는 최대 연결 수는 65,536개라는 이야기. (물론 실제로는 이런저런 제약 때문에 그 숫자가 더 작아진다) 물론, 서버 한 대와 클라이언트 여러 대가 포함된 테스트 환경을 구축한다면 이 문제는 피할 수 있다. local address에 여러 IP를 사용할 수 있다면 그 IP 개수 만큼 전체 연결 개수를 늘릴 수 있으니까. 하지만 서버 하나 테스트 하겠다고 그런 환경을 구축한다는 건 좀 에러고...

그래서 이를 피하기 위해 이용한 꼼수는, 서버가 여러 포트를 listen하게 하는 것. 그렇게 하면 이런 식으로 소켓을 생성할 수 있다:

local addressremote address
192.168.0.10:20001
192.168.0.10:8080
192.168.0.10:20001192.168.0.10:8081
192.168.0.10:20002
192.168.0.10:8080
192.168.0.10:20002
192.168.0.10:8081

위와 같이, local address가 같더라도 remote address가 다르면 소켓을 식별할 수 있으므로, 서버에 할당한 listen port 수 * 50K 만큼의 연결을 만들어 테스트할 수 있는 것이다. 천만 개의 소켓을 테스트하고 싶다? 서버에 listen port를 200개 할당하면 된다. 즉, 200 * 50,000 = 10,000,000 (10M). 호스트에 여러 IP를 할당하는  것도 한 가지 방법이기는 하지만, 네트워크 설정을 수동으로 변경해야 하므로 귀찮다.


어쨌든 이런 방법을 사용하면 개발 환경에서도 10M 연결을 테스트할 수 있다. 하지만 주의사항이 있는데, 어떤 이유에서인지 모르겠지만 OS X에서는 이 방법을 사용해도 65K 이상의 연결을 생성할 수 없다. netstat으로 확인한 결과 OS X에서는 remote address가 서로 다르더라도 local address가 중복되는 경우가 없으며, 무조건 local address를 사용하여 소켓을 식별하는 것으로 추정된다. (추정일 뿐이라 정확하지 않을 수 있다. 이 방식을 변경할 수 있는 설정이 어딘가 있을 것 같기도 한데, 불행하게도 찾지 못했다) Ubuntu Server 14.10에서는 확실히 동작하는 것을 확인했으므로, 삽질이 귀찮다면 Ubuntu Server 14.10를 사용하시기를 권한다. 뭔가 잘못되어도 내가 도울 수 있는 부분이 없다;

그러면 이제 OS 튜닝으로 넘어가야 하는데… 사실 이 튜닝에 대한 방법은 OS마다 서로 다르고, 어떤 종류의 서버를 구현하느냐에 따라 필요한 설정이 달라지는 관계로 스스로 검색하면서 필요한 내용을 찾아보는 수 밖에 없다. 내 경우만 해도 Ubuntu와 OS X를 같이 세팅해야 했던 상황이라 설정 내용을 공유하기도 그렇고… 이 작업을 하면서 도움이 되었던 링크 몇 개를 공유하는 것으로 대신한다 (모두 영문):

- The C10K problem

- Performance Tuning the Network Stack on Mac OS X Part 2

Linux TCP/IP tuning for scalability

- RED HAT ENTERPRISE LINUX 7 PERFORMANCE TUNING GUIDE (CentOS에도 적용 가능)

The C10M(!) problem



그리고 이를 테스트하기 위해 급조한 코드를 여기 공유한다:

위 코드는 TCP / WebSocket을 지원하는 Echo 서버와 클라이언트로 구성되어 있는데, 클라이언트에는 대량의 소켓을 생성하거나 메시지를 전송할 수 있는 기능이 구현되어 있다. Python으로 작성되어 있고, 사용하려면 Tornado를 설치해야 한다. PyPy를 사용할 경우 성능 향상이 있기는 한데, 이 프로그램이 하는 일은 대부분 I/O에 관련된 작업이다보니 패킷 전송에는 그다지 성능 향상이 없었고, 연결 생성에는 상당한 성능 향상이 있었다.

Ubuntu Server 14.10에서 실행해 본 소감은, 메모리를 무지하게 먹는다. 위 프로그램을 실행해보면 TCP는 연결당 10.5KB, WebSocket은 연결당 15.5KB 정도가 할당된다. 이는, 만약 1백만 개의 연결을 생성하면 TCP 연결의 경우 1,000,000 * 2 * 10,500 = 21,000,000,000B = 21GiB의 메모리를 사용한다는 의미. (중간에 2를 곱한 것은 소켓이 생성될 때 서버에서 한 개, 클라이언트에서 한 개가 생성되기 때문이다) WebSocket의 경우에는 오버헤드가 더 있어서 31GiB 정도를 사용한다는 계산이 나온다. 1천만 개의 연결이라면? 각각 210GiB, 310GiB가 될 것이다. 케냘 소유의 PC 중에서는 21GB가 넘는 메모리를 가진 시스템이 없기 때문에 1백만 개의 연결 생성조차 테스트할 수가 없었고(…) EC2에 인스턴스를 생성해서야 테스트가 가능했다. 테스트에는 r3.4xlarge 타입의 인스턴스(122GiB memory)가 사용되었다.

실행 결과 화면을 첨부하는 것으로 이 글을 마무리할까 한다.