Frappe SaaS 바로 구축 템플릿·Runbook (동시 3,000 기준)
목적: Redis 3인스턴스(cache/queue/socketio) 완전 분리+각각 HA, App 클러스터 Compose+Frappe 설정, Worker 튜닝·운영 Runbook, 모니터링·테넌트 관측성을 바로 구축 가능한 형태로 정리.
참조: saas-expanded-multitenancy-redis-storage-plan.md.
실서비스에서는 각 Redis HA 세트를 서로 다른 VM/노드에 분산 배치하는 것을 권장한다.
0. 빠른 시작 요약
| 구성 요소 | 디렉터리 | 실행 전 준비 | 실행 |
|---|---|---|---|
| Redis HA (cache) | infra/redis-ha/cache/ | sentinel2.conf, sentinel3.conf를 sentinel1.conf와 동일하게 복사 | docker compose up -d |
| Redis HA (queue) | infra/redis-ha/queue/ | 동일 | docker compose up -d |
| Redis HA (socketio) | infra/redis-ha/socketio/ | 동일 | docker compose up -d |
| App 클러스터 | app-cluster/ | .env·common_site_config.json 반영, DB·Redis VIP 연결 가능 여부 확인, sites 볼륨 최초 시 bench init 등 필요 시 수행 | docker compose up -d |
| 모니터링 | monitoring/ (선택) | 권장: 컨트롤 플레인·Edge 관측성은 cloudflare-based-monitoring-plan.md 참조. 아래 §4는 Data Plane(Hetzner) 로컬 메트릭용 옵션 또는 Hybrid 시 보완용. prometheus.yml targets를 실제 호스트/포트로 수정 후 docker compose up -d. |
- Grafana(로컬 옵션 사용 시): 최초 로그인 후 Data source에 Prometheus 추가 (URL:
http://prometheus:9090, 동일 compose 네트워크 기준). - Scheduler: App 노드 여러 대 시 한 대에서만 scheduler 서비스 기동.
- 보안:
.env,common_site_config.json등 비밀번호는 버전관리 제외. 시크릿 관리자·환경변수 주입 권장.
문서 목차: §1 Redis HA · §2 App 클러스터 · §3 Worker Runbook · §4 모니터링 · §5 테넌트 관측성 · §6 운영 원칙 · §7 구성 파일 체크리스트 · §8 다음 단계 제안 · §9 트러블슈팅 · §10 버전·요구사항 · §11 배포 후 검증 · §12 백업·복구 요약 · §13 용어·약어 · §14 관련 기획서 · §15 포트·엔드포인트 정리 · §16 구축 순서 · §17 네트워크·노출 권장.
0.1 전체 구성 개요
flowchart TB
subgraph 외부
U[클라이언트]
end
subgraph App노드
N[Nginx :80]
W[Web :8000]
S[SocketIO :9000]
Wk[Workers]
Sch[Scheduler]
end
subgraph Redis_HA
RC[Redis Cache :6379]
RQ[Redis Queue :6380]
RS[Redis Socket :6381]
end
subgraph DB
M[(MariaDB :3306)]
end
subgraph 모니터링
P[Prometheus :9090]
G[Grafana :3000]
end
U --> N
N --> W
N --> S
W --> RC
W --> RQ
W --> RS
W --> M
S --> RS
Wk --> RQ
Wk --> M
Sch --> RQ
P --> G
1. Redis 3인스턴스 완전 분리 + 각각 HA(Sentinel)
1.1 디렉터리 구조
infra/redis-ha/ cache/ docker-compose.yml redis-master.conf redis-replica.conf sentinel1.conf, sentinel2.conf, sentinel3.conf queue/ docker-compose.yml redis-master.conf redis-replica.conf sentinel1.conf, sentinel2.conf, sentinel3.conf socketio/ docker-compose.yml redis-master.conf redis-replica.conf sentinel1.conf, sentinel2.conf, sentinel3.conf1.2 공통 원칙
- cache:
allkeys-lru, 큰 메모리(예: 8GB) - queue:
noeviction(절대 eviction 금지), 안전 버퍼(예: 4GB) - socketio:
volatile-lru또는allkeys-lru, 1~2GB - Sentinel 3개(Quorum=2). App이 Sentinel을 직접 못 쓰는 경우가 많으므로 각 역할별 VIP/Proxy(HAProxy/Keepalived) 권장.
1.3 CACHE 세트 (예시)
cache/redis-master.conf
bind 0.0.0.0port 6379requirepass StrongCachePassmasterauth StrongCachePassappendonly yesappendfsync everysecmaxmemory 8gbmaxmemory-policy allkeys-lrucache/redis-replica.conf
bind 0.0.0.0port 6379requirepass StrongCachePassmasterauth StrongCachePassreplicaof redis-cache-master 6379appendonly yesappendfsync everysecmaxmemory 8gbmaxmemory-policy allkeys-lrucache/sentinel1.conf (sentinel2.conf, sentinel3.conf는 동일 내용으로 복사. 포트는 compose에서 26380/26381로 매핑)
port 26379sentinel monitor cachemaster redis-cache-master 6379 2sentinel auth-pass cachemaster StrongCachePasssentinel down-after-milliseconds cachemaster 5000sentinel failover-timeout cachemaster 60000sentinel parallel-syncs cachemaster 1cache/docker-compose.yml
version: "3.8"services: redis-cache-master: image: redis:7 container_name: redis-cache-master command: ["redis-server", "/usr/local/etc/redis/redis-master.conf"] volumes: - ./redis-master.conf:/usr/local/etc/redis/redis-master.conf:ro - cache-master-data:/data ports: - "6379:6379" restart: unless-stopped redis-cache-replica: image: redis:7 container_name: redis-cache-replica command: ["redis-server", "/usr/local/etc/redis/redis-replica.conf"] volumes: - ./redis-replica.conf:/usr/local/etc/redis/redis-replica.conf:ro - cache-replica-data:/data restart: unless-stopped depends_on: [redis-cache-master] redis-cache-sentinel-1: image: redis:7 container_name: redis-cache-sentinel-1 command: ["redis-sentinel", "/usr/local/etc/redis/sentinel.conf"] volumes: - ./sentinel1.conf:/usr/local/etc/redis/sentinel.conf:ro ports: ["26379:26379"] restart: unless-stopped depends_on: [redis-cache-master, redis-cache-replica] redis-cache-sentinel-2: image: redis:7 container_name: redis-cache-sentinel-2 command: ["redis-sentinel", "/usr/local/etc/redis/sentinel.conf"] volumes: - ./sentinel2.conf:/usr/local/etc/redis/sentinel.conf:ro ports: ["26380:26379"] restart: unless-stopped depends_on: [redis-cache-master, redis-cache-replica] redis-cache-sentinel-3: image: redis:7 container_name: redis-cache-sentinel-3 command: ["redis-sentinel", "/usr/local/etc/redis/sentinel.conf"] volumes: - ./sentinel3.conf:/usr/local/etc/redis/sentinel.conf:ro ports: ["26381:26379"] restart: unless-stopped depends_on: [redis-cache-master, redis-cache-replica]volumes: cache-master-data: cache-replica-data:1.4 QUEUE 세트 (핵심: noeviction)
queue/redis-master.conf
bind 0.0.0.0port 6379requirepass StrongQueuePassmasterauth StrongQueuePassappendonly yesappendfsync everysecmaxmemory 4gbmaxmemory-policy noevictionqueue/redis-replica.conf
bind 0.0.0.0port 6379requirepass StrongQueuePassmasterauth StrongQueuePassreplicaof redis-queue-master 6379appendonly yesappendfsync everysecmaxmemory 4gbmaxmemory-policy noevictionqueue/sentinel1.conf
port 26379sentinel monitor queuemaster redis-queue-master 6379 2sentinel auth-pass queuemaster StrongQueuePasssentinel down-after-milliseconds queuemaster 5000sentinel failover-timeout queuemaster 60000sentinel parallel-syncs queuemaster 1queue/docker-compose.yml (외부 포트: master 6380, sentinel 26479/26480/26481)
version: "3.8"services: redis-queue-master: image: redis:7 container_name: redis-queue-master command: ["redis-server", "/usr/local/etc/redis/redis-master.conf"] volumes: - ./redis-master.conf:/usr/local/etc/redis/redis-master.conf:ro - queue-master-data:/data ports: ["6380:6379"] restart: unless-stopped redis-queue-replica: image: redis:7 container_name: redis-queue-replica command: ["redis-server", "/usr/local/etc/redis/redis-replica.conf"] volumes: - ./redis-replica.conf:/usr/local/etc/redis/redis-replica.conf:ro - queue-replica-data:/data restart: unless-stopped depends_on: [redis-queue-master] redis-queue-sentinel-1: image: redis:7 container_name: redis-queue-sentinel-1 command: ["redis-sentinel", "/usr/local/etc/redis/sentinel.conf"] volumes: - ./sentinel1.conf:/usr/local/etc/redis/sentinel.conf:ro ports: ["26479:26379"] restart: unless-stopped depends_on: [redis-queue-master, redis-queue-replica] redis-queue-sentinel-2: image: redis:7 container_name: redis-queue-sentinel-2 command: ["redis-sentinel", "/usr/local/etc/redis/sentinel.conf"] volumes: - ./sentinel2.conf:/usr/local/etc/redis/sentinel.conf:ro ports: ["26480:26379"] restart: unless-stopped depends_on: [redis-queue-master, redis-queue-replica] redis-queue-sentinel-3: image: redis:7 container_name: redis-queue-sentinel-3 command: ["redis-sentinel", "/usr/local/etc/redis/sentinel.conf"] volumes: - ./sentinel3.conf:/usr/local/etc/redis/sentinel.conf:ro ports: ["26481:26379"] restart: unless-stopped depends_on: [redis-queue-master, redis-queue-replica]volumes: queue-master-data: queue-replica-data:1.5 SOCKETIO 세트
socketio/redis-master.conf
bind 0.0.0.0port 6379requirepass StrongSocketPassmasterauth StrongSocketPassappendonly yesappendfsync everysecmaxmemory 2gbmaxmemory-policy allkeys-lrusocketio/redis-replica.conf
bind 0.0.0.0port 6379requirepass StrongSocketPassmasterauth StrongSocketPassreplicaof redis-socket-master 6379appendonly yesappendfsync everysecmaxmemory 2gbmaxmemory-policy allkeys-lrusocketio/sentinel1.conf
port 26379sentinel monitor socketmaster redis-socket-master 6379 2sentinel auth-pass socketmaster StrongSocketPasssentinel down-after-milliseconds socketmaster 5000sentinel failover-timeout socketmaster 60000sentinel parallel-syncs socketmaster 1socketio/docker-compose.yml (외부 포트: master 6381, sentinel 26579/26580/26581)
version: "3.8"services: redis-socket-master: image: redis:7 container_name: redis-socket-master command: ["redis-server", "/usr/local/etc/redis/redis-master.conf"] volumes: - ./redis-master.conf:/usr/local/etc/redis/redis-master.conf:ro - socket-master-data:/data ports: ["6381:6379"] restart: unless-stopped redis-socket-replica: image: redis:7 container_name: redis-socket-replica command: ["redis-server", "/usr/local/etc/redis/redis-replica.conf"] volumes: - ./redis-replica.conf:/usr/local/etc/redis/redis-replica.conf:ro - socket-replica-data:/data restart: unless-stopped depends_on: [redis-socket-master] redis-socket-sentinel-1: image: redis:7 container_name: redis-socket-sentinel-1 command: ["redis-sentinel", "/usr/local/etc/redis/sentinel.conf"] volumes: - ./sentinel1.conf:/usr/local/etc/redis/sentinel.conf:ro ports: ["26579:26379"] restart: unless-stopped depends_on: [redis-socket-master, redis-socket-replica] redis-socket-sentinel-2: image: redis:7 container_name: redis-socket-sentinel-2 command: ["redis-sentinel", "/usr/local/etc/redis/sentinel.conf"] volumes: - ./sentinel2.conf:/usr/local/etc/redis/sentinel.conf:ro ports: ["26580:26379"] restart: unless-stopped depends_on: [redis-socket-master, redis-socket-replica] redis-socket-sentinel-3: image: redis:7 container_name: redis-socket-sentinel-3 command: ["redis-sentinel", "/usr/local/etc/redis/sentinel.conf"] volumes: - ./sentinel3.conf:/usr/local/etc/redis/sentinel.conf:ro ports: ["26581:26379"] restart: unless-stopped depends_on: [redis-socket-master, redis-socket-replica]volumes: socket-master-data: socket-replica-data:1.6 App 연결 단순화 (VIP/Proxy)
- cache VIP:
redis-cache.service.local:6379 - queue VIP:
redis-queue.service.local:6379 - socket VIP:
redis-socket.service.local:6379 - VIP는 HAProxy + Keepalived로 구현 권장.
HAProxy 예시 (cache 1대, Sentinel 미사용·마스터만 바라볼 때)
App이 Sentinel을 쓰지 않고 단일 엔드포인트를 쓸 때, VIP 대신 HAProxy로 마스터만 노출하는 최소 설정:
# haproxy.cfg 일부frontend redis_cache bind *:6379 default_backend redis_cache_masterbackend redis_cache_master balance roundrobin server master redis-cache-master:6379 check실제 HA 구성 시에는 Sentinel이 선출한 현재 마스터를 HAProxy가 바라보도록 스크립트 또는 keepalived + script로 연동하는 방식을 사용한다.
2. App 클러스터 docker-compose + Frappe 설정
2.0 폴더 구조 (권장)
app-cluster/ docker-compose.yml .env nginx/ default.conf sites/ # 벤치 볼륨 내부에 반영되는 경우도 동일 구조 common_site_config.jsonmonitoring/ docker-compose.yml prometheus.yml alert-rules.yml alertmanager.yml2.1 common_site_config.json (VIP 기준)
{ "db_host": "10.0.2.10", "db_port": 3306, "redis_cache": "redis://:StrongCachePass@redis-cache.service.local:6379/0", "redis_queue": "redis://:StrongQueuePass@redis-queue.service.local:6379/0", "redis_socketio": "redis://:StrongSocketPass@redis-socket.service.local:6379/0", "socketio_port": 9000}2.2 App compose (1노드 템플릿, 수평 확장 가능)
- nginx: 80, upstream web:8000, socketio:9000.
- web: frappe/erpnext,
bench start --no-socketio, env DB_HOST, REDIS_*. - socketio:
node socketio.js, REDIS_SOCKETIO. - scheduler:
bench schedule— 클러스터에서 1대만 active. - worker_short:
bench worker --queue short. - worker_default:
bench worker --queue default. - worker_long:
bench worker --queue long. - volumes: sites-vol, logs-vol.
app-cluster/docker-compose.yml
version: "3.8"services: nginx: image: nginx:alpine container_name: prego-nginx depends_on: [web, socketio] ports: ["80:80"] volumes: - ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro - sites-vol:/workspace/sites restart: unless-stopped web: image: frappe/erpnext:latest container_name: prego-web command: ["bash", "-lc", "bench start --no-socketio"] env_file: [.env] environment: DB_HOST: ${DB_HOST} DB_PORT: ${DB_PORT} REDIS_CACHE: ${REDIS_CACHE_URL} REDIS_QUEUE: ${REDIS_QUEUE_URL} REDIS_SOCKETIO: ${REDIS_SOCKETIO_URL} volumes: - sites-vol:/workspace/sites - logs-vol:/workspace/logs restart: unless-stopped socketio: image: frappe/erpnext:latest container_name: prego-socketio command: ["bash", "-lc", "node /workspace/apps/frappe/socketio.js"] env_file: [.env] environment: REDIS_SOCKETIO: ${REDIS_SOCKETIO_URL} SOCKETIO_PORT: ${SOCKETIO_PORT} volumes: [sites-vol:/workspace/sites] restart: unless-stopped scheduler: image: frappe/erpnext:latest container_name: prego-scheduler command: ["bash", "-lc", "bench schedule"] env_file: [.env] volumes: - sites-vol:/workspace/sites - logs-vol:/workspace/logs restart: unless-stopped worker_short: image: frappe/erpnext:latest container_name: prego-worker-short command: ["bash", "-lc", "bench worker --queue short"] env_file: [.env] volumes: - sites-vol:/workspace/sites - logs-vol:/workspace/logs restart: unless-stopped worker_default: image: frappe/erpnext:latest container_name: prego-worker-default command: ["bash", "-lc", "bench worker --queue default"] env_file: [.env] volumes: - sites-vol:/workspace/sites - logs-vol:/workspace/logs restart: unless-stopped worker_long: image: frappe/erpnext:latest container_name: prego-worker-long command: ["bash", "-lc", "bench worker --queue long"] env_file: [.env] volumes: - sites-vol:/workspace/sites - logs-vol:/workspace/logs restart: unless-stoppedvolumes: sites-vol: logs-vol:2.3 nginx/default.conf (Frappe 멀티사이트 + Socket.IO)
/socket.io→ upstream frappe_socketio (Upgrade, Connection).- 나머지 → upstream frappe_web.
X-Frappe-Site-Name$host. - client_max_body_size 50m, proxy_read_timeout 120s.
app-cluster/nginx/default.conf
upstream frappe_web { server web:8000; keepalive 32;}upstream frappe_socketio { server socketio:9000; keepalive 8;}map $http_upgrade $connection_upgrade { default upgrade; '' close;}server { listen 80; server_name _; client_max_body_size 50m; gzip on; gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; location /assets { proxy_pass http://frappe_web; proxy_set_header Host $host; } location /socket.io { proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_read_timeout 120s; proxy_pass http://frappe_socketio; } location / { proxy_set_header Host $host; proxy_set_header X-Frappe-Site-Name $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_read_timeout 120s; proxy_connect_timeout 10s; proxy_send_timeout 120s; proxy_pass http://frappe_web; }}2.4 .env (App 노드 공통)
app-cluster/.env
DB_HOST=10.0.2.10DB_PORT=3306DB_USER=frappeDB_PASSWORD=StrongDBPassREDIS_CACHE_URL=redis://:StrongCachePass@redis-cache.service.local:6379/0REDIS_QUEUE_URL=redis://:StrongQueuePass@redis-queue.service.local:6379/0REDIS_SOCKETIO_URL=redis://:StrongSocketPass@redis-socket.service.local:6379/0SOCKETIO_PORT=90002.5 클러스터 운영 규칙
- scheduler는 단 1대만 실행 (중복 방지).
- sites-vol은 공유 스토리지(NFS/EFS) 또는 배포 동기화 전략 필요.
2.6 최초 1회 설정 (sites 볼륨)
sites 볼륨이 비어 있으면 web/scheduler/worker가 기동해도 사이트가 없어 오류가 난다. 최초 1회: 동일 이미지로 일회성 컨테이너를 띄워 bench init·bench new-site <site_name> 등을 실행하거나, 이미 사이트가 들어 있는 볼륨/이미지를 사용한다. 멀티 노드 확장 시에는 해당 sites 볼륨을 NFS 등으로 공유하거나, 배포 파이프라인에서 노드별로 동기화한다.
2.7 수평 확장 (Worker·App 노드)
- Worker 증설: 동일 노드에서
docker compose up -d --scale worker_default=4처럼 scale 하거나, compose 파일을 복제해worker_default인스턴스를 늘린다. 권장은 노드당 short 2·default 4·long 2. - App 노드 추가: LB 뒤에 동일
app-cluster구성을 새 노드에 배포. scheduler는 한 노드에서만 기동하고, 나머지 노드에서는 scheduler 서비스를 제거하거나 비활성화. - 모니터링 연동: (로컬 Prometheus 사용 시) Prometheus가 다른 호스트의 exporter를 스크래핑하려면
prometheus.yml의 targets를 해당 호스트 IP/호스트명으로 지정하고, 네트워크(방화벽·Docker 네트워크)에서 접근 가능하도록 설정. 전역 관측성은 Cloudflare 기반 모니터링 활용 권장.
3. Worker 병목 제거 Runbook (구체 실행)
3.1 권장 Queue 설계 (필수)
- short: 알림/이메일/가벼운 후처리
- default: 일반 백그라운드
- long: 리포트/대량 Import/급여/출결 마감/배치
3.2 Worker 배치 권장 (App 노드 3대)
- 각 노드: short x2, default x4, long x2. 총 24. long은 가능하면 전용 워커 노드로 격리.
3.3 절대 금지
- long 작업을 default에 섞기
- queue Redis에 eviction 허용(작업 유실)
- worker concurrency 무작정 상향(DB/Redis 선행 부하)
3.4 모니터링 트리거 예시
- long 대기열 10분 이상 증가 → long worker +50%
- default 대기열 업무시간 증가 → default worker +25%
- DB CPU 75% + queue 동시 증가 → long rate 제한 + 야간 배치 전환
3.5 테넌트 독점 방지
- 대량 Import/급여: 예약 실행(오프피크), 동시 실행 테넌트 수 제한(예: 2개).
- Enterprise: 전용 long worker 풀 또는 전용 Redis/DB(상위 플랜).
3.6 장애 대응 (큐 적체)
- long 폭주: long만 worker 증설, default/short 보호.
- Redis queue 메모리 상승: 작업 생성 rate 먼저 제한, DB 상태 보고 worker 증설.
- DB 슬로우쿼리: long이 만드는 리포트/집계 SQL 튜닝 또는 Replica 읽기 전환.
4. 모니터링 (Data Plane 로컬 옵션: Prometheus + Grafana + Alert)
Prego 전체 관측성: API·Edge·테넌트 사용량·Status Page는 Cloudflare 기반(Analytics Engine·D1·Workers 대시보드·Logpush/R2)을 권장. cloudflare-based-monitoring-plan.md, api-control-plane-implementation-plan.md §16–§17 참조.
아래 §4.1~은 Hetzner Data Plane 내 Redis·MySQL·Nginx·Node 등 로컬 메트릭이 필요할 때의 옵션이며, Hybrid Observability 시 인프라 계층 보완용으로 사용.
4.1 구조 (로컬 옵션)
- App/DB/Redis 노드 → Node Exporter, Redis Exporter(3), MySQL Exporter, Nginx Exporter → Prometheus → Alertmanager → Grafana.
4.2 prometheus.yml scrape
- job: node, redis-cache, redis-queue, redis-socket, mysql, nginx.
monitoring/prometheus.yml
global: scrape_interval: 15srule_files: - "alert-rules.yml"scrape_configs: - job_name: "node" static_configs: - targets: ["app1:9100", "app2:9100"] - job_name: "redis-cache" static_configs: - targets: ["redis-cache-exporter:9121"] - job_name: "redis-queue" static_configs: - targets: ["redis-queue-exporter:9121"] - job_name: "redis-socket" static_configs: - targets: ["redis-socket-exporter:9121"] - job_name: "mysql" static_configs: - targets: ["mysql-exporter:9104"] - job_name: "nginx" static_configs: - targets: ["nginx-exporter:9113"]monitoring/docker-compose.yml
version: "3.8"services: prometheus: image: prom/prometheus container_name: prometheus volumes: - ./prometheus.yml:/etc/prometheus/prometheus.yml - ./alert-rules.yml:/etc/prometheus/alert-rules.yml ports: ["9090:9090"] alertmanager: image: prom/alertmanager container_name: alertmanager volumes: - ./alertmanager.yml:/etc/alertmanager/alertmanager.yml ports: ["9093:9093"] # alertmanager.yml 예: route receiver 'default', inhibit_rules 등. 필요 시 별도 작성. grafana: image: grafana/grafana container_name: grafana ports: ["3000:3000"] environment: - GF_SECURITY_ADMIN_PASSWORD=StrongGrafanaPass4.3 핵심 Alert Rules
- RedisQueueEviction: redis_evicted_keys_total{job=“redis-queue”} > 0 → CRITICAL (작업 유실 위험).
- RedisMemoryHigh: used/max > 80% 5분 → warning.
- LongQueueBacklog: redis_db_keys(redis-queue) > 500 10분 → warning.
- MySQLHighThreadsRunning, MySQLReplicationLag.
- HighHTTP5xx, HighLatencyP95.
monitoring/alert-rules.yml
groups: - name: redis-alerts rules: - alert: RedisQueueEviction expr: redis_evicted_keys_total{job="redis-queue"} > 0 for: 1m labels: severity: critical annotations: description: "Queue Redis eviction detected — possible job loss" - alert: RedisMemoryHigh expr: redis_memory_used_bytes / redis_memory_max_bytes > 0.8 for: 5m labels: severity: warning annotations: description: "Redis memory usage above 80%" - name: queue-alerts rules: - alert: LongQueueBacklog expr: redis_db_keys{job="redis-queue"} > 500 for: 10m labels: severity: warning annotations: description: "Long queue backlog increasing" - name: mysql-alerts rules: - alert: MySQLHighThreadsRunning expr: mysql_global_status_threads_running > 100 for: 5m labels: severity: warning annotations: description: "High MySQL concurrent threads" - alert: MySQLReplicationLag expr: mysql_slave_status_seconds_behind_master > 10 for: 2m labels: severity: critical annotations: description: "Replication lag detected" - name: app-alerts rules: - alert: HighHTTP5xx expr: rate(nginx_http_requests_total{status=~"5.."}[5m]) > 0.01 for: 5m labels: severity: warning annotations: description: "High 5xx error rate" - alert: HighLatencyP95 expr: histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m])) > 0.8 for: 10m labels: severity: warning annotations: description: "p95 latency above 800ms"참고:
redis_db_keys는 queue Redis의 전체 키 수 기준. short/default/long 큐별 length는 RQ exporter 또는 §5의 tenant/queue custom metric으로 수집하는 것을 권장.
monitoring/alertmanager.yml (최소 예시 — 알림 채널은 환경에 맞게 수정)
global: resolve_timeout: 5mroute: receiver: default group_by: [alertname] group_wait: 10s group_interval: 10s repeat_interval: 12hreceivers: - name: default # webhook_configs / slack_configs / email_configs 등 추가4.4 Grafana 대시보드
- System Overview (CPU/Memory/Network/Disk).
- Redis Layer (Used Memory %, Ops/sec, Evicted Keys, Replication).
- Queue Health (short/default/long length, jobs/sec, 대기시간).
- DB Layer (Threads_running, Slow queries, Replication lag).
- SLA View (p95 latency, 5xx rate, Active users).
설정: Grafana에서 Data source → Add data source → Prometheus, URL http://prometheus:9090 (같은 docker-compose 네트워크일 때). 저장 후 대시보드에서 Prometheus 쿼리 사용.
4.5 동시 3,000 알림 전략 요약
| 유형 | 경고 기준 | 대응 |
|---|---|---|
| Queue 적체 | long 10분 증가 | long worker 증설 |
| Redis 메모리 | 80% 초과 | cache TTL 점검 |
| Queue eviction | 1개라도 발생 | CRITICAL |
| Replication lag | >10초 | Failover 점검 |
| HTTP 5xx | >1% | 즉시 조사 |
4.5.1 알림별 조치 요약 (Runbook)
| 알림 규칙 | 조건 | 조치 |
|---|---|---|
| RULE-A1 Long queue backlog | long 대기열 10분 연속 증가, p95 > 5분 | long worker 증설, 배치/테넌트 동시 실행 제한 |
| RULE-A2 Default queue backlog | default queue length > 1,000 지속 5분 | default worker +25%, 무거운 작업 long으로 이동 |
| RULE-A3 Short queue latency | short p95 > 30초 | short worker 증설, 알림/이메일 공급 제한 |
| RULE-B1 Queue Redis eviction | evicted_keys > 0 (queue redis) | CRITICAL. 작업 생성 차단, 메모리·정책 점검 |
| RULE-B2 Redis memory pressure | used/max > 80% 10분 | cache TTL 점검, maxmemory 증설, 키 폭증 원인 분석 |
| RULE-C1 HTTP 5xx | 5xx rate > 1% 5분 | 최근 배포/마이그레이션·DB/Redis 상태 확인, 롤백 검토 |
| RULE-C2 Latency p95 | p95 > 800ms 10분 | DB slow query/락, Redis ops, worker의 DB 부하 확인 |
4.6 운영 성숙도 단계
| 단계 | 내용 |
|---|---|
| Level 1 | CPU/Memory 기반 인프라 모니터링 |
| Level 2 | Queue 기반 병목 조기 감지 (length, 대기시간) |
| Level 3 | SLA 기반 알림 (p95, 5xx, replication lag) |
| Level 4 | 테넌트별 리소스 분석·Top-N·Enterprise 격리 |
5. 테넌트별 리소스 분리 모니터링 (Tenant-level Observability)
5.1 목표
- 어떤 테넌트가 DB/Queue/캐시를 독점하는지 조기 감지.
- Enterprise SLA 분리, 테넌트 샤딩 자동화 근거 확보.
5.2 전략
- 모든 요청에 tenant label: Frappe
frappe.local.site로 tenant 확보, Prometheus metric에 label 부여. - Custom metric 예시:
tenant_http_requests_total,tenant_http_latency_seconds,tenant_jobs_total(tenant, queue),tenant_job_wait_seconds.
5.2.1 Prometheus 메트릭 정의 (Frappe 앱에 추가)
# 예: frappe/custom_prometheus_metrics.py 또는 앱 내 metrics 모듈from prometheus_client import Counter, Histogram
tenant_http_requests = Counter( "tenant_http_requests_total", "HTTP requests by tenant", ["tenant", "method", "status"],)tenant_http_latency = Histogram( "tenant_http_latency_seconds", "HTTP latency by tenant", ["tenant"], buckets=(0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0),)tenant_jobs = Counter( "tenant_jobs_total", "Jobs enqueued by tenant", ["tenant", "queue"],)tenant_job_wait = Histogram( "tenant_job_wait_seconds", "Queue wait time by tenant", ["tenant", "queue"], buckets=(1, 5, 15, 30, 60, 120, 300),)5.2.2 Middleware에서 tenant 라벨 부여
# 요청 처리 후: tenant = frappe.local.site (또는 request 헤더에서 site)# response 반환 직전에:import frappetenant = getattr(frappe.local, "site", "") or ""tenant_http_requests.labels(tenant=tenant, method=request.method, status=response.status_code).inc()tenant_http_latency.labels(tenant=tenant).observe(latency_seconds)5.2.3 RQ Job enqueue 시 tenant 기록
# job enqueue 시점에 (frappe.enqueue 또는 RQ 훅에서):tenant = getattr(frappe.local, "site", "") or ""tenant_jobs.labels(tenant=tenant, queue=queue_name).inc()# 실행 시작 시 대기 시간 기록: (start_time - enqueue_time) → tenant_job_wait.labels(...).observe(wait_sec)5.3 Grafana
- Tenant Top 10: HTTP request, long queue, DB heavy, error rate.
- 특정 테넌트 Drilldown: $tenant 변수, latency p95, queue wait, jobs/sec.
- Enterprise SLA View: tenant=“enterpriseA” 필터.
PromQL 예시 (Tenant Top 10)
topk(10, sum(rate(tenant_http_requests_total[5m])) by (tenant))topk(10, sum(rate(tenant_jobs_total{queue="long"}[5m])) by (tenant))Enterprise SLA 알림 예시 (99.9% 목표)
- alert: EnterpriseHighLatency expr: histogram_quantile(0.95, rate(tenant_http_latency_seconds_bucket{tenant="enterpriseA"}[5m])) > 0.5 for: 5m labels: severity: critical annotations: description: "Enterprise tenant A p95 latency above 500ms"5.4 자동 분리 기준
- 특정 tenant QPS > 전체 30% → 전용 DB 분리 고려.
- 특정 tenant long job 50% 이상 → 전용 long worker 또는 전용 DB.
- Queue eviction(queue redis) → 즉시 작업 생성 제한 + 메모리/정책 점검.
5.5 플랜별 격리 수준
| 플랜 | 격리 |
|---|---|
| Basic | 공유 DB/Redis |
| Pro | long worker 격리 |
| Enterprise | 전용 DB + 전용 worker + 전용 Redis |
5.6 복잡도 vs 가치
| 항목 | 복잡도 | SaaS 가치 |
|---|---|---|
| Tenant HTTP metric | 낮음 | 높음 |
| Queue tenant tagging | 중간 | 매우 높음 |
| DB per-tenant 분석 | 낮음 | 높음 |
| Redis per-tenant 추정 | 높음 | 중간 |
5.7 권장 적용 단계
- 1단계(필수): HTTP + Queue tenant metric 구현, Tenant Top-N 대시보드.
- 2단계: Queue 대기시간 histogram, 테넌트별 SLA 알림 분리.
- 3단계: 자동 DB 분리 기준 수립, Enterprise 전용 리소스 상품화.
6. 운영 원칙 요약
- Queue Redis는 eviction 0 유지 (noeviction).
- Scheduler는 클러스터에서 1개만.
- long 작업은 항상 격리 (큐 + 워커 + 가능하면 DB Replica).
- Alert는 행동 가능한 조건만 설정.
- 비밀번호·시크릿:
.env,common_site_config.json등은 버전관리 제외, 시크릿 관리자 또는 CI/CD 환경변수로 주입. - 테넌트별 병목 식별 → 격리 필요 고객 조기 발견 → SLA 상품화·자동 샤딩 가능.
7. 구성 파일 체크리스트
구축 시 만들어 둘 파일을 체크용으로 나열한다. 비밀번호·호스트명은 환경에 맞게 치환.
| 디렉터리 | 파일 |
|---|---|
infra/redis-ha/cache/ | docker-compose.yml, redis-master.conf, redis-replica.conf, sentinel1.conf, sentinel2.conf, sentinel3.conf |
infra/redis-ha/queue/ | 동일 6개 |
infra/redis-ha/socketio/ | 동일 6개 |
app-cluster/ | docker-compose.yml, .env |
app-cluster/nginx/ | default.conf |
app-cluster/sites/ | common_site_config.json (또는 볼륨 내부에 반영) |
monitoring/ | docker-compose.yml, prometheus.yml, alert-rules.yml, alertmanager.yml |
8. 다음 단계 제안
- HAProxy + Keepalived: Sentinel이 선출한 현재 마스터를 VIP로 노출하는 구성을 상세 설계·배포.
- RQ Exporter: queue별 length·대기시간 메트릭 수집 후 Grafana·Alert에 연동.
- Frappe/ERPNext 이미지: 사이트가 포함된 커스텀 이미지 또는 배포 스크립트로 sites 볼륨 초기화 자동화.
- 테넌트 메트릭: §5의 Prometheus 메트릭·Middleware·RQ 훅을 실제 Frappe 앱에 적용 후 Top-N·SLA 알림 검증.
9. 트러블슈팅
| 현상 | 확인·조치 |
|---|---|
| App에서 Redis/DB connection refused | .env·common_site_config.json의 호스트/포트 확인. App 컨테이너에서 redis-cache.service.local·DB 호스트 해석 및 접속 가능 여부 확인 (동일 네트워크 또는 VIP 라우팅). |
| Scheduler/작업이 중복 실행됨 | Scheduler는 한 노드에서만 기동. 다른 노드의 scheduler 서비스를 중지하거나 compose에서 제거. |
| Queue 적체·worker가 안 먹힘 | queue Redis가 noeviction인지 확인. worker 수·큐 배치(short/default/long 분리) 확인. DB 슬로우쿼리·락 여부 확인. |
| Prometheus가 target을 스크래핑 못 함 | prometheus.yml의 targets 호스트/포트가 Prometheus 컨테이너에서 접근 가능한지 확인. Docker라면 동일 네트워크 또는 호스트 IP:포트로 지정. 방화벽 개방 확인. |
| Grafana에 데이터 없음 | Data source에 Prometheus URL(http://prometheus:9090) 추가·저장·테스트. Prometheus UI에서 해당 job이 스크래핑 성공하는지 확인. |
10. 버전·요구사항
- Redis: 7 (이 문서의 compose·conf 기준). Sentinel 호환 버전 유지.
- Frappe/ERPNext:
frappe/erpnext:latest또는 사용 중인 버전 태그. bench·node 경로는 이미지에 맞게 조정. - 모니터링: Prometheus·Alertmanager·Grafana 최신 안정 버전. Exporter(redis_exporter, node_exporter, mysql_exporter, nginx exporter)는 Prometheus 공식 또는 호환 버전.
- Docker: Compose V2(
docker compose) 권장. 네트워크·볼륨 공유 시 호스트/스토리지 요구사항 충족.
11. 배포 후 검증 체크리스트
| 항목 | 방법 |
|---|---|
| Redis cache/queue/socketio 응답 | 각 Redis 포트에서 redis-cli -p 6379 PING (또는 6380/6381), requirepass면 AUTH <pass> 후 PING. |
| Sentinel 상태 | redis-cli -p 26379 SENTINEL master cachemaster (cache). queue/socketio는 해당 sentinel 포트·master 이름으로 확인. |
| App → DB 연결 | App 컨테이너에서 bench doctor 또는 DB 접속 테스트. |
| App → Redis 연결 | bench 콘솔 또는 앱 로그에서 cache/queue/socketio 연결 오류 없음 확인. |
| Queue worker 동작 | 큐에 테스트 job enqueue 후 short/default/long worker 로그에서 처리 확인. |
| Nginx → web/socketio | curl -I http://localhost/, /socket.io 경로 접근. |
| Prometheus targets | Prometheus UI → Status → Targets에서 job별 UP 여부 확인. |
| Grafana·알림 | 대시보드에서 메트릭 표시, Alertmanager에 테스트 알림 전송(선택). |
12. 백업·복구 요약
- Redis: AOF/RDB는 각 컨테이너의
/data볼륨에 저장됨. 볼륨 백업 또는redis-cli BGSAVE후 RDB 파일 복사. HA 구성에서는 replica에서도 백업 가능. - MariaDB: Primary/Replica 백업 정책은 saas-db-separation-and-scaling-plan.md 등 DB 계획 문서 참조. 정기 dump·PITR 정책 수립 권장.
- sites 볼륨: Frappe 사이트·첨부 파일이 포함됨. 공유 스토리지(NFS 등) 스냅샷 또는 노드별 주기적 동기화·백업 권장.
- 설정 파일:
common_site_config.json,.env, compose·conf 파일은 버전관리(비밀번호 제외) 또는 설정 저장소에 보관해 복구 시 재배포에 사용.
13. 용어·약어
| 용어 | 설명 |
|---|---|
| VIP | Virtual IP. HAProxy·Keepalived로 제공하는 단일 엔드포인트. 장애 시 다른 노드로 전환. |
| Quorum | Sentinel이 마스터 다운을 판단하는 데 필요한 Sentinel 동의 수. 본 문서는 3개 Sentinel, Quorum=2. |
| noeviction | Redis 메모리 한도 시 키를 삭제하지 않음. queue Redis는 작업 유실 방지를 위해 필수. |
| allkeys-lru | 메모리 한도 시 최근 덜 쓰인 키부터 제거. cache·socketio용. |
| RQ | Redis Queue. Frappe/bench의 백그라운드 작업 큐. short/default/long 등 큐 이름 사용. |
| Sentinel | Redis 고가용성: 마스터 감시·자동 failover·클라이언트에 현재 마스터 알림. |
| AOF/RDB | Redis 지속성: AOF(로그 재생), RDB(스냅샷). |
| p95 | 95 percentile. 응답 시간 등에서 상위 5%를 제외한 값. SLA 지표로 자주 사용. |
| PITR | Point-In-Time Recovery. 특정 시점으로 DB 복구. |
14. 관련 기획서
- saas-expanded-multitenancy-redis-storage-plan.md — Redis·R2·Full HA·동시 3,000 확장 전제.
- saas-db-separation-and-scaling-plan.md — DB 서버 분리·MariaDB Primary/Replica·샤딩.
- ansible-implementation-plan.md — Frappe/bench·DB 연동 배포 자동화.
- IMPLEMENTATION_INDEX.md — 기획·구현 문서 목차.
15. 포트·엔드포인트 정리
방화벽·로드밸런서·VIP 설계 시 참고. 단일 호스트에 모두 띄울 때 포트 충돌 없도록 구성됨.
| 구분 | 서비스 | 호스트 포트 | 비고 |
|---|---|---|---|
| Redis cache | master | 6379 | |
| Redis cache | sentinel 1/2/3 | 26379, 26380, 26381 | |
| Redis queue | master | 6380 | |
| Redis queue | sentinel 1/2/3 | 26479, 26480, 26481 | |
| Redis socketio | master | 6381 | |
| Redis socketio | sentinel 1/2/3 | 26579, 26580, 26581 | |
| App | Nginx | 80 | LB에서 이 포트로 분산 |
| App | Web (Gunicorn) | 8000 | 내부, Nginx가 프록시 |
| App | SocketIO | 9000 | 내부, Nginx가 /socket.io 프록시 |
| DB | MariaDB | 3306 | App/Worker에서 접속 |
| 모니터링 | Prometheus | 9090 | |
| 모니터링 | Alertmanager | 9093 | |
| 모니터링 | Grafana | 3000 | |
| Exporter(참고) | Node | 9100 | Prometheus가 스크래핑 |
| Exporter(참고) | Redis | 9121 | 인스턴스별 |
| Exporter(참고) | MySQL | 9104 | |
| Exporter(참고) | Nginx | 9113 |
16. 구축 순서(권장)
처음부터 구축할 때 권장하는 순서다. DB·Redis가 이미 있으면 해당 단계는 생략한다.
- MariaDB 준비 — Primary(및 필요 시 Replica). App/Worker가 접속 가능한 호스트/포트/계정 확인.
- Redis HA — cache → queue → socketio 순으로 각 디렉터리에서
docker compose up -d. sentinel2/3 conf 복사 확인. - VIP/Proxy(선택) — Redis를 단일 엔드포인트로 쓸 경우 HAProxy 등 구성.
.env·common_site_config.json의 Redis 호스트를 VIP로 설정. - App 클러스터 —
.env·common_site_config.json반영, sites 볼륨 최초 시 bench init/new-site 수행 후docker compose up -d. Scheduler는 한 노드만 기동. - 모니터링 —
prometheus.ymltargets를 실제 호스트로 수정 후docker compose up -d. Grafana에서 Prometheus 데이터 소스 추가. - 검증 — §11 배포 후 검증 체크리스트 실행.
17. 네트워크·노출 권장
- 외부 노출: LB·Nginx(80/443)만 공개. Web(8000)·SocketIO(9000)·Redis·MariaDB·Prometheus·Grafana는 내부망 또는 관리용으로만 제한 노출.
- App ↔ Redis/DB: 동일 VPC·네트워크 또는 VPN/전용선으로 통신. Redis/DB 포트는 App·Worker·모니터링용 IP만 허용 권장.
- 모니터링: Prometheus/Grafana는 관리자 접근만 허용. Exporters는 Prometheus 서버에서만 접근 가능하도록 방화벽 설정 권장.