웹훅 수집을 자체 호스팅된 n8n + Postgres로 옮긴 이유: 패킷 손실 제로를 위한 4단계 DLQ 아키텍처
요약
미션 크리티컬한 웹훅 처리는 일반적인 n8n 환경에서 타임아웃이나 부하로 인한 데이터 손실 위험이 있습니다. 본문은 이러한 문제를 해결하기 위해 PostgreSQL을 활용한 4단계 디커플링 파이프라인 아키텍처를 제시합니다. 이 패턴은 원자적 중복 제거와 전용 DLQ(Dead-Letter Queue)를 통해 웹훅의 내구성과 손실 제로 처리를 보장합니다.
핵심 포인트
- 표준 n8n 환경은 이벤트 폭주 시 타임아웃 및 데이터 손실 위험이 있습니다.
- 4단계 디커플링 파이프라인을 통해 웹훅 수집의 내구성을 확보해야 합니다.
- PostgreSQL의 ON CONFLICT 기능을 활용하여 원자적 중복 제거를 구현합니다.
- 전용 DLQ 테이블을 두어 글로벌 오류 발생 시 실행 가로채기 및 기록을 수행합니다.
만약 Stripe 체크아웃, GitHub 이벤트 또는 API 수집과 같은 미션 크리티컬한 웹훅(webhook)을 처리하기 위해 n8n을 자체 호스팅(self-host, Docker, Portainer 또는 베어 메탈 환경)한다면, 아마도 표준적인 실패 모드에 직면했을 것입니다:
- 타임아웃 연쇄 반응 (Timeout cascades): Webhook 노드가 다운스트림 워크플로우 노드의 완료를 기다릴 경우, 이벤트가 폭주하면 504 Gateway Timeouts 또는 n8n 러너 타임아웃이 발생합니다. 이로 인해 전송 서비스는 백오프(back off)하거나 엔드포인트를 비활성화하게 됩니다.
- 부하 하에서의 데이터 손실 (Data loss under load): 다운스트림 서비스(CRM, 데이터베이스, 서드파티 API 등)가 429 속도 제한(rate limit)에 걸리거나 네트워크 오류가 발생하면, 기본 동작은 실행을 종료시킵니다. 또한, 자체 호스팅된 SQLite/Postgres DB를 간결하게 유지하기 위해 실행 데이터 가지치기(data pruning) 기능이 활성화되어 있다면, 원본 페이로드(raw payload)는 영원히 사라집니다.
여기서는 PostgreSQL 백업을 사용하는 자체 호스팅 n8n에서 99.99%의 웹훅 내구성(durability), 원자적 중복 제거(atomic deduplication), 그리고 손실 제로 데드 레터 큐(Dead-Letter Queue, DLQ)를 달성하는 정확한 프로덕션 패턴을 소개합니다.
아키텍처: 4단계 디커플링 파이프라인 (The Architecture: 4-Tier Decoupled Pipeline)
text [수신 웹훅 버스트] │ ▼
┌─────────────────────────────────────────────────────────┐
│ 1. 수집 계층 (Webhook 노드) │
│ - 응답 모드: '수신 시(On Received)' (즉시 200 OK) │
│ - 40ms 미만으로 응답하며 지연 시간을 분리합니다. │
└─────────────────────────┬───────────────────────────────┘
│ ▼
┌─────────────────────────────────────────────────────────┐
│ 2. 원자적 버퍼 및 중복 제거 (PostgreSQL) │
│ - INSERT INTO stripe_event_buffer (...) │
│ - ON CONFLICT (stripe_event_id) DO UPDATE... │
│ - 반환값: is_new_record (경쟁 조건 방지) │
└─────────────────────────┬───────────────────────────────┘
│ ┌─────────────┴─────────────┐ │
│ 새 이벤트(New Event) │ 중복/재실행(Duplicate / Replay) ▼ ▼
┌─────────────────────────┐ ┌───────────────────────────┐ │
│ 3. 다운스트림 워크플로우 │ │ 실행 종료 │
│ - 데이터베이스 업서트 (Database Upserts) │ │ - 서드파티 API │
│ (상태: DUPLICATE_SKIP) │ └───────────────────────────┘
└───────────┬─────────────┘ │
│ ├──────────────────────────┐ ▼ (성공) ▼ (노드 실패 시) ┌─────────────────────────┐ ┌───────────────────────────┐ │
│ UPDATE status='SUCCESS' │ │ 4.
글로벌 오류 트리거 (Global Error Trigger)
│ └─────────────────────────┘ │
- 실행 가로채기 (Intercepts Execution) │
│ - DLQ 테이블에 기록 (Writes to DLQ Table) │
│ - 온콜(On-Call)에게 알림 전송 (Alerts On-Call) │
│ - 재실행 페이로드 준비 (Preps Replay Payload) │
└───────────────────────────┘
핵심 기술 상세 내용 (Key Technical Details)
- 디커플링된 수집 (Decoupled Ingestion)
다운스트림 지연(downstream latency)이 들어오는 웹훅 연결을 차단하는 일이 없도록 합니다.
- 웹훅 노드(Webhook node)의 응답 모드를 '수신 시 (On Received)'로 설정하고, 응답 코드는 200이며 본문은
{"status": "queued", "received_at": "={{ $now.toISO() }}"}를 사용합니다. - 웹훅 응답 지연 시간이 40ms 미만으로 떨어져 외부 웹훅이 타임아웃되거나 백오프(backing off)하는 것을 방지합니다.
-
PostgreSQL의 원자적 중복 제거 잠금 (Atomic Deduplication Lock in PostgreSQL)
Stripe와 같은 외부 제공업체는 웹훅이 한 번 이상 전달될 수 있음을 명시적으로 지정합니다. 병렬 실행으로 인한 중복 실행을 방지하기 위해 다음 SQL을 사용합니다:
sql INSERT INTO stripe_event_buffer ( stripe_event_id, event_type, payload, status, created_at ) VALUES ( $1, -- event.id $2, -- event.type $3, -- event JSON 'PROCESSING', NOW() ) ON CONFLICT (stripe_event_id) DO UPDATE SET retry_attempt = stripe_event_buffer.retry_attempt + 1 RETURNING id, status, (xmax = 0) AS is_new_record;
만약is_new_record가 false이고 상태가 이미 COMPLETED라면, 워크플로우는 즉시 종료됩니다. 중복 처리가 제로(Zero duplicate processing)입니다. -
글로벌 오류 트리거 하위 워크플로우 (Global Error Trigger Sub-Workflow)
워크플로우 설정에서 오류 워크플로우(Error Workflow)가 전용 데드레터 큐(Dead-Letter Queue, DLQ) 워크플로우를 가리키도록 구성합니다:
- 오류 트리거 노드는
$execution.id,$execution.error.message,$execution.error.stack, 그리고 원본 페이로드(raw payload)를 포착합니다. - 실패한 항목들은 status = 'QUARANTINED' 및 retry_count = 0으로
webhook_quarantine테이블에 기록됩니다.
- 자동 지수 백오프 재실행 (Automated Exponential Backoff Replay)
가벼운 주기적 워크플로우(recurring workflow)가 10분마다 실행됩니다: - 다음 SQL을 쿼리합니다:
SELECT * FROM webhook_quarantine WHERE status = 'QUARANTINED' AND retry_count < 3 AND next_retry_at <= NOW() - 격리된 페이로드(quarantined payload)를 사용하여 실행 하위 워크플로우를 트리거합니다.
- 성공하면 상태를 RESOLVED로 업데이트합니다.
- 반복 실패 시, 백오프 시간 간격(
POW(2, retry_count) * 5 minutes)을 증가시킵니다.
재시도 횟수(retry_count)가 3 이상이면 'DEAD_LETTER_MANUAL_REVIEW'로 표시하고 조치 가능한 알림을 생성합니다.
데이터베이스 스키마 (PostgreSQL)
CREATE TABLE IF NOT EXISTS stripe_event_buffer (
id SERIAL PRIMARY KEY,
stripe_event_id VARCHAR(128) UNIQUE NOT NULL,
event_type VARCHAR(128) NOT NULL,
customer_email VARCHAR(255),
amount NUMERIC(10, 2),
payload JSONB NOT NULL,
status VARCHAR(32) DEFAULT 'PROCESSING',
retry_count INT DEFAULT 0,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS webhook_quarantine (
id SERIAL PRIMARY KEY,
workflow_id VARCHAR(64) NOT NULL,
execution_id VARCHAR(64) NOT NULL,
failed_node VARCHAR(128),
error_message TEXT,
payload JSONB,
status VARCHAR(32) DEFAULT 'QUARANTINED',
retry_count INT DEFAULT 0,
next_retry_at TIMESTAMP WITH TIME ZONE,
resolved_at TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX idx_buffer_stripe_id ON stripe_event_buffer(stripe_event_id);
CREATE INDEX idx_quarantine_status ON webhook_quarantine(status, next_retry_at);
이 패턴은 자체 호스팅된 n8n 인스턴스를 안정적으로 유지하고, 실행 로그를 무기한 보관하여 발생하는 데이터베이스 비대화(database bloat)를 방지하며, 고객 거래 손실이 전혀 없도록 보장합니다.
제출자: /u/mynameisammy [링크] [댓글]
AI 자동 생성 콘텐츠
본 콘텐츠는 r/SelfHosted (AI filter)의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기