serversim
학습시뮬레이터템플릿가격
…
…
메뉴 열기
학습↗시뮬레이터↗템플릿↗가격↗
내 Documents문서
목차/11단원11 / 16

서버의 방법 ② — 이벤트루프

기다리는 일 하나마다 사람을 한 명씩 붙여 두는 게 꼭 필요할까요? 직원 한 명이 여러 곳에 전화를 걸어 두고 벨이 울리는 것만 받으면, 자리 하나로도 여러 통화를 동시에 붙잡을 수 있습니다.

기초에서 먼저 보기이벤트루프는 언제 유리하고 언제 불리할까?→

발상을 뒤집어 봅시다. 기다림을 스레드가 붙잡고 있어야 할 이유가 있을까요? "이 작업이 끝나면 이 함수를 실행해 줘"라고 적어두고 스레드는 즉시 다음 일을 하러 가면 됩니다. 그러면 기다림은 잠든 스레드가 아니라 목록에 적힌 한 줄이 됩니다. 이것이 이벤트루프입니다.

① 콜백 — 나중에 부를 함수를 미리 건네기

아래 코드에서 db.query()는 결과를 기다리지 않고 곧바로 반환합니다. 대신 "결과가 오면 실행할 함수"를 함께 넘깁니다. 이렇게 나중에 불릴 목적으로 건네는 함수를 콜백이라고 합니다. 핸들러 함수 자체는 순식간에 끝나고, 스레드는 즉시 다음 요청을 처리하러 갑니다.

② 이벤트루프의 정체는 while 문 하나

두 번째 코드 상자가 이벤트루프의 전부입니다. 무한 루프를 돌면서 완료된 이벤트를 큐에서 꺼내고, 거기 매달린 콜백을 실행합니다. 새 요청이 도착한 것도 이벤트, 디비 응답이 도착한 것도 이벤트입니다. 마법 같은 이름과 달리 구조는 놀랄 만큼 단순합니다.

③ 커널의 도움 — 한꺼번에 물어보기

연결이 1만 개일 때 "이 연결에 데이터 왔니?"를 하나씩 물어보면 그것만으로도 CPU가 바닥납니다. 그래서 운영체제가 "이 1만 개 중에 준비된 것만 알려줘"라고 한 번에 물어보는 방법을 제공합니다(리눅스의 epoll, macOS의 kqueue). 이벤트루프가 실용적이 된 결정적 이유가 이것입니다.

④ 무엇이 좋아졌나 — 10단원과 비교

같은 요청량을 스레드 하나로 처리합니다. 화면 아래 숫자를 10단원과 비교해 보세요. 완료 속도는 비슷하거나 오히려 낫지만, 스레드 메모리는 4.0에서 1.x로 떨어지고 갈아타기 횟수는 0에 가깝습니다. 이 모형은 루프 안에서 요청을 바꾸는 일을 OS 스레드 교대로 세지 않습니다. 실제 루프 스레드도 OS에 선점되거나 이벤트 대기 중 잠들 수 있습니다.

⑤ epoll이 실제로 해주는 일

루프가 매번 모든 연결을 확인한다면 연결 수에 비례해 느려집니다. 예전의 select가 그랬습니다. epoll과 kqueue는 관심 있는 소켓을 커널에 미리 등록해 두고, 준비된 것만 목록으로 돌려줍니다. 그래서 연결이 만 개여도 실제로 준비된 열 개만 받아옵니다. 이벤트루프가 현실적인 선택이 된 것은 이 커널 기능이 나온 다음입니다.

⑥ 백프레셔 — 빨리 받는 것이 항상 좋지는 않다

루프는 요청을 아주 빨리 받아들일 수 있습니다. 문제는 받아들인 만큼 처리할 수 있느냐입니다. 처리 속도보다 수용 속도가 빠르면 대기 중인 콜백과 버퍼가 메모리에 쌓이다가 결국 터집니다. 그래서 리액티브 스택은 소비자가 감당할 수 있는 만큼만 요청하게 하는 장치를 둡니다. 이것을 백프레셔라고 합니다.

핸들러 (콜백 스타일) — 등록하고 즉시 반환
1handle(req) {
2 id = parse(req)① CPU 일함
3 db.query(id, (user) => {② "끝나면 불러줘" 등록만
4 res.send(toJson(user))③ 나중에 루프가 실행
5 })
6} // 기다리지 않고 바로 끝!
이벤트루프의 정체 — 이게 전부다
1while (true) {
2 ev = queue.take() // 완료된 IO, 새 요청...
3 ev.callback() // 해당 콜백 실행
4}
속도요청 수

시뮬레이터에서 해보기

  1. 1.'리액티브 스택' 프리셋의 WebFlux 카드를 선택하세요. ioWorkerCount는 이벤트루프 스레드 수입니다. 요청이 DB 응답을 기다리는 동안 다른 요청을 진행할 수 있습니다.
  2. 2.부하·seed·실행 시간을 고정하고 기본 설정의 CPU·처리량·지연을 기록하세요. ioWorkerCount를 줄인 뒤 초기화하고 같은 시간 동안 실행해 지표를 비교합니다.
  3. 3.실행 자리가 부족하면 계산이 밀릴 수 있고, 이미 충분하다면 늘려도 개선이 작을 수 있습니다. 다른 프레임워크와 CPU를 비교할 때는 요청 비용과 설정도 달라지므로 그 차이를 스레드 수 하나의 효과로 해석하지 마세요.
시뮬레이터에서 실험해보기 →

이 단원의 용어

논블로킹(non-blocking)
지금 처리할 수 없으면 기다리지 않고 상태나 오류를 반환하는 방식. 소켓 읽기에서는 EAGAIN 같은 결과를 받고 나중에 다시 시도한다.
콜백(callback)
"나중에 이 일이 끝나면 실행해 줘"라고 미리 건네는 함수.
이벤트 큐
완료된 IO, 새 요청 등 처리 대기 중인 사건들이 쌓이는 줄.
C10K 문제
한 서버에서 동시 접속 1만 개를 감당하려면 어떻게 해야 하는가라는 1999년의 화두.
epoll / kqueue
등록된 소켓의 읽기·쓰기 준비 상태를 알려주는 커널 기능. I/O 완료 통지와는 다르며, 반환된 이벤트는 프로그램이 각각 처리한다.
백프레셔
소비 속도에 맞춰 수용 속도를 제한하는 장치. 없으면 큐가 무한히 자란다.

왜 이렇게 발전했나

엔진엑스가 아파치를 성능으로 앞선 이유, Node.js와 Redis가 스레드 하나로 수만 개의 연결을 감당하는 이유가 모두 이 구조입니다. 스레드 수를 늘려 동시성을 사는 대신, 기다림을 자료구조로 바꿔 메모리를 아낀 것입니다.

이벤트루프 = 기다림을 스레드 밖(목록)으로 빼기.

남은 문제

공짜는 아닙니다. 코드가 콜백으로 쪼개져 읽기 어려워집니다. 먼저 그 문제부터 해결해 봅시다 →

← 이전 단원

10단원. 서버의 방법 ① — 스레드풀

다음 단원 →

12단원. 코루틴 / async-await — 콜백을 동기 모양으로