[Server] Event loop와 Multi-threading의 조합

장성호·2023년 9월 2일

[Server]

목록 보기
1/6
post-thumbnail

Event loop와 Multi-threading의 조합을 접하다

Spring boot에서 MVC로 사이드 프로젝트를 개발하던 도중, RestTemplate의 deprecated 문제 때문에 WebClient를 사용하라고 해서 잠깐 Spring WebFlux를 만질 일이 생겼다. 근데 deprecated 이유를 알아보니 HTTP 작업을 처리할 때, RestTemplate는 blocking이고 WebClinet는 non-blocking으로 동작하기 때문에 WebClinet를 사용하라는 것 같았다. Event loop로 동작하나 싶어서 찾아보니, WebFlux는 Event loop 모델을 채택해서 동작하도록 설계해놓았었다.

프론트 진영에서 일하고 있다보니 javascript나 dart가 event loop를 쓰는 건 알았어도, java가 event loop를 사용한다니까 굉장히 새로워보였다. 최근 Flutter Forward 2023에서 dart server framework도 소개 되었으니까, 이번 기회에 각 언어별 서버 프레임워크의 성능이 어떤지 알아보고 싶었다. techempower 사이트에 들어가서 일단 순위를 한 번 보았다.

빠르다고 소문난 Go랑 Java, Javascript로 찾아봤는데 spring이랑 nodejs 순위가 생각보다 많이 낮아서 놀랬다. 그리고 vertx라는 프레임워크랑 Kotlin의 조합이 생각보다 순위가 엄청 높길래 신기했다. 그래서 이김에 Go, Java, Javascript, Dart로 필터를 변경해서 순위를 산출해봤다.

Dart는 순위가 귀엽다... 이 두 사진 중에서 가장 흥미로웠던 거는 vertx라는 프레임워크였다. 사이트나 문서도 잘 되어있고 한국에서도 꽤나 유명한 프레임워크인 것 같았다. 특히 techempower 사이트 기준 Spring이나 Node.js랑 지표 차이가 월등하길래 이유가 궁금했다.

찾아보니 Event loop와 Worker Verticle라는 Thread pool이 핵심인 것 같았다. 그럼 Multi-threading이라는 이야기인데 event loop와 multi-threading의 조합이 얼마나 강력한 시너지를 내는건지, javascript나 dart 환경에서는 이게 가능할지가 궁금해졌다.

Web Workers, worker_thread

Javascript에서 찾은 키워드는 Web Workers와 worker_thread이었고, 둘은 다음과 같은 차이점이 있었다.

  • Web Workers는 multi-threading 기반으로 웹 브라우저에서 비동기 처리를 위해 사용한다.
  • worker_thread는 node.js에 추가된 multi-threading 모듈이다.

요즘 Thread

Web Workers 같은 경우에는 multi-threading인지, multi-processing 인지 하도 헷갈려서, MDN Web Docs 문서를 뒤지던 도중 다음과 같은 글귀를 발견했다.

Several runtimes communicating together
A web worker or a cross-origin iframe has its own stack, heap, and message queue. Two distinct runtimes can only communicate through sending messages via the postMessage method. This method adds a message to the other runtime if the latter listens to message events.

Worker global contexts and functions
Workers run in a different global context than the current window!

About thread safety
The Worker interface spawns real OS-level threads, and mindful programmers may be concerned that concurrency can cause "interesting" effects in your code if you aren't careful.

Worker interface가 OS 수준의 thread를 가진다는데, 서로 다른 전역 context에서 실행되면서 stack, heap도 자신만의 것이 있다고 한다. OS 강의 때 배운 software thread는 Stack만 고유 영역을 가지는 거라서, 내 기준 Web worker는 multi-processing처럼 느껴졌다. 좀 더 찾아보니 Go나 Javascript처럼 고수준 언어에서는 thread를 다르게 정의한다고 한다.

찾아본 걸 정리해본 결과 위 사진과 같다. MDN 문서를 볼 때 Multi-processing처럼 느껴지는 이유가 있었다. 위를 바탕으로 IPC처럼 message나 shared memory를 활용해 multi-threading을 진행할 수 있다. 참고로 Vert.x 같은 경우에는 JVM 환경에서 실행되기 때문에, thread가 나뉘어도 heap은 공유된다. 'Write Once, Run Anywhere' 철학 + GC 관리 일원화 때문이라고 한다.

Vert.x와 Node.js의 속도 차이

techempower 사이트 표에서 알 수 있듯이 Vert.x랑 Node.js의 속도 차이가 심하게 많이 난다. Node.js에서 multi-threading를 사용했는지 여부는 알 수가 없기는 하지만, 그 이유에 대해서 이래저래 찾아봤다.

  1. 클라이언트 연결
    • Vert.x는 한 process 내에서 여러 개의 event loop를 만들어, 클라이언트 연결을 각 event loop가 처리 가능
    • Node.js는 한 process 내에서 여러 개의 event loop를 만들 수 있지만, 클라이언트 연결은 main thread만 관여 => Vert.x와 같은 동작을 수행하고 싶으면 process fork가 필요
  2. Context switching 비용
    • Vert.x는 thread 단위로 스케쥴링 되기 때문에 context switching 비용이 적음. (stack, data)
    • Node.js는 Vert.x처럼 하려면 process 단위로 스케쥴링 되기 때문에 context switching 비용이 많음. (메모리 구조 전체)
  3. 이벤트 루프에 대한 로드밸런싱 관리 주체
    • Vert.x는 한 process 내에서의 event loop group을 형성해서 사용하기 때문에, Vert.x가 알아서 event loop 로드밸런싱 진행
    • Node.js는 여러 process가 각각 event loop를 가지기 때문에, event loop에 대한 로드밸런싱을 하고 싶으면 OS나 별도의 도움을 받아야함.

우리 dart는요?


Multi-threading이 반영된 건지는 알 수 없지만 느리다. 그래도 앞의 글을 열심히 읽었고, dart의 동시 환경을 이해하고 있다면 결론이 금방 나온다.

출처: https://dart.dev/language/concurrency

Node.js의 worker_thread와 같은 single event loop 모델이다. 그렇기 때문에 Vert.x와 Node.js를 비교와 유사하다.

Spring WebFlux랑 Vert.x는?

결론부터 말하면 둘 다 Netty 서버를 사용하고, Netty 서버는 CPU 코어 수에 따라 여러 개의 event loop를 만들어 event loop group을 지원할 수 있다. 따라서 둘 다 클라이언트 요청을 event loop group에 배분할 수 있다. 이러한 Netty 서버의 기능을 바탕으로 구현된 것이 서로 다를 뿐이다. 따라서 위에서의 'Vert.x vs 다른 서버' 비교는 정확히 'Netty 서버 vs 다른 서버' 비교로 하는 것이 더 정확한 것 같다.

후기

프론트 쪽에서 multi-threading 키워드로 작성된 글들을 읽다 보면, 아무리 봐도 multi-processing인 데 전부 다 multi-threading이라고 적혀 있다. 그래서 multi-processing인지 multi-threading 인지 제대로 알기 위해서 조사를 시작한 것도 있다.

가장 많이 배운 것은 변해가고 있는 thread의 개념이다. 내가 생각했던 거는 OS 수준에서의 thread 개념이고, Application 수준에서의 thread 정의는 각 진영에 입맛에 맞게 바뀌어 가고 있다는 것을 깨달았다. 앞으로는 Concurrency를 공부할 땐 이번처럼 용어 정의 및 메모리 구조를 명확히 짚고 넘어가는 습관을 들여야겠다. Goroutine이나 Coroutine 같은 메커니즘에서는 thread가 또 다른 거 같았기 때문이다. OS thread였나..?

그리고 Flutter Forward 2023 때 dart server framework가 나왔다고 하길래 그 때는 굉장히 환호했다. Dart가 강력해지는구나 싶었지만 이번 포스팅을 통해, 클라이언트 맞춤 언어의 근본 철학이 server 성능을 잡기에는 한계가 있을 수 밖에 없다는 것도 알았다. 이것도 사용자가 좀 많아졌을 때의 이야기지만 말이다. (인프라에 돈 발라서 scale out하면 되는거 아니에요!?) Java 진영의 Loom project처럼 VM 레벨에서 Virtual thread 이야기도 많이 나오던데, 이 포스팅은 추후 살을 좀 더 붙여가면서 수정해야겠다. 실제로 코드도 돌려보면서 비교해보는 걸로!

References

MDN Web Docs on Web Workers
MDN Web Docs on The event loop
HTML Living Standard on Web Workers
Vert.x Core on Standard Verticles
Dart on Concurrency
Concurrency in Spring WebFlux

profile
일벌리기 좋아하는 사람

0개의 댓글