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

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

Source: Dev.to

2015년, 실시간이며 연산량이 많은 웹 애플리케이션을 구축하려면 일시적인 클라우드 환경의 제약을 피할 수 없었습니다. Heroku는 PaaS의 절대적인 왕이었지만, 라우터가 30초라는 엄격한 타임아웃을 강제했습니다. 사용자가 활성 세션 내에서 무거운 상태 기반 데이터 세트를 처리해야 할 때, 해당 작업을 웹 dyno에서 수행할 수 없었습니다.

해결책은 맞춤형 클라우드 네이티브 워커 티어를 도입하는 것으로, 다음과 같은 특징을 가집니다:

  • 사용자 세션당 전용 프로세스를 생성합니다.
  • 세션 데이터를 메모리에 유지합니다.
  • Redis를 통해 웹 dyno와 비동기적으로 통신합니다.

아래는 Node.js, Socket.IO, Redis, 그리고 Heroku Platform API를 사용해 이 시스템을 설계하는 실용적인 가이드입니다.

아키텍처 개요

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

환경 전략

환경프로비저닝 메커니즘예시
로컬 개발Node.js 자식 프로세스child_process.fork('worker.js')
Heroku 프로덕션Heroku 플랫폼 API를 통한 일회성 다이노POST /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 채널을 통해 웹 다이노에 다시 전달됩니다.

워커 라이프사이클 관리

One‑off dynos are billed by the second, so orphaned workers must be cleaned up promptly.
원‑오프 다이노는 초 단위로 청구되므로, 고아 워커는 즉시 정리해야 합니다.

장부 관리

  • Mapping – The web dyno stores a Redis hash: Session_ID → Worker_Dyno_ID (or local PID).
    Mapping – 웹 다이노는 Redis 해시를 저장합니다: Session_ID → Worker_Dyno_ID (또는 로컬 PID).

  • Heartbeats – Each worker periodically writes a heartbeat key with a TTL (e.g., worker:xyz:hb).
    Heartbeats – 각 워커는 주기적으로 TTL이 설정된 하트비트 키를 기록합니다 (예: worker:xyz:hb).

정리 전략

TriggerAction
클라이언트 연결 해제웹 다이노가 워커에게 "terminate" JSON‑RPC 명령을 보냅니다.
웹 다이노 충돌워커가 하트비트 누락을 감지하고 자체 유휴 시간을 모니터링한 뒤, 유휴 상태가 되면 process.exit(0)을 호출합니다.
유휴 시간 초과워커가 자동으로 종료되어 Heroku가 원‑오프 다이노를 종료하고 청구를 중단합니다.

결론

다음 요소들을 결합함으로써:

  • Heroku’s Platform API를 사용한 온‑디맨드 dyno 프로비저닝,
  • Redis transactional polling을 통한 신뢰성 있는 메시지 전달, 그리고
  • In‑memory bitwise computation을 이용한 초고속 필터링,

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

0 조회
Back to Blog

관련 글

더 보기 »