Heroku에서 상태 저장형 세션 기반 워커 티어 구축 (2015년 기준)
Source: Dev.to
번역하려는 전체 텍스트를 제공해 주시면, 원본 포맷과 코드 블록을 그대로 유지하면서 한국어로 번역해 드리겠습니다.
Architecture Overview
전통적인 백그라운드 작업 큐(예: Celery, Resque)와 달리, 익명 워커가 상태 없는 작업을 가져가는 방식이 아니라 사용자 세션과 워커 프로세스 사이에 1:1 매핑이 필요합니다. 사용자가 WebSocket을 통해 연결하면 시스템은 전용 워커를 할당하고, 해당 워커는 사용자‑특정 데이터셋을 메모리로 로드한 뒤 명령을 대기합니다. 이후의 계산 및 필터링 작업은 거의 지연 없이 수행됩니다.
환경 전략
| 환경 | 프로비저닝 메커니즘 | 예시 |
|---|---|---|
| 로컬 개발 | Node.js 자식 프로세스 | child_process.fork('worker.js') |
| Heroku 프로덕션 | Heroku Platform API를 통한 일회성 dyno | POST /apps/{app}/dynos (예: node worker.js --session=xyz) |
추상적인 WorkerFactory 가 차이를 숨기고, 애플리케이션 나머지에 일관된 인터페이스를 제공합니다.
워커 프로비저닝 흐름
- Web dyno가 새로운 세션을 감지 → 인증된 HTTP 요청을 Heroku Platform API에 보내 일회성 dyno를 부팅합니다.
- 개발 환경에서는, 웹 프로세스가 API 속도 제한을 피하고 테스트 속도를 높이기 위해 로컬 자식 프로세스를 포크합니다.
Redis를 이용한 메시지 큐
Heroku dyno 간의 직접적인 프로세스‑대‑프로세스 통신은 기본적으로 지원되지 않으므로, Redis가 브로커 역할을 합니다.
메시지 흐름
- Ingress – 클라이언트가 Socket.IO를 통해 웹 dyno에 명령을 보냅니다.
- Queue – 웹 dyno는 명령을 JSON‑RPC 2.0 페이로드로 포맷하고,
session:xyz:queue와 같은 세션‑전용 Redis 리스트에LPUSH로 푸시합니다. - 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 페이로드를 파싱하고 해당 메모리 내 작업을 호출합니다.
인메모리 비트마스크 필터링
복잡하고 다면적인 필터링(예: 겹치는 기준을 가진 수천 개 항목의 카탈로그)의 경우, 워커는 바이너리 마스킹을 사용합니다:
- 각 항목에 해당 속성을 나타내는 비트마스크를 할당합니다.
- 사용자 필터를 목표 비트마스크로 변환합니다.
- 인메모리 배열에 대해 비트 연산자(예:
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
| Trigger | Action |
|---|---|
| Client disconnects | Web dyno sends a "terminate" JSON‑RPC command to the worker. |
| Web dyno crash | Worker detects missing heartbeats, monitors its own idle time, and calls process.exit(0) when idle. |
| Idle timeout | Worker exits automatically, causing Heroku to shut down the one‑off dyno and stop billing. |
결론
다음을 결합함으로써:
- Heroku의 Platform API를 사용한 온‑디맨드 dyno 프로비저닝,
- Redis 트랜잭션 폴링을 통한 신뢰성 있는 메시지 전달, 그리고
- 인‑메모리 비트 연산을 이용한 초고속 필터링,
2015년대 Node.js 애플리케이션은 HTTP 타임아웃을 우회하고, 실시간 고부하 데이터 처리를 제공하며, 비용을 억제하면서도 막대한 성능 향상을 달성할 수 있었습니다.