Heroku에서 상태 저장형 세션 기반 워커 티어 구축 (2015년 기준)

발행: (2026년 3월 28일 오후 01:43 GMT+9)
6 분 소요
원문: Dev.to

Source: Dev.to

번역하려는 전체 텍스트를 제공해 주시면, 원본 포맷과 코드 블록을 그대로 유지하면서 한국어로 번역해 드리겠습니다.

Architecture Overview

전통적인 백그라운드 작업 큐(예: Celery, Resque)와 달리, 익명 워커가 상태 없는 작업을 가져가는 방식이 아니라 사용자 세션과 워커 프로세스 사이에 1:1 매핑이 필요합니다. 사용자가 WebSocket을 통해 연결하면 시스템은 전용 워커를 할당하고, 해당 워커는 사용자‑특정 데이터셋을 메모리로 로드한 뒤 명령을 대기합니다. 이후의 계산 및 필터링 작업은 거의 지연 없이 수행됩니다.

환경 전략

환경프로비저닝 메커니즘예시
로컬 개발Node.js 자식 프로세스child_process.fork('worker.js')
Heroku 프로덕션Heroku Platform API를 통한 일회성 dynoPOST /apps/{app}/dynos (예: node worker.js --session=xyz)

추상적인 WorkerFactory 가 차이를 숨기고, 애플리케이션 나머지에 일관된 인터페이스를 제공합니다.

워커 프로비저닝 흐름

  1. Web dyno가 새로운 세션을 감지 → 인증된 HTTP 요청을 Heroku Platform API에 보내 일회성 dyno를 부팅합니다.
  2. 개발 환경에서는, 웹 프로세스가 API 속도 제한을 피하고 테스트 속도를 높이기 위해 로컬 자식 프로세스를 포크합니다.

Redis를 이용한 메시지 큐

Heroku dyno 간의 직접적인 프로세스‑대‑프로세스 통신은 기본적으로 지원되지 않으므로, Redis가 브로커 역할을 합니다.

메시지 흐름

  1. Ingress – 클라이언트가 Socket.IO를 통해 웹 dyno에 명령을 보냅니다.
  2. Queue – 웹 dyno는 명령을 JSON‑RPC 2.0 페이로드로 포맷하고, session:xyz:queue와 같은 세션‑전용 Redis 리스트에 LPUSH로 푸시합니다.
  3. Polling – 워커 dyno가 이 리스트를 지속적으로 폴링합니다.

트랜잭션을 이용한 안전한 디큐

워커가 충돌했을 때 메시지를 잃지 않도록, 워커는 모든 대기 중인 명령을 원자적으로 읽고 큐를 비우는 Redis 트랜잭션을 사용합니다.

// worker-poll.js (Node.js)
redis.multi()
  .lrange('session:xyz:queue', 0, -1) // Get all pending messages
  .ltrim('session:xyz:queue', 1, 0)   // Empty the list
  .exec((err, results) => {
    if (err) {
      console.error('Redis transaction failed:', err);
      return;
    }
    const messages = results[0];
    if (messages && messages.length) {
      processMessages(messages);
    }
  });

*processMessages*는 각 JSON‑RPC 페이로드를 파싱하고 해당 메모리 내 작업을 호출합니다.

인메모리 비트마스크 필터링

복잡하고 다면적인 필터링(예: 겹치는 기준을 가진 수천 개 항목의 카탈로그)의 경우, 워커는 바이너리 마스킹을 사용합니다:

  1. 각 항목에 해당 속성을 나타내는 비트마스크를 할당합니다.
  2. 사용자 필터를 목표 비트마스크로 변환합니다.
  3. 인메모리 배열에 대해 비트 연산자(예: AND)를 적용합니다.
// Example: filtering with bitmasks
const results = items.filter(item => (item.mask & filterMask) === filterMask);

V8이 비트 연산을 네이티브 속도로 실행하기 때문에, 워커는 수만 건의 레코드를 십밀리초 수준으로 필터링할 수 있습니다. 결과 ID는 Redis pub/sub 채널을 통해 웹 다이노로 다시 푸시됩니다.

Worker Lifecycle Management

One‑off dynos are billed by the second, so orphaned workers must be cleaned up promptly.

Bookkeeping

  • Mapping – The web dyno stores a Redis hash: Session_ID → Worker_Dyno_ID (or local PID).
  • Heartbeats – Each worker periodically writes a heartbeat key with a TTL (e.g., worker:xyz:hb).

Cleanup Strategies

TriggerAction
Client disconnectsWeb dyno sends a "terminate" JSON‑RPC command to the worker.
Web dyno crashWorker detects missing heartbeats, monitors its own idle time, and calls process.exit(0) when idle.
Idle timeoutWorker exits automatically, causing Heroku to shut down the one‑off dyno and stop billing.

결론

다음을 결합함으로써:

  • Heroku의 Platform API를 사용한 온‑디맨드 dyno 프로비저닝,
  • Redis 트랜잭션 폴링을 통한 신뢰성 있는 메시지 전달, 그리고
  • 인‑메모리 비트 연산을 이용한 초고속 필터링,

2015년대 Node.js 애플리케이션은 HTTP 타임아웃을 우회하고, 실시간 고부하 데이터 처리를 제공하며, 비용을 억제하면서도 막대한 성능 향상을 달성할 수 있었습니다.

0 조회
Back to Blog

관련 글

더 보기 »