Ruby에서의 동시성: 프로세스, 스레드, 파이버 활용 전략

XO Ruby Chicago 2025 - Concurrency in Ruby: From fork() to Fiber by Yuri Bocharov

작성자
jeff
발행일
2025년 12월 21일

핵심 요약

  • 1 루비는 프로세스, 스레드, 파이버 세 가지 주요 도구를 통해 동시성을 지원하며, 각 도구는 고유한 특성과 최적화된 사용 사례를 가집니다.
  • 2 루비의 GVL(Global VM Lock)은 CPU 바운드 작업 시 스레드의 병렬 실행을 제한하므로, IO 바운드 작업에 스레드가 유리하고 CPU 바운드 작업에는 프로세스 확장이 효과적입니다.
  • 3 초기 성능 문제의 원인은 CPU 바운드 작업에 과도한 스레드를 할당한 것이었으며, 이는 컨텍스트 전환 오버헤드를 증가시키고 실제 병렬 처리를 방해했습니다.

도입

본 발표는 실제 업무에서 발생한 성능 문제를 해결하는 과정에서 루비 동시성의 깊은 이해가 필요했음을 배경으로 합니다. 초기 문제는 PDF 생성 및 발송을 처리하는 백그라운드 작업 큐가 과도한 스레드 할당에도 불구하고 오히려 느려지는 현상이었습니다. 발표자는 이 문제를 해결하기 위해 루비의 동시성 모델을 탐구하게 되었으며, 이 강연을 통해 동시성의 정의, 루비에서 제공하는 세 가지 주요 동시성 프리미티브(프로세스, 스레드, 파이버)의 작동 방식, 그리고 실제 서버 환경에서의 적용 및 성능 개선 사례를 다룹니다. 궁극적으로는 초기 문제의 근본 원인과 올바른 해결책을 제시하는 것을 목표로 합니다.

동시성(Concurrency)과 병렬성(Parallelism)

  • 동시성: 동시에 여러 작업을 ‘처리’하는 개념입니다. 예를 들어, 요리사가 파스타를 삶고, 소스를 만들고, 양파를 써는 등 여러 작업을 번갈아 가며 진행하는 것과 같습니다.

  • 병렬성: 동시에 여러 ‘행동’을 취하는 개념입니다. 이는 하드웨어(예: CPU 코어)의 지원을 받아 실제로 여러 작업을 동시에 실행하는 것을 의미합니다. 양파 써는 작업을 세 배로 늘리려면 세 명의 사람이 필요하듯, 컴퓨터에서는 추가 CPU 코어가 필요합니다.

루비의 동시성 프리미티브

루비는 동시성 구현을 위해 주로 세 가지 프리미티브를 제공합니다.

1. 프로세스 (Process)

  • 특징: 운영체제가 제공하는 독립적인 실행 단위입니다. 각 프로세스는 고유한 메모리 공간, 파일 핸들, 프로세스 ID 등을 가집니다. fork를 통해 새로운 프로세스를 생성할 수 있습니다.

  • 오버헤드: 프로세스를 생성하는 데는 많은 오버헤드가 발생합니다. 모든 자원을 복제하거나 공유해야 하기 때문입니다.

  • 병렬성: 프로세스는 진정한 병렬 처리를 가능하게 합니다. 각 프로세스는 별도의 CPU 코어에서 독립적으로 실행될 수 있습니다.

  • 최적 활용: CPU 코어 수에 맞춰 정적인 수의 프로세스를 생성하는 것이 효율적입니다. 웹 서버에서는 Unicorn이나 Pitchfork와 같은 pre-forking 방식이 이에 해당합니다. CPU 코어 수를 초과하는 프로세스는 성능 저하를 유발합니다.

2. 스레드 (Thread)

  • 특징: 프로세스 내에서 실행되는 경량 실행 단위입니다. 부모 프로세스의 메모리 공간, 파일 핸들 등 많은 자원을 공유하므로 프로세스보다 생성 오버헤드가 적습니다.

  • GVL (Global VM Lock): C Ruby에서 스레드는 GVL의 제약을 받습니다. GVL은 한 번에 하나의 스레드만이 루비 코드를 실행할 수 있도록 허용합니다. 따라서 루비 코드(예: 피보나치 계산)는 스레드를 사용해도 병렬로 실행되지 않고 직렬로 처리됩니다.

  • IO 바운드 작업: GVL은 IO 작업(예: sleep, 데이터베이스 호출) 중에는 해제될 수 있습니다. 이 경우 다른 스레드가 루비 코드를 실행하거나 다른 IO 작업을 수행할 수 있어, IO 바운드 작업에서는 스레드가 병렬성을 제공합니다.

  • 최적 활용: IO 바운드 작업이 많은 경우 스레드 풀을 사용하여 효율성을 높일 수 있습니다. Puma와 같은 웹 서버는 프로세스와 스레드를 결합하여 이 모델을 활용합니다.

3. 파이버 (Fiber / Co-routine)

  • 특징: 스레드 내에서 실행되는 가장 경량화된 실행 단위입니다. 스레드보다 훨씬 저렴하며, 하나의 스레드 안에 수많은 파이버를 생성할 수 있습니다.

  • 협력적(Cooperative): 파이버는 선점형(preemptive) 스레드와 달리, 스케줄러가 강제로 제어를 빼앗지 않습니다. 파이버는 명시적으로 yield를 호출하거나 IO 작업이 발생할 때 자발적으로 제어권을 넘겨야 합니다.

  • 스케줄러: 루비에는 내장된 파이버 스케줄러가 없으므로, async 젬과 같은 외부 라이브러리를 사용해야 합니다.

  • 한계: CPU 바운드 작업에서 한 파이버가 장시간 실행되면 다른 파이버의 실행이 지연될 수 있습니다. IO 바운드 작업에 매우 효율적입니다.

  • 최적 활용: Falcon과 같은 파이버 기반 서버는 이러한 특성을 활용하여 높은 동시성을 달성합니다. 특히 IO 작업이 많은 환경에서 뛰어난 성능을 보입니다.

초기 문제의 해결

발표 초기에 언급된 ‘PDF 생성 큐가 스레드 수를 늘려도 느려지는’ 문제는 CPU 바운드 작업(PDF 생성)에 과도하게 많은 스레드를 사용했기 때문입니다. GVL로 인해 20개의 스레드가 있더라도 한 번에 하나의 스레드만 루비 코드를 실행할 수 있었고, 나머지 스레드는 대기 상태에 있었습니다. 또한 100밀리초마다 발생하는 불필요한 컨텍스트 전환 비용이 누적되어 오히려 성능이 저하되었습니다. 해결책은 CPU 코어 수에 맞춰 프로세스 수를 늘려 진정한 병렬 처리를 가능하게 하는 것이었습니다.

결론

결론적으로 동시성은 여러 작업을 동시에 '처리'하는 것이고, 병렬성은 여러 '행동'을 동시에 취하는 것으로, 병렬성은 하드웨어 자원을 요구합니다. 루비는 프로세스, 스레드, 파이버라는 세 가지 핵심 도구를 제공하여 동시성을 관리합니다. 프로세스는 진정한 병렬 처리를 제공하지만 오버헤드가 크고 CPU 코어 수에 맞춰 제한적으로 사용해야 합니다. 스레드는 IO 바운드 작업에 효율적이지만 GVL로 인해 CPU 바운드 루비 코드는 직렬로 실행됩니다. 파이버는 가장 경량화되고 협력적인 모델로, IO 바운드 작업에 매우 유리하지만 명시적인 제어권 양도가 필요합니다. 따라서 루비 애플리케이션의 성능을 최적화하기 위해서는 워크로드의 특성(CPU 바운드 vs. IO 바운드)을 정확히 파악하고, 각 동시성 프리미티브의 장단점을 고려하여 적절한 도구를 선택하고 구성하는 것이 중요합니다.

댓글 0

로그인이 필요합니다

댓글을 작성하거나 대화에 참여하려면 로그인이 필요합니다.

로그인 하러 가기

아직 댓글이 없습니다

첫 번째 댓글을 작성해보세요!