Rinda Ops 알림 전수조사 리포트

2026-06-11 · alpha / beta 4건 알림 분석 (PostgreSQL 커넥션 ×3, Docker 메모리 ×1) · BullMQ worker 96종 전수조사 포함

TL;DR

알림 4건 중 긴급 조치 필요는 beta 단 1건(커넥션 누수). beta elysia 메모리 99% CRITICAL은 오진단(실제 99%는 postgres-1 page cache, elysia는 12%·OOM 0). alpha 커넥션은 DB_POOL_MAX=20 + blue-green 2중 가동에 의한 구조적 정원초과(누수 아님). worker 폭주는 없음 — DB는 pool당 20 hard-cap. 다만 beta Redis(1.52M keys·peak 5G·noeviction)가 모니터에 안 잡힌 진짜 잠재 리스크.

alpha PostgreSQL
68 /100
WARNING 정원초과·누수아님
beta PostgreSQL
78 /100
누수 확정 고아 커넥션 20
beta elysia 메모리
12 %
오진단 알림값 99%는 PG
beta Redis (미알림)
5.0 G peak
잠재 1.52M keys·noeviction
WARNING

alpha PostgreSQL 커넥션 69/100

판정: 커넥션 누수 아님 — 풀 사이징 + blue-green 2중 가동에 의한 구조적 정원초과. 활성 쿼리 0건, idle-in-transaction 0건. DB는 한가한데 풀이 커넥션만 점유.

점유 주체 (application_name)role / hostconns정체
rinda-apipostgres / .1520elysia 컨테이너 A (pool max 20 만재)
rinda-workerpostgres / .1611–20BullMQ 워커
rinda-apipostgres / 115.91.133.18712–13외부 dev 머신 직결
rinda-api-analyticsanalytics_reader / .155분석 풀 (상한 정상)
(background)8autovacuum/checkpointer 등 정상

근본 원인 (3중 중첩)

  1. DB_POOL_MAX=20 env 오버라이드 — 코드 기본값은 10 (config.ts:177)
  2. blue-green 컨테이너 -1161(08:33) + -1162(08:39) 둘 다 running → API만 풀 40
  3. 외부 dev 머신(115.91.133.187)이 운영 정원 13 잠식
정원 계산: API 20×2 + worker 20 + analytics 5 + dev 13 + bg 8 ≈ 66–70 → 임계 60·피크 69와 일치
  • P1DB_POOL_MAX 10으로 환원 (Infisical alpha) — API 점유 40→20, 즉시 임계 아래
  • P1blue-green 구 컨테이너 정리 확인 — 1161/1162 동시 생존이 전환완료 후 상태인지 점검 (old stop 누락 의심)
  • P2외부 dev 머신(115.91.133.187) alpha DB 직결 회수 — 로컬 DB 사용 안내
누수 확정

beta PostgreSQL 커넥션 78/100

판정: 진짜 커넥션 누수. 죽은 worker 컨테이너(IP 172.18.0.8)의 DB 풀 20개가 graceful shutdown 없이 좀비로 잔존. 재배포마다 +20 누적되는 구조.

client_addrappbackend_startconns정체
172.18.0.11rinda-worker08:36:3820현 worker (정상)
172.18.0.8rinda-worker08:18–08:3420고아 — 존재하지 않는 IP, ClientRead 영구대기
172.18.0.9rinda-api~23현 elysia (정상)
현 실행 컨테이너 IP: .5 .6 .7 .9 .10 .11 — .8은 어디에도 없음. OS TCP 소켓 0 = half-open(클라 죽음·PG는 모름).

근본 원인

  1. worker 재배포(08:36, IP .8→.11) 시 SIGTERM 핸들러에서 DB pool.end() 미호출 → 구 풀 20개 좀비화
  2. PG idle_session_timeout·tcp_keepalives 미설정 → half-open 자동회수 안 됨
  3. 증폭: 앱 컨테이너 다수 × pool 20 → 헤드룸 빡빡
  • P0고아 커넥션 즉시 정리SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE client_addr='172.18.0.8'; (20개 회수→즉시 해소)
  • P1worker SIGTERM에 pool graceful shutdown 추가worker.close() + pool.end() 연결 (재발 차단·근본해결)
  • P2PG idle_session_timeout(10min) + tcp_keepalives_idle 설정 — 좀비 자동회수 방어선
  • P2(중기) 풀 헤드룸 확보 또는 PgBouncer(transaction pooling) 검토
오진단 / FALSE ALARM

beta Docker elysia-server 메모리 99% CRITICAL

판정: 긴급 아님 — 모니터의 컨테이너 오매핑 + cache 포함 메트릭. 99%로 잡힌 건 elysia가 아니라 postgres-1이고, 그마저도 누수가 아니라 reclaimable page cache.

컨테이너Mem%분석
elysia-server (알림 대상)752 MiB / 6 GiB12%건강. anon 909MB, heap 에러 0
postgres-1 (실제 99%)7.21 GiB / 8 GiB90%anon 1GB + page cache 7.16GB + shared_buffers 2GB
redis-11.52 GiB / 10 GiB15%정상
RestartCount 0 · OOMKilled false · cgroup oom_kill 0 · postgres 6일 무중단. memory.events max 10.4M = harmless reclaim(OOM 아님). 호스트 30GiB 중 21GiB available.

근본 원인

  1. 모니터가 elysia 라벨에 postgres 압력을 오매핑 (컨테이너 인덱스 혼동 의심)
  2. "99%"는 cgroup memory.current raw = 회수가능 page cache 포함. 실 working-set 아님
  • P1알림 메트릭 수정 — working-set = memory.current − inactive_file 로 계산 (postgres 99%→30~40%, 반복 false CRITICAL 차단)
  • P1모니터 컨테이너 매핑 수정 — postgres 압력이 elysia 라벨로 보고되지 않도록
  • P2(선택) postgres 메모리 limit 8G→10~12G 상향 — reclaim churn 완화 (cosmetic)
폭주 아님

worker (BullMQ) 전수조사

판정: worker 폭주에 의한 커넥션/메모리 증가 가설 기각. DB pool은 프로세스당 20 hard-cap이라 concurrency가 커넥션을 곱하지 않음. 단, 모니터에 안 잡힌 beta Redis가 진짜 잠재 리스크.

지표alphabeta비고
Redis used / peak332M1.27G / 5.08Gmaxmemory 8G · noeviction
Redis keys (DBSIZE)236K1.52Mretention + stale send-acc 잔여
connected / blocked clients8 / 0564 / 179bzpopmin = BullMQ 정상 blocking poll
worker DB conns4020= 컨테이너수 × pool 20 (cap 작동)
최대 active job (큐)325lead-on-demand-enrich = concurrency 정확히 일치
sequence-email delayed = alpha 1,513 / beta 99,644 → ZSET score가 now~+91일 분포 = 정상 미래예약 발송(적체 아님). concurrency는 단일 pool에 queue될 뿐 커넥션을 각자 잡지 않음.

주목할 진짜 리스크

  1. beta Redis 1.52M keys / peak 5.08G / noeviction — 8G maxmemory에 근접 시 OOM → worker 전면 stall 연쇄 가능
  2. 구동요인: BullMQ completed/failed retention(큐당 최대 1000) + 99k delayed set + 죽은 계정의 stale send-acc-* 키 잔여
  3. beta lead-on-demand-enrich waiting 2,426 (draining 중, failed 1 — 양호)
  • P1beta Redis 위생removeOnComplete/removeOnFail retention 축소 + 죽은 send-acc-* 키 GC (live는 1~3개뿐)
  • P2비핵심 키 클래스에 maxmemory-policy 안전장치 또는 >6G 알림 추가 — noeviction OOM 캐스케이드 방지
  • P2lead-on-demand-enrich 배수 추세 재확인 — 평탄/증가 시 enrich 상류 점검
전수 96종

BullMQ 워커 종류 전수조사

worker.ts가 부팅하는 start*Worker() = 총 96종 + worker-buyersearch 전용 컨테이너 + per-account 동적 spawn(send-acc-*). 전부 단일 20-slot DB pool 공유 → concurrency 합과 무관하게 커넥션은 컨테이너당 20 cap.

지표의미
총 워커 종류96+ buyersearch 컨테이너 + 동적 send-acc-*
concurrency: 1 (단일)~45절반이 직렬 — 커넥션 압박 거의 없음
concurrency: 2~5~30경량 병렬
고-concurrency 소수4sequence-email 40 · lead-on-demand-enrich 25 · web-extraction 20 · personalized-email-streaming 20
고-concurrency 워커도 단일 pool에 queue될 뿐 각자 커넥션을 잡지 않음. pool(20)이 throttle. Redis는 워커당 전용 connection → beta 564 clients/179 blocked(bzpopmin, 정상)의 원인.

도메인별 워커 분류 (11개 그룹)

도메인주요 워커
시퀀스·발송22sequence-email(40)·sequence-email-loader·sequence-activation·sequence-lifecycle·sequence-proposal-{ai-completion,commit,preview-email}·enrollment-resume·bulk-email·personalized-email-streaming(20)·outbox-dispatcher·followup-email·follow-up-{auto-send,daily-digest,draft-generate}·nudge-email·auto-nudge·reply-nudge·re-engagement-nudge·ooo-resend·recipient-send-time-learning·per-account-worker-manager
워밍업6warmup-{send,scheduler,engage,reputation,recovery,bounce-retry}
AI 에이전트10agent-{approval-expiry,approval-handler,goal-orchestrator,memory-archival,memory-extract,mission,mission-postmortem,wisdom-curator}·sales-advisor-auto·sales-autopilot
리드·바이어 발굴/인리치16buyer-search-{agent-v1,pipeline,pro}·lead-discovery-{analyze,search}·lead-on-demand-enrich(25)·lead-csv-export·lead-pipeline-gmail-backfill·hot-lead-evaluation·visitor-enrich·web-extraction(20)·group-analysis-v2·group-assessment·scoring-preset-generate·legal-address-suggest·business-card-ocr
답장·CRM 분류6reply-classification-outcomes·reply-tags-auto-classify·crm-stage-classify·crm-email-backfill·thread-summary-reconcile·unreplied-reply-digest
LinkedIn SDR5linkedin-sdr-{outreach,profile-view,scheduler}·unipile-inbox-poll·unipile-inactive-cleanup
이메일 인프라·평판6ses-monitor·ses-suppression-sync·sendgrid-suppression-sync·verify-sending-domain·dns-health-check·api-key-monitor
빌링·라이프사이클9billing-payment·paddle-webhook-debug·trial-{expiration,intervention}·proxy-workspace-expiry·account-deletion·customer-group-deletion·dsar-fulfillment·onboarding-auto-generate
알림·브로드캐스트·다이제스트7admin-notification-broadcast·board-{email-broadcast,notification}·release-note-{email,notification}·weekly-digest·customer-share-report-refresh
콘텐츠·퍼블리시4blog-pipeline·cross-publish·recording-transcription·standup-translation
벤치마크·시스템5benchmark-pipeline·benchmark-quick-run·server-health-monitor·db-cleanup·test
합계 96종. 동적: per-account-worker-manager가 활성 발송계정당 Worker 1개 spawn(concurrency = SEQ_EMAIL_MAX_SLOTS_PER_ACCOUNT, worker 컨테이너 12 / 그 외 6), 5분 idle 후 despawn. live send-acc-* = alpha 3 / beta 1 (수백 개 meta 키는 stale 잔여, GC 대상).

종합 우선순위

  • P0beta 고아 커넥션 20개 즉시 terminate — 유일한 긴급 건. 알림 즉시 해소
  • P1worker graceful pool shutdown (beta 누수 근본차단) + alpha DB_POOL_MAX 10 환원 & blue-green 정리
  • P1모니터 2건 수정 — 메모리 메트릭(cache 차감) + 컨테이너 매핑. 반복 false CRITICAL의 근원
  • P2beta Redis 위생 — 모니터에 안 잡힌 진짜 잠재 OOM 리스크 선제 정리