Skip to content

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.conf

1.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.0
port 6379
requirepass StrongCachePass
masterauth StrongCachePass
appendonly yes
appendfsync everysec
maxmemory 8gb
maxmemory-policy allkeys-lru

cache/redis-replica.conf

bind 0.0.0.0
port 6379
requirepass StrongCachePass
masterauth StrongCachePass
replicaof redis-cache-master 6379
appendonly yes
appendfsync everysec
maxmemory 8gb
maxmemory-policy allkeys-lru

cache/sentinel1.conf (sentinel2.conf, sentinel3.conf는 동일 내용으로 복사. 포트는 compose에서 26380/26381로 매핑)

port 26379
sentinel monitor cachemaster redis-cache-master 6379 2
sentinel auth-pass cachemaster StrongCachePass
sentinel down-after-milliseconds cachemaster 5000
sentinel failover-timeout cachemaster 60000
sentinel parallel-syncs cachemaster 1

cache/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.0
port 6379
requirepass StrongQueuePass
masterauth StrongQueuePass
appendonly yes
appendfsync everysec
maxmemory 4gb
maxmemory-policy noeviction

queue/redis-replica.conf

bind 0.0.0.0
port 6379
requirepass StrongQueuePass
masterauth StrongQueuePass
replicaof redis-queue-master 6379
appendonly yes
appendfsync everysec
maxmemory 4gb
maxmemory-policy noeviction

queue/sentinel1.conf

port 26379
sentinel monitor queuemaster redis-queue-master 6379 2
sentinel auth-pass queuemaster StrongQueuePass
sentinel down-after-milliseconds queuemaster 5000
sentinel failover-timeout queuemaster 60000
sentinel parallel-syncs queuemaster 1

queue/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.0
port 6379
requirepass StrongSocketPass
masterauth StrongSocketPass
appendonly yes
appendfsync everysec
maxmemory 2gb
maxmemory-policy allkeys-lru

socketio/redis-replica.conf

bind 0.0.0.0
port 6379
requirepass StrongSocketPass
masterauth StrongSocketPass
replicaof redis-socket-master 6379
appendonly yes
appendfsync everysec
maxmemory 2gb
maxmemory-policy allkeys-lru

socketio/sentinel1.conf

port 26379
sentinel monitor socketmaster redis-socket-master 6379 2
sentinel auth-pass socketmaster StrongSocketPass
sentinel down-after-milliseconds socketmaster 5000
sentinel failover-timeout socketmaster 60000
sentinel parallel-syncs socketmaster 1

socketio/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_master
backend 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.json
monitoring/
docker-compose.yml
prometheus.yml
alert-rules.yml
alertmanager.yml

2.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-stopped
volumes:
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

Terminal window
DB_HOST=10.0.2.10
DB_PORT=3306
DB_USER=frappe
DB_PASSWORD=StrongDBPass
REDIS_CACHE_URL=redis://:StrongCachePass@redis-cache.service.local:6379/0
REDIS_QUEUE_URL=redis://:StrongQueuePass@redis-queue.service.local:6379/0
REDIS_SOCKETIO_URL=redis://:StrongSocketPass@redis-socket.service.local:6379/0
SOCKETIO_PORT=9000

2.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 장애 대응 (큐 적체)

  1. long 폭주: long만 worker 증설, default/short 보호.
  2. Redis queue 메모리 상승: 작업 생성 rate 먼저 제한, DB 상태 보고 worker 증설.
  3. 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: 15s
rule_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=StrongGrafanaPass

4.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: 5m
route:
receiver: default
group_by: [alertname]
group_wait: 10s
group_interval: 10s
repeat_interval: 12h
receivers:
- 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 eviction1개라도 발생CRITICAL
Replication lag>10초Failover 점검
HTTP 5xx>1%즉시 조사

4.5.1 알림별 조치 요약 (Runbook)

알림 규칙조건조치
RULE-A1 Long queue backloglong 대기열 10분 연속 증가, p95 > 5분long worker 증설, 배치/테넌트 동시 실행 제한
RULE-A2 Default queue backlogdefault queue length > 1,000 지속 5분default worker +25%, 무거운 작업 long으로 이동
RULE-A3 Short queue latencyshort p95 > 30초short worker 증설, 알림/이메일 공급 제한
RULE-B1 Queue Redis evictionevicted_keys > 0 (queue redis)CRITICAL. 작업 생성 차단, 메모리·정책 점검
RULE-B2 Redis memory pressureused/max > 80% 10분cache TTL 점검, maxmemory 증설, 키 폭증 원인 분석
RULE-C1 HTTP 5xx5xx rate > 1% 5분최근 배포/마이그레이션·DB/Redis 상태 확인, 롤백 검토
RULE-C2 Latency p95p95 > 800ms 10분DB slow query/락, Redis ops, worker의 DB 부하 확인

4.6 운영 성숙도 단계

단계내용
Level 1CPU/Memory 기반 인프라 모니터링
Level 2Queue 기반 병목 조기 감지 (length, 대기시간)
Level 3SLA 기반 알림 (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 frappe
tenant = 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
Prolong 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/socketiocurl -I http://localhost/, /socket.io 경로 접근.
Prometheus targetsPrometheus 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. 용어·약어

용어설명
VIPVirtual IP. HAProxy·Keepalived로 제공하는 단일 엔드포인트. 장애 시 다른 노드로 전환.
QuorumSentinel이 마스터 다운을 판단하는 데 필요한 Sentinel 동의 수. 본 문서는 3개 Sentinel, Quorum=2.
noevictionRedis 메모리 한도 시 키를 삭제하지 않음. queue Redis는 작업 유실 방지를 위해 필수.
allkeys-lru메모리 한도 시 최근 덜 쓰인 키부터 제거. cache·socketio용.
RQRedis Queue. Frappe/bench의 백그라운드 작업 큐. short/default/long 등 큐 이름 사용.
SentinelRedis 고가용성: 마스터 감시·자동 failover·클라이언트에 현재 마스터 알림.
AOF/RDBRedis 지속성: AOF(로그 재생), RDB(스냅샷).
p9595 percentile. 응답 시간 등에서 상위 5%를 제외한 값. SLA 지표로 자주 사용.
PITRPoint-In-Time Recovery. 특정 시점으로 DB 복구.

14. 관련 기획서


15. 포트·엔드포인트 정리

방화벽·로드밸런서·VIP 설계 시 참고. 단일 호스트에 모두 띄울 때 포트 충돌 없도록 구성됨.

구분서비스호스트 포트비고
Redis cachemaster6379
Redis cachesentinel 1/2/326379, 26380, 26381
Redis queuemaster6380
Redis queuesentinel 1/2/326479, 26480, 26481
Redis socketiomaster6381
Redis socketiosentinel 1/2/326579, 26580, 26581
AppNginx80LB에서 이 포트로 분산
AppWeb (Gunicorn)8000내부, Nginx가 프록시
AppSocketIO9000내부, Nginx가 /socket.io 프록시
DBMariaDB3306App/Worker에서 접속
모니터링Prometheus9090
모니터링Alertmanager9093
모니터링Grafana3000
Exporter(참고)Node9100Prometheus가 스크래핑
Exporter(참고)Redis9121인스턴스별
Exporter(참고)MySQL9104
Exporter(참고)Nginx9113

16. 구축 순서(권장)

처음부터 구축할 때 권장하는 순서다. DB·Redis가 이미 있으면 해당 단계는 생략한다.

  1. MariaDB 준비 — Primary(및 필요 시 Replica). App/Worker가 접속 가능한 호스트/포트/계정 확인.
  2. Redis HA — cache → queue → socketio 순으로 각 디렉터리에서 docker compose up -d. sentinel2/3 conf 복사 확인.
  3. VIP/Proxy(선택) — Redis를 단일 엔드포인트로 쓸 경우 HAProxy 등 구성. .env·common_site_config.json의 Redis 호스트를 VIP로 설정.
  4. App 클러스터.env·common_site_config.json 반영, sites 볼륨 최초 시 bench init/new-site 수행 후 docker compose up -d. Scheduler는 한 노드만 기동.
  5. 모니터링prometheus.yml targets를 실제 호스트로 수정 후 docker compose up -d. Grafana에서 Prometheus 데이터 소스 추가.
  6. 검증 — §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 서버에서만 접근 가능하도록 방화벽 설정 권장.
Help