Files
web-page-backend/docs/superpowers/specs/2026-06-29-distributed-worker-observability-design.md
gahusb f0fad05f2d docs: 분산 워커 관측 시스템(NAS↔Windows) 설계 스펙 추가
music/video/image/insta-render + task-watcher + ai_trade의 heartbeat 기반
관측, agent-office /nodes 집계 API + 텔레그램 경보, web-ui Three.js 파이프라인
시각화를 다루는 3-repo 설계. heartbeat 키 스키마 + /nodes 응답 스키마를
잠그는 계약으로 정의.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019LV86jBozkNhSFXJA412fq
2026-06-29 17:25:13 +09:00

15 KiB

분산 워커 관측 시스템 (Distributed Worker Observability) — 설계 문서

작성일: 2026-06-29 · 작성 세션: BE (web-backend 소유) 대상 repo 3종: web-ai(워커) · web-backend(NAS 집계/경보) · web-ui(Three.js 대시보드)


1. 문제 정의 (Problem)

NAS 백엔드의 음악/영상/이미지/인스타 생성은 무거운 작업을 Windows AI 머신(192.168.45.59)의 WSL2 Docker 워커에 위임한다. NAS 게이트웨이(music/video/image/insta-lab)가 Redis 큐(queue:<svc>-render)에 job을 push하면, Windows 워커가 BLMOVE로 꺼내 처리하고 /api/internal/<svc>/update webhook으로 결과를 회신한다. 트레이딩봇 ai_trade(:8001)는 별도로 NAS stock(:18500)에서 HTTP pull을 한다.

핵심 문제: 이 분산 워커들이 살아있는지 NAS·사용자가 알 길이 없다.

  • 각 워커에 로컬 /health 엔드포인트가 있으나 Windows 머신 안에서만 접근 가능.
  • 실제 사고: insta-render 워커가 redis 블로킹 read 버그로 2026-05-22 ~ 06-08 약 2주간 사일런트로 죽어 있었고(모든 슬레이트 draft 정지) 아무도 몰랐다. 일감이 없을 때의 "한가함"과 "죽음"을 구분할 수단이 없었던 것이 근본 원인.

2. 목표 / 비목표 (Goals / Non-goals)

목표 (Phase 1)

  • G1. 6개 워커(music/video/image/insta-render + task-watcher + ai_trade)의 생사·상태를 NAS에서 인지.
  • G2. 큐 깊이·실패(dead-letter)·고아작업(processing)·일시정지(paused) 상태를 집계.
  • G3. 상태 전이(다운/복구/실패누적)를 텔레그램으로 자동 경보.
  • G4. web-ui 신규 페이지 /infra에서 NAS↔Windows 파이프라인을 Three.js로 시각화 — 정상이면 통신이 흐르는 애니메이션, 장애면 해당 구간을 끊김/빨강으로 표시.

비목표 (Phase 2 이후로 보류)

  • 원격 제어(워커 재시작, 큐 pause/resume, dead-letter 재처리) — Windows 머신 제어가 필요해 보안·구현 복잡도 큼.
  • GPU 사용률(VRAM) 모니터링, stuck-task 자동 감지, WebSocket 라이브 푸시.
  • 다중 노드 확장(현재 Windows 노드 1대).

3. 아키텍처 & 토폴로지

        web-backend (NAS, 192.168.45.54)              Windows 노드 (192.168.45.59)
   ┌──────────────────────────────────┐          ┌────────────────────────────────────┐
   │  music-lab ─┐                     │  ① job   │  WSL2 Docker:                       │
   │  video-lab ─┤                     │  push    │   ┌─ music-render                   │
   │  image-lab ─┼─► [ Redis 큐 버스 ]═╪══════════╪══►├─ video-render  (ReliableQueue)  │
   │  insta-lab ─┘   queue:*-render    │          │   ├─ image-render                   │
   │                 queue:paused      │◄═════════╪═══├─ insta-render                   │
   │                                   │ ② webhook│   └─ task-watcher (paused 토글)     │
   │  agent-office                     │◄─────────╪──   각 워커 → worker:<name>:heartbeat│
   │   ├─ node_monitor (집계)          │◄─heartbeat   (Redis SET, TTL 45s)              │
   │   └─ scheduler (1분 경보 cron)    │          │                                     │
   │                                   │          │  Windows 호스트(WSL 밖):            │
   │  stock (:18500) ◄── HTTP pull ────╪──────────╪──  ai_trade (:8001) ─ heartbeat ───►│
   └──────────────┬───────────────────┘          └────────────────────────────────────┘
                  │ GET /api/agent-office/nodes  (FE 2~3초 폴링)
                  ▼
        web-ui  /infra  ←  Three.js 파이프라인 시각화

설계 기반(이미 존재하는 자산)

  • 워커들은 이미 NAS Redis(redis://192.168.45.54:6379)에 BLMOVE로 연결 → heartbeat도 같은 Redis에 SET하면 방화벽/인바운드 포트 불필요, queue:paused여도 heartbeat는 계속 뛰므로 "정지 중이지만 살아있음"과 "죽음"을 구분 가능.
  • _shared/reliable_queue.py(ReliableQueue)가 이미 processing:queue:<svc>-render:<worker_id> 리스트와 dead_letter:queue:<svc>-render 리스트를 Redis에 남김 → 집계기가 신규 워커 코드 없이 큐 깊이·실패·고아작업을 읽을 수 있음.

채택하지 않은 대안

  • 집계기를 게이트웨이 중 하나에 배치 → "어느 게이트웨이가 전체 노드 상태를 소유하나"가 의미상 어색. agent-office가 ops 브레인(텔레그램·스케줄러·WebSocket·서비스 로그 수집 보유)이라 의미상 정확.
  • NAS→워커 HTTP /health 폴링 → 워커별 포트 노출 + NAS→Windows 인바운드 접속 필요. Redis heartbeat가 단방향(워커→Redis)이라 더 단순.
  • 라이브 갱신을 WebSocket으로 → Phase 1은 2~3초 폴링으로 충분(단순). WebSocket은 Phase 2 강화.

4. 컴포넌트 설계

4.1 web-ai — heartbeat 생산자 (AI 세션 소유)

4.1.1 render 워커 4종 (services/*-render/)

  • 신규 공용 모듈 services/_shared/heartbeat.py:
    • async def heartbeat_loop(redis, name, stats, interval=15, ttl=45)interval초마다 worker:<name>:heartbeat 키에 JSON 값을 SET ... EX ttl.
    • 값 스키마는 §5.1 참조. 죽으면 키가 TTL 만료 → 집계기가 "missing = dead" 판정.
  • 각 워커 main.py lifespan에서 worker_loop와 함께 heartbeat_loop 태스크 spawn.
  • state 산정: queue:paused가 set이면 paused, 현재 job 처리 중이면 busy, 아니면 idle. 처리 중 여부와 카운터(jobs_done/jobs_failed/last_job_at)는 poll_once가 갱신하는 모듈 레벨 stats 객체로 추적.
  • TTL=45s = interval(15s)의 3배 → 1~2회 누락은 dead로 오판하지 않음.

4.1.2 task-watcher (services/task-watcher/)

  • watcher_loop에 동일 heartbeat 추가. worker:task-watcher:heartbeatstate + 현재 mode(trading/free)를 함께 발행 → 대시보드가 paused의 이유("작업중(트레이딩)")를 표시.

4.1.3 ai_trade (ai_trade/) — 다른 런타임

  • ai_trade는 Windows 호스트에서 직접 uvicorn 실행(WSL Docker 아님), NAS Redis 큐에 연결되어 있지 않음(현재 NAS stock으로 HTTP pull만).
  • 변경: redis.asyncio 의존성 추가 → main.py lifespan에 heartbeat 태스크 추가 → 같은 NAS Redis(192.168.45.54:6379)에 worker:ai_trade:heartbeat SET.
    • Redis는 Windows 머신에서 이미 도달 가능(render 워커들이 같은 호스트에서 BLMOVE 중).
    • heartbeat 로직은 ~10줄이므로 ai_trade 자체 미니 헬퍼로 둔다(_shared import 경로 의존 회피 — render 워커는 컨테이너 PYTHONPATH로 _shared 접근, ai_trade는 호스트 실행이라 경로가 다름). 계약(키 스키마)만 동일하면 코드 공유 불필요.
  • state 의미가 다름: render 워커의 idle/busy/paused가 아니라 market_open(poll_loop 활성·신호 생성 중) / market_closed(휴장·장외 idle). task-watcher의 queue:paused와 무관(트레이딩은 일시정지 대상 아님).
  • 토폴로지 표현: Redis 큐 버스가 아니라 HTTP pull 파이프라인(ai_trade ⇄ NAS stock :18500)으로 별도 표시.

4.2 web-backend / agent-office — 집계기 + 경보 (이 BE 세션 소유)

4.2.1 Redis 클라이언트 추가

  • agent-office는 현재 Redis 미사용 → requirements.txtredis>=5.0(asyncio) 추가, docker-compose.yml agent-office 블록에 REDIS_URL 환경변수 + depends_on: redis 추가.

4.2.2 app/node_monitor.py 신규

  • 워커 레지스트리(상수): 각 워커의 name, 연관 queue(있으면), internal webhook 경로, 토폴로지 link 타입(redis-queue | http-pull).
  • async def collect_status() -> dict:
    • 각 워커: GET worker:<name>:heartbeat → 존재하면 alive=True + JSON 파싱 + last_beat_age_s = now - ts; 없으면 alive=False(dead).
    • 각 render 큐: LLEN queue:<svc>-render(depth), LLEN dead_letter:queue:<svc>-render, processing:queue:<svc>-render:* 키 스캔으로 in-flight 수.
    • GET queue:paused + TTL → paused 플래그 + reason(task-watcher heartbeat의 mode).
    • Redis 연결 실패 → redis_ok=False(전 구간 degrade).
    • link 상태 합성(§5.2).
  • 응답 스키마는 §5.2.

4.2.3 엔드포인트

  • GET /api/agent-office/nodescollect_status(). nginx /api/agent-office/ 이미 라우팅됨 → nginx 변경 불필요.

4.2.4 경보 cron (scheduler)

  • _run_node_health_check (APScheduler, 1분 간격):
    • 직전 상태 _node_state(인메모리 dict)와 비교:
      • alive → dead: 🔴 <name> 워커 다운 (last beat Xs ago)
      • dead → alive: 🟢 <name> 워커 복구
      • dead_letter 카운트가 임계(NODE_ALERT_DEADLETTER_THRESHOLD, 기본 1) 신규 초과: <queue> 실패 누적 N건
    • _notified 패턴(기존 youtube_publisher.poll_state_changes 재사용)으로 스팸 방지, 복구 시 재알림 가능하도록 set 차집합.
    • 텔레그램 발송은 agent-office 기존 봇 재사용.

4.3 web-ui — Three.js 대시보드 (FE 세션 소유)

  • 신규 의존성: three + @react-three/fiber + @react-three/drei(React 코드베이스이므로 r3f가 관용적).
  • 신규 라우트 /infra(Router.jsx) + Nav 등록.
  • pages/infra/InfraMonitor.jsx:
    • r3f <Canvas> 토폴로지 — 좌측 NAS(게이트웨이 sub-node) / 중앙 Redis 큐 버스(글로우 코어) / 우측 Windows 노드(워커 sub-node). ai_trade는 별도 HTTP-pull 파이프라인.
    • 노드 간 파이프라인(튜브) + 상태별 머티리얼/애니메이션(§6).
    • useNodeStatus 훅: GET /api/agent-office/nodes를 2~3초 폴링 → 상태를 시각 상태로 매핑(src/api.js에 헬퍼 추가).
    • 2D 폴백: WebGL 미지원/모바일 대비 카드·테이블 요약 뷰 토글.
    • 실제 구현 시 designer 스킬 활성화(브레인스토밍 단계에서는 금지).

5. 잠그는 계약 (Contracts)

3 세션이 독립 병렬 작업하려면 이 두 스키마만 고정하면 된다.

5.1 Heartbeat 키 스키마

  • : worker:<name>:heartbeat (name ∈ music-render, video-render, image-render, insta-render, task-watcher, ai_trade)
  • (JSON 문자열), SET ... EX 45:
{
  "name": "image-render",
  "kind": "render",          // "render" | "watcher" | "trader"
  "state": "idle",           // render: idle|busy|paused / watcher: trading|free / trader: market_open|market_closed
  "ts": "2026-06-29T12:34:56Z",   // UTC ISO8601 (heartbeat 발신 시각)
  "last_job_at": "2026-06-29T12:30:00Z",  // nullable
  "jobs_done": 42,
  "jobs_failed": 1,
  "mode": "free"             // task-watcher 전용(paused 이유), 그 외 생략 가능
}

5.2 /api/agent-office/nodes 응답 스키마

{
  "redis_ok": true,
  "paused": false,
  "paused_reason": "trading",     // queue:paused가 set일 때 task-watcher mode
  "generated_at": "2026-06-29T12:34:57Z",
  "workers": [
    {
      "name": "image-render", "kind": "render",
      "alive": true, "state": "idle", "last_beat_age_s": 3,
      "queue_depth": 0, "dead_letter": 0, "processing": 0,
      "jobs_done": 42, "jobs_failed": 1, "last_job_at": "2026-06-29T12:30:00Z"
    }
  ],
  "links": [
    { "from": "nas", "to": "image-render", "type": "redis-queue", "status": "healthy" },
    { "from": "ai_trade", "to": "nas-stock", "type": "http-pull", "status": "healthy" }
  ]
}
  • link.statushealthy | paused | down | degraded. 산정: 워커 dead → down; paused → paused; dead_letter>0 → degraded; redis_ok=false → 전 링크 down.

6. 시각화 상태 (Three.js)

상태 파이프라인(튜브) 노드
정상 idle 시안/그린, 파티클이 NAS→워커→NAS 루프로 흐름(느림) 초록 글로우 + 큐깊이/처리수 HUD
정상 busy 파티클 빠르게 흐름 "처리 중 N"
일시정지 paused 앰버, 파티클 느려짐/정지 "⏸ 작업중(트레이딩)" 라벨
장애 dead / link down 빨강, 흐름 멈춤, 끊긴 지점 스파크/단절 빨강 + ⚠ 경고, "last beat Xs ago"
실패누적 dead-letter>0 해당 튜브 뱃지 dead-letter 카운트 강조
Redis/집계기 다운 중앙 버스 전체 빨강 "집계 서버 연결 끊김" 오버레이
  • ai_trade의 HTTP-pull 파이프라인은 큐 흐름이 아닌 pull 방향(ai_trade→NAS stock) 파티클로 구분 표현. market_closed는 정상 idle과 동일 톤(휴장은 장애 아님).

7. 에러 처리

  • heartbeat TTL 만료 = dead 판정(권위 신호). 큐가 비어 일감이 없어도 heartbeat가 살아있으면 alive로 정확 판정(2주 사일런트 사고 재발 방지).
  • Redis 다운 → /nodesredis_ok=false 반환(500 아님) → 대시보드가 전 구간 degrade 표시.
  • agent-office 다운 → FE 폴링 실패 → "집계 서버 연결 끊김" 오버레이.
  • 집계기는 read-only(Redis에 쓰지 않음) → 워커 동작에 영향 0.

8. 테스트

  • web-ai: heartbeat.py 단위 테스트(fakeredis/mock) — 발신 주기·TTL·state 전이·카운터. ai_trade heartbeat 별도 테스트.
  • web-backend: node_monitor.collect_status 테스트(mock redis: 키 존재/만료/큐 깊이/dead-letter 케이스) + 경보 전이 테스트(alive→dead→alive, dead-letter 증가). TDD 적용.
  • web-ui: InfraMonitor 컴포넌트가 mock 상태로 렌더 + 상태→색상 매핑 단위 테스트(r3f는 렌더 스모크 수준).

9. 단계 (Phasing)

  • Phase 1 (본 스펙 전체): 6 워커(render 4 + task-watcher + ai_trade) heartbeat / /nodes API / 텔레그램 경보 / Three.js /infra 대시보드.
  • Phase 2 (후속): GPU 사용률(VRAM 16GB 경합 가시화), stuck-task 감지, WebSocket 라이브 푸시, 원격 제어(워커 재시작·pause/resume·dead-letter 재처리).

10. 세션 분담 & 협업 (co-gahusb)

  • 소유권: BE(이 세션)=web-backend, AI 세션=web-ai, FE 세션=web-ui. 각자 자기 repo만 커밋.
  • 선행 게이트: §5의 두 계약(heartbeat 키 스키마 + /nodes 응답 스키마)을 먼저 확정·공유 → 3 세션 병렬 진행.
  • 공유 리소스 락: agent-office 의존성/compose 변경은 compose 락, nginx 무변경(불필요). 배포는 nas-deploy 락.
  • BE 작업: agent-office redis 추가 + node_monitor.py + /nodes + 경보 cron + 본 메모리 기록. AI/FE 작업은 co-gahusb 태스크로 배분.

11. 메모리 갱신 계획

  • 신규 cross-cutting 메모리 infra_distributed_workers.md 작성: 큐 계약 / webhook 계약 / ReliableQueue 키 / heartbeat 키 스키마 / task-watcher paused / node_monitor·/nodes·경보. MEMORY.md 인덱스 등재.
  • 관련 서비스 메모리(service_video/image/music/insta)에 heartbeat·관측 추가 사실을 cross-link.