원문 캡처 · github.com
원문 캡처 · github.com

Mummy는 Nim으로 작성한 멀티스레드 HTTP 1.1 및 WebSocket 서버다. 설치 명령은 nimble install mummy이며, API 참조를 확인할 수 있다.

제작자는 서버 하드웨어의 성능을 최대한 활용하면서도 개발자가 편리하게 작업할 수 있도록 Mummy를 만들었다고 설명한다. HTTP keep-alive와 gzip 응답 압축을 자동으로 지원하고 WebSocket 기능을 내장했다. {.async.}를 사용하지 않고 소켓 입출력을 다중화한다.

Mummy를 사용하려면 Nim 컴파일 때 --threads:on과 --mm:orc 또는 --mm:arc를 지정해야 한다. 이름은 고대 이집트와 관련된 것으로, 프로젝트 문서의 이미지를 가리킨다.

설계와 적용 대상

Mummy는 소켓 입출력을 한 스레드에서 처리하고, 들어오는 HTTP 요청과 WebSocket 이벤트를 작업자 스레드 풀에 전달하는 구조다. 제작자에 따르면 HTTP 핸들러는 스레드를 직접 다루지 않아도 될 수 있다. 이 설계는 서버 CPU 코어 수가 계속 늘어나는 상황을 활용하도록 고안됐으며, 제작자는 AMD가 96코어 192스레드 서버 CPU를 발표한 사례를 들었다.

Mummy는 {.async.}, Future[], await 등을 사용하지 않아도 되며, 비동기 함수와 일반 함수의 사용 방식이 달라지는 문제를 피한다고 제작자는 설명한다. 다중화된 비차단 소켓 입출력의 처리량은 유지하면서, 블로킹 또는 비용이 큰 호출 하나가 서버 전체를 멈추게 할 우려는 줄인다는 주장이다. 제작자는 비동기 방식이 DNS 조회나 파일 읽기처럼 예상하기 어려운 작업에서 멈춰 모든 요청 처리를 지연시킬 수 있다고 지적한다.

요청 핸들러는 일반 Nim 코드로 작성하며, 스레드를 막는 작업도 허용된다고 설명한다. 예를 들어 PostgreSQL 쿼리 결과를 기다릴 수 있다. 오류 처리는 일반적인 try except를 사용할 수 있고, 핸들러에서 처리되지 않은 예외가 발생해도 서버 전체가 중단되지는 않는다고 한다. 다중 코어와 Nim 팀의 ARC·ORC 및 Nim 2.0 작업을 활용하는 점도 장점으로 든다.

제작자는 Mummy를 REST, JSON, RPC, WebSocket, 템플릿 기반 HTML처럼 메모리 사용량이 비교적 적은 API 서버에 적합한 제품으로 소개한다. 반면 큰 파일을 많이 제공하거나 대용량 파일 업로드를 처리할 경우에는 서버 메모리가 충분하지 않다면 적합하지 않을 수 있다. 요청을 메모리에서 완전히 받은 뒤 작업자 스레드로 전달하고, 응답도 메모리에서 전송하는 구조이기 때문이다.

Mummy와 함께 쓰도록 만든 라이브러리로는 HTTP RPC에 유용하며 libcurl을 효율적으로 사용하게 해주는 Curly와 멀티스레드 서버용 Redis 클라이언트인 Ready가 있다.

HTTP·WebSocket 서버 실행 예시

다음은 HTTP 서버 예시다.

nim
import mummy, mummy/routers

proc indexHandler(request: Request) =
 var headers: HttpHeaders
 headers["Content-Type"] = "text/plain"
 request.respond(200, headers, "Hello, World!")

var router: Router
router.get("/", indexHandler)

let server = newServer(router)
echo "Serving on http://localhost:8080"
server.serve(Port(8080))

컴파일하고 실행하는 명령은 다음과 같다.

nim
nim c --threads:on --mm:orc -r examples/basic_router.nim

다음은 WebSocket 서버 예시다.

nim
import mummy, mummy/routers

proc indexHandler(request: Request) =
 var headers: HttpHeaders
 headers["Content-Type"] = "text/html"
 request.respond(200, headers, """
 <script>
 var ws = new WebSocket("ws://localhost:8080/ws");
 ws.onmessage = function (event) {
 document.body.innerHTML = event.data;
 };
 </script>
 """)

proc upgradeHandler(request: Request) =
 let websocket = request.upgradeToWebSocket()
 websocket.send("Hello world from WebSocket!")

proc websocketHandler(
 websocket: WebSocket,
 event: WebSocketEvent,
 message: Message
) =
 case event:
 of OpenEvent:
 discard
 of MessageEvent:
 echo message.kind, ": ", message.data
 of ErrorEvent:
 discard
 of CloseEvent:
 discard

var router: Router
router.get("/", indexHandler)
router.get("/ws", upgradeHandler)

let server = newServer(router, websocketHandler)
echo "Serving on http://localhost:8080"
server.serve(Port(8080))

실행 명령은 다음과 같다.

nim
nim c --threads:on --mm:orc -r examples/basic_websockets.nim

examples/ 폴더에는 추가 샘플 코드와 WebSocket 채팅 서버 예시가 있다.

성능과 벤치마크

제작자는 HTTP 서버 성능 비교를 러닝화 비교에 비유한다. 나쁜 신발은 분명 있지만, 적당한 신발을 고른 뒤에는 신발보다 달리는 사람, 즉 실제 핸들러가 무엇을 하는지가 중요하다는 설명이다. 이에 따라 다음 세 가지를 우선순위로 제시한다.

  • 선택한 HTTP 서버가 성능을 불필요하게 떨어뜨리지 않는지 확인한다.
  • 쉽게 성능 문제를 일으킬 수 있는 HTTP 서버를 피한다.
  • 빠르고 안정적인 핸들러를 작성하고 유지하는 데 도움이 되는 방식을 우선한다.

제작자는 Mummy가 이 세 기준을 충족한다고 본다. 요청 수신과 전달, 응답 전송을 효율적으로 처리하고 불필요한 메모리 복사를 피하며 CPU가 핸들러 작업에 집중하도록 설계했다는 주장이다. 또한 비동기 방식과 같은 다중화 입출력을 사용하므로 전통적인 멀티스레드 서버가 취약할 수 있는 저속·저빈도 공격(low-and-slow)에 취약하지 않다고 설명한다. 단일 블로킹 또는 CPU 집약적 작업이 전체 비동기 서버를 멈추게 하는 문제도 Mummy에서는 발생하지 않는다고 주장한다. 핸들러가 요청을 받아 응답하는 단순한 일반 Nim 코드라 유지보수성·신뢰성·성능에 유리하다는 설명도 덧붙였다.

벤치마크는 Ubuntu 22.04 서버의 4코어·8스레드 CPU에서 실행했다. 테스트 대상인 tests/wrk_ 서버들은 약 10ms가 걸리는 요청을 모사하도록 구성됐다. 모든 벤치마크에 사용한 명령은 다음과 같다.

text
wrk -t10 -c100 -d10s http://localhost:8080

각 서버의 빌드·실행 명령과 초당 요청 수는 다음과 같다.

  • Mummy — nim c --mm:orc --threads:on -d:release -r tests/wrk_mummy.nim; 초당 요청 수: 9,547.56
  • AsyncHttpServer — nim c --mm:orc --threads:off -d:release -r tests/wrk_asynchttpserver.nim; 초당 요청 수: 7,979.67
  • HttpBeast — nim c --mm:orc --threads:on -d:release -r tests/wrk_httpbeast.nim; 초당 요청 수: 9,862.00
  • Jester — nim c --mm:orc --threads:off -d:release -r tests/wrk_jester.nim; 초당 요청 수: 9,692.81
  • Prologue — nim c --mm:orc --threads:off -d:release -r tests/wrk_prologue.nim; 초당 요청 수: 9,749.22
  • NodeJS — node tests/wrk_node.js; 초당 요청 수: 8,544.60
  • Go — go run tests/wrk_go.go; 초당 요청 수: 9,171.55

이 수치는 위 서버 환경과 요청 모사 조건에서 나온 결과다. 제작자는 HTTP 서버 벤치마크만으로 실제 핸들러 성능을 판단하기 어렵다는 점도 강조한다.

테스트와 사용 사례

Mummy의 소켓 읽기 및 파싱 코드를 대상으로 퍼저를 실행해 잘못된 소켓 데이터가 들어왔을 때 서버가 충돌하거나 오작동하지 않는지 확인했다고 프로젝트 문서는 설명한다. 퍼저는 다음 명령으로 실행할 수 있다.

nim
nim c -r tests/fuzz_recv.nim

프로젝트 문서에는 실제 사용 사례로 소형 가상 머신에서 CPU와 RAM을 거의 사용하지 않고 초당 500건 이상 HTTP 요청을 처리한 사례와 동시 WebSocket 연결 10만 개를 처리하고 100만 개 이상까지 확장할 여지가 있다는 사례가 소개돼 있다.