학습일지/K-Digital Traing

[KDT] AIaaS 마스터클래스 2주차 - Prometheus, Grafana 실습

tierr 2025. 4. 2. 15:37

Prometheus Grafana 실습

  • Docker Desktop 설치 (Windows/macOS)
  • 텍스트 에디터 (VS Code 권장)
  • 기존 Loki 스택 실습 환경 (기존 디렉토리 계속 사용)
  • Day7 실습했던 폴더에 이어서 진행한다.

1. 프로젝트를 구성하는 폴더 및 파일 구조 

파일구조는 다음과 같다. loki-stack-demo 폴더 안에 새로 추가할 파일들이 몇개 있다.

loki-stack-demo/
├── fastapi/             # 기존 폴더
├── load-test-app/   # 새로 추가
│   ├── Dockerfile
│   ├── requirements.txt
│   └── app.py
├── prometheus/      # 새로 추가
│   ├── prometheus.yml
│   └── rules.yml
├── alertmanager/    # 새로 추가
│   └── alertmanager.yml
├── docker-compose.yml  # 기존 파일 수정
└── ... (기존 파일들)

2. 소스코드 작성

2-1. 부하 테스트용 애플리케이션 (신규 파일 작성)

 load-test-app/Dockerfile

더보기
# ✅ Python 3.9의 슬림 버전 이미지를 베이스로 사용
#    용량 작고 불필요한 패키지가 제거되어있는 미니멀 이미지
FROM python:3.9-slim

# ✅ 컨테이너 안에서 작업할 기본 디렉토리 지정
#    이후 모든 명령은 /app 경로 기준으로 실행됨
WORKDIR /app

# ✅ 로컬의 requirements.txt (의존성 목록 파일) -> 컨테이너의 현재 디렉토리인 /app로 복사
COPY requirements.txt .

# ✅ 의존성 설치 (캐시 없이)
#    --no-cache-dir 옵션 : pip 캐시를 남기지 않음 → 이미지 용량 최적화
RUN pip install --no-cache-dir -r requirements.txt

# ✅ Flask 애플리케이션 메인 파일인 app.py를 컨테이너에 복사
COPY app.py .

# ✅ 컨테이너 외부에서 접근할 포트 지정 (Flask 기본 포트)
EXPOSE 5000

# ✅ 컨테이너 시작 시 실행할 명령어
#    → Flask 앱 실행
CMD ["python", "app.py"]

✅ 요약

명령어 설명
FROM Python 런타임 기반 이미지
WORKDIR 작업 디렉토리 설정
COPY 코드 및 설정 파일 복사
RUN 라이브러리 설치
EXPOSE 외부 접근 포트 열기 (5000)
CMD 앱 실행 (Flask 서버 구동)

 

  load-test-app/requirements.txt

더보기
# ✅ Flask: 간단한 Python의 웹 서버/REST API 프레임워크
#    - 부하 테스트용 HTTP 서버(app.py) 구현에 사용
flask==2.0.1

# ✅ Werkzeug: Flask 내부에서 사용하는 WSGI 서버 및 유틸리티
#    - 요청/응답 처리, 라우팅, 예외 처리 등 기본 기능 제공
werkzeug==2.0.1

# ✅ Prometheus 메트릭 수집 및 노출용 라이브러리
#    - Prometheus가 수집할 수 있는 `/metrics` 엔드포인트 제공
#    - request count, latency 등 모니터링 가능
#    - 사용 예) load_app_request_total, load_app_request_latency_seconds...
prometheus-client==0.11.0

# ✅ psutil: 시스템 자원 사용량(CPU, 메모리 등)을 모니터링하는 라이브러리
#    - `load_app_memory_usage_bytes`, `cpu_usage_percent` 계산에 사용
psutil==5.8.0

✅ 요약

 

패키지 설명 용도
flask 웹 서버 프레임워크 엔드포인트 처리
werkzeug *WSGI 서버 핵심 도구 Flask 내부 핵심 구성 요소
prometheus-client Prometheus용 메트릭 라이브러리 /metrics 노출
psutil 시스템 리소스 측정 도구 CPU/메모리 모니터링

🧩 WSGI 서버에 대해서 아래 글에 정리해 놓았다.

https://tierr.tistory.com/23

 

 load-test-app/app.py

localhost:5000

더보기
import time
import random
import threading
# ✅ psutil: 시스템 자원 (CPU, 메모리) 정보를 얻는 모듈
import psutil
import gc  # 가비지 컬렉션을 위한 모듈 추가
# ✅ Flask: 웹 서버 프레임워크
from flask import Flask, request, Response, jsonify
# ✅ Prometheus 메트릭을 수집하고 노출하는 라이브러리
from prometheus_client import Counter, Histogram, Gauge, generate_latest, REGISTRY

# ✅ Flask 앱 인스턴스 생성
app = Flask(__name__)

# 메트릭 정의 - Prometheus 메트릭 타입별 용도
# Counter: 계속 증가하는 값 (요청 수 등)
# Histogram: 값의 분포 측정 (응답 시간 등)
# Gauge: 증가하거나 감소할 수 있는 값 (메모리, CPU 사용량 등)
# ✅ 요청 수 카운터: 총 요청 수를 상태 코드 및 엔드포인트별로 구분
REQUEST_COUNT = Counter('load_app_requests_total', 'Total app HTTP requests', ['method', 'endpoint', 'status'])
# ✅ 요청 지연 시간 측정용 히스토그램 (초 단위)
REQUEST_LATENCY = Histogram('load_app_request_latency_seconds', 'Request latency in seconds', ['method', 'endpoint'])
# ✅ 메모리 사용량 게이지 (bytes)
MEMORY_USAGE = Gauge('load_app_memory_usage_bytes', 'Memory usage in bytes')
# ✅ CPU 사용률 게이지 (퍼센트)
CPU_USAGE = Gauge('load_app_cpu_usage_percent', 'CPU usage in percent')

# 스트레스 테스트 제어를 위한 글로벌 변수
stress_memory = False  # 메모리 부하 테스트 활성화 플래그
stress_cpu = False     # CPU 부하 테스트 활성화 플래그
memory_chunks = []     # 메모리 할당을 위한 리스트 (참조 유지 목적)

# 백그라운드 작업을 관리하기 위한 스레드 객체 저장
background_threads = []

def update_metrics():
    """백그라운드 스레드에서 주기적으로 시스템 메트릭 업데이트
    
    1초마다 현재 프로세스의 메모리 사용량과 CPU 사용률을 측정하여 
    Prometheus 게이지에 업데이트합니다.
    """
    while True:
        # 메모리 사용량 업데이트 - 바이트 단위
        MEMORY_USAGE.set(psutil.Process().memory_info().rss)
        
        # CPU 사용량 업데이트 - 퍼센트 단위
        CPU_USAGE.set(psutil.Process().cpu_percent(interval=0.1))
        
        time.sleep(0.9)  # 약 1초마다 업데이트 (interval 포함)

# 백그라운드 메트릭 업데이트 스레드 시작 - 앱 시작 시 실행
metrics_thread = threading.Thread(target=update_metrics, daemon=True)
metrics_thread.start()
print("메트릭 모니터링 스레드 시작됨")

def cpu_stress():
    """CPU에 부하를 주는 함수
    
    무작위 숫자 생성을 반복하여 CPU 사용률을 높입니다.
    stress_cpu 플래그가 False가 되면 스레드가 종료됩니다.
    """
    print("CPU 부하 테스트 시작")
    global stress_cpu
    
    while stress_cpu:
        # 백만 개의 랜덤 숫자 생성 - CPU 집약적 작업
        _ = [random.random() for _ in range(1000000)]
        time.sleep(0.01)  # 짧은 대기로 CPU 100% 점유 방지
    
    print("CPU 부하 테스트 종료")

def memory_stress():
    """메모리에 부하를 주는 함수
    
    10MB씩 메모리를 할당하여 memory_chunks 리스트에 저장합니다.
    최대 100MB까지 할당하거나 stress_memory 플래그가 False가 되면 종료됩니다.
    """
    print("메모리 부하 테스트 시작")
    global stress_memory, memory_chunks
    
    try:
        # 메모리 초기화 (이전 테스트에서 남은 메모리 해제)
        memory_chunks = []
        
        allocated_mb = 0
        while stress_memory and allocated_mb < 100:  # 최대 100MB 제한
            # 10MB 크기의 문자열 할당
            memory_chunks.append(' ' * 10 * 1024 * 1024)  # 10MB
            allocated_mb += 10
            print(f"메모리 할당: {allocated_mb}MB")
            time.sleep(1)  # 1초마다 10MB씩 증가
    
    except Exception as e:
        print(f"메모리 부하 테스트 중 오류 발생: {e}")
    
    print("메모리 부하 테스트 종료")

@app.route('/')
def home():
    """메인 페이지
    
    간단한 응답을 반환하며, 약간의 무작위 지연을 추가합니다.
    요청 수와 응답 시간을 Prometheus 메트릭으로 기록합니다.
    """
    start_time = time.time()  # 요청 시작 시간
    status_code = 200
    
    # 랜덤 지연 추가 (0-0.5초) - 실제 환경의 변동성 시뮬레이션
    time.sleep(random.random() * 0.5)
    
    # 요청 카운터 증가 및 응답 시간 기록
    REQUEST_COUNT.labels('get', '/', status_code).inc()
    REQUEST_LATENCY.labels('get', '/').observe(time.time() - start_time)
    
    return 'Load Testing Application\n'

@app.route('/metrics')
def metrics():
    """Prometheus 메트릭 엔드포인트
    
    Prometheus가 주기적으로 이 엔드포인트를 스크랩하여
    모든 정의된 메트릭(요청 수, 응답 시간, 메모리, CPU 등)을 수집합니다.
    """
    return Response(generate_latest(REGISTRY), mimetype='text/plain')

@app.route('/error')
def error():
    """에러 응답을 시뮬레이션하는 엔드포인트
    
    500 상태 코드를 반환하여 서버 오류 상황을 시뮬레이션합니다.
    에러 요청 수와 응답 시간을 Prometheus 메트릭으로 기록합니다.
    """
    start_time = time.time()
    status_code = 500  # 서버 오류 상태 코드
    
    # 에러 요청 카운터 증가 및 응답 시간 기록
    REQUEST_COUNT.labels('get', '/error', status_code).inc()
    REQUEST_LATENCY.labels('get', '/error').observe(time.time() - start_time)
    
    return 'Error occurred', 500

@app.route('/slow')
def slow():
    """느린 응답을 시뮬레이션하는 엔드포인트
    
    1-3초 사이의 지연을 추가하여 느린 응답을 시뮬레이션합니다.
    느린 요청 수와 응답 시간을 Prometheus 메트릭으로 기록합니다.
    """
    start_time = time.time()
    status_code = 200
    
    # 1-3초 사이 랜덤 지연 - 느린 응답 시뮬레이션
    delay = 1 + random.random() * 2
    time.sleep(delay)
    
    # 요청 카운터 증가 및 응답 시간 기록
    REQUEST_COUNT.labels('get', '/slow', status_code).inc()
    REQUEST_LATENCY.labels('get', '/slow').observe(time.time() - start_time)
    
    return f'Slow response (delay: {delay:.2f}s)\n'

@app.route('/status')
def status():
    """현재 부하 테스트 상태를 반환하는 엔드포인트
    
    현재 메모리 및 CPU 부하 테스트 상태와 실제 리소스 사용량을 JSON 형식으로 반환합니다.
    """
    global stress_memory, stress_cpu, memory_chunks
    
    # 현재 리소스 사용량 및 부하 테스트 상태 수집
    status_info = {
        "memory_stress_active": stress_memory,
        "cpu_stress_active": stress_cpu,
        "allocated_memory_mb": len(memory_chunks) * 10,  # 10MB 단위로 할당
        "memory_usage_mb": round(psutil.Process().memory_info().rss / (1024 * 1024), 2),
# ✅ CPU 사용률 게이지 (퍼센트)
        "cpu_usage_percent": round(psutil.Process().cpu_percent(interval=0.1), 2)
    }
    
    return jsonify({
        "status": "running",
        "stress_tests": status_info,
        "timestamp": time.strftime("%Y-%m-%d %H:%M:%S")
    })

@app.route('/stress/memory/<action>')
def stress_memory_endpoint(action):
    """메모리 부하 테스트 엔드포인트
    
    메모리 사용량을 인위적으로 증가시키거나 해제합니다.
    'start': 메모리 사용량을 점진적으로 증가 (최대 100MB)
    'stop': 할당된 메모리를 해제하고 가비지 컬렉션 실행
    """
    global stress_memory, memory_chunks, background_threads
    
    if action == 'start':
        if not stress_memory:
            stress_memory = True
            print("메모리 부하 테스트 시작 명령 수신")
            
            # 별도 스레드에서 메모리 부하 생성
            memory_thread = threading.Thread(target=memory_stress, daemon=True)
            memory_thread.start()
            background_threads.append(memory_thread)
            
            return 'Memory stress started\n'
        else:
            return 'Memory stress already running\n'
            
    elif action == 'stop':
        stress_memory = False
        print("메모리 부하 테스트 중지 명령 수신")
        
        # 메모리 명시적 해제
        memory_chunks = []
        
        # 가비지 컬렉션 강제 실행
        gc.collect()
        print("메모리 해제 및 가비지 컬렉션 실행")
        
        return 'Memory stress stopped\n'
    else:
        return 'Invalid action\n', 400

@app.route('/stress/cpu/<action>')
def stress_cpu_endpoint(action):
    """CPU 부하 테스트 엔드포인트
    
    CPU 사용률을 인위적으로 증가시키거나 정상화합니다.
    'start': CPU 집약적인 작업을 통해 CPU 사용률 증가
    'stop': CPU 부하 생성 중단
    """
    global stress_cpu, background_threads
    
    if action == 'start':
        if not stress_cpu:
            stress_cpu = True
            print("CPU 부하 테스트 시작 명령 수신")
            
            # 별도 스레드에서 CPU 부하 생성
            cpu_thread = threading.Thread(target=cpu_stress, daemon=True)
            cpu_thread.start()
            background_threads.append(cpu_thread)
            
            return 'CPU stress started\n'
        else:
            return 'CPU stress already running\n'
            
    elif action == 'stop':
        stress_cpu = False
        print("CPU 부하 테스트 중지 명령 수신")
        return 'CPU stress stopped\n'
    else:
        return 'Invalid action\n', 400

if __name__ == '__main__':
    # 앱이 시작될 때 출력
    print("부하 테스트 애플리케이션 시작")
    print("사용 가능한 엔드포인트:")
    print("- /: 기본 페이지")
    print("- /metrics: Prometheus 메트릭")
    print("- /slow: 느린 응답 시뮬레이션")
    print("- /error: 오류 응답 시뮬레이션")
    print("- /status: 현재 부하 테스트 상태")

# 🔹 메모리 부하 테스트 시작
    print("- /stress/memory/start|stop: 메모리 부하 테스트")

# 🔹 CPU 부하 테스트 시작
    print("- /stress/cpu/start|stop: CPU 부하 테스트")
    
    # 모든 인터페이스에서 5000번 포트로 서비스 시작
    app.run(host='0.0.0.0', port=5000)

✅ 요약

엔드포인트 설명
/ 기본 응답 (약간의 지연 포함)
/slow 느린 응답 시뮬레이션 (1~3초 지연)
/error 500 에러 응답
/metrics Prometheus 메트릭 제공
/status 현재 메모리/CPU 상태
/stress/memory/start 메모리 부하 시작 (100MB 할당)
/stress/memory/stop 메모리 부하 해제
/stress/cpu/start CPU 부하 시작 (무한 루프)
/stress/cpu/stop CPU 부하 중단

🔍 Prometheus 메트릭

메트릭 이름 *타입 설명
load_app_requests_total Counter 요청 수 (method, endpoint, status별 구분)
load_app_request_latency_seconds Histogram 요청 지연 시간 분포
load_app_memory_usage_bytes Gauge 현재 메모리 사용량
load_app_cpu_usage_percent Gauge 현재 CPU 사용률

 

🧩 Prometheus의 타입에 대해서 아래 글에 정리해 놓았다.

https://tierr.tistory.com/22

 

2-2. Prometheus 및 AlertManager 설정  (신규 파일 작성)

 prometheus/prometheus.yml

더보기
global:
  scrape_interval: 15s         # 모든 타겟의 메트릭을 15초 간격으로 수집
  evaluation_interval: 15s     # 알림 규칙을 15초마다 평가

# Alertmanager 설정
alerting:
  alertmanagers:
    - static_configs:
        - targets:               # Prometheus가 조건을 만족하는 메트릭에 대해 Alert를 전송할 대상 지정
            - alertmanager:9093  # AlertManager 컨테이너 주소 (도커 내부)

# 알림 조건을 정의한 외부 파일 로드
rule_files:
  - "rules.yml"  # 알림 조건(예: 에러율이 일정 이상 등)들이 들어 있는 규칙 파일
# 메트릭 수집 대상 정의
scrape_configs:

  # FastAPI 앱의 메트릭 수집 설정
  - job_name: "fastapi"
    metrics_path: "/metrics"  # FastAPI가 제공하는 엔드포인트(/metrics)에서 메트릭 수집
    static_configs:
      - targets: ["fastapi:8000"]  # 도커 네트워크상의 서비스명:포트

  # 부하 테스트용 Flask앱의 메트릭 수집 설정
  - job_name: "load-test-app"
    static_configs:
      - targets: ["load-test-app:5000"]  # Flask 앱

  # Prometheus 자체 메트릭 (자기 자신도 모니터링)
  - job_name: "prometheus"
    static_configs:
      - targets: ["localhost:9090"]

  # cAdvisor 컨테이너 메트릭 (Docker 컨테이너의 CPU, 메모리, 디스크 사용량 추적)
  - job_name: "cadvisor"
    static_configs:
      - targets: ["cadvisor:8080"]

  # Node Exporter 메트릭 (호스트 시스템의 메트릭 수집)
  - job_name: "node"
    static_configs:
      - targets: ["node-exporter:9100"]

✅ 요약

설정 설명
scrape_interval 모든 대상에서 메트릭 수집 주기 설정
alertmanagers 알림을 보낼 대상 설정 (AlertManager)
rule_files 알림 조건을 정의한 외부 파일 지정
scrape_configs 메트릭을 수집할 대상들 정의 (FastAPI, Prometheus 등)

 

 prometheus/rules.yml

더보기
groups:
  - name: app_alerts          # 알림 그룹 이름(app_alerts)
    rules:

      # ✅ 높은 메모리 사용량 알림
      - alert: HighMemoryUsage                         # 알림 이름(HighMemoryUsage)
        expr: load_app_memory_usage_bytes > 100000000  # 메모리 사용량이 100MB 초과 시 알림
        for: 1m                                        # 1분간 지속 시 경고
        labels:
          severity: warning                            # 경고 등급 (warning)
        annotations:
          summary: "High memory usage detected"        # 경고 제목
          description: "App memory usage is {{ $value | humanize }} bytes"  # 상세 설명 (실제 메모리 사용량 표시)

      # ✅ 높은 CPU 사용량 알림
      - alert: HighCpuUsage
        expr: load_app_cpu_usage_percent > 80          # CPU 사용률 80% 초과 시 알림
        for: 1m                                        # 1분간 지속 시 경고
        labels:
          severity: warning
        annotations:
          summary: "High CPU usage detected"
          description: "App CPU usage is {{ $value }}%"  # 상세 설명 (실제 CPU 사용량 표시)

      # ✅ 느린 응답 시간 알림
      - alert: SlowResponseTime
        expr: rate(load_app_request_latency_seconds_sum[5m]) 
            / rate(load_app_request_latency_seconds_count[5m]) > 1  # 5분 동안의 평균 응답 시간이 1초 초과 시 알림
        for: 2m                                                     # 2분간 지속 시 경고
        labels:
          severity: warning
        annotations:
          summary: "Slow response time detected"
          description: "Average response time is {{ $value }} seconds"  # 상세 설명 (평균 응답 시간 표시)

 

✅ 알림 규칙 요약

알림 이름 조건 지속 시간 의미
HighMemoryUsage 메모리 사용량 > 100MB 1분 과도한 메모리 사용 감지
HighCpuUsage CPU 사용률 > 80% 1분 CPU 부하 감지
SlowResponseTime 평균 응답 시간 > 1초 2분 서비스 지연 경고

📦 사용 목적

  • 시스템 또는 앱이 지속적으로 비정상 상태일 때만 알림
  • AlertManager를 통해 Slack, Email 등으로 전송 가능
  • Grafana에서도 상태를 시각적으로 확인 가능


  alertmanager/alertmanager.yml

더보기
global:      # 경고 처리 타이밍 설정
  resolve_timeout: 5m  # 알림이 해결된 후 '해결됨' 상태를 전파하는 데 걸리는 시간 (5분)

route:       # 알림 라우팅 정책 - 어떤 알림을 누구에게 언제 보낼지 정의
  group_by: ['alertname']        # 동일 alertname을 가진 알림끼리 그룹화
  group_wait: 10s                # 첫 알림이 발생하고 그룹 알림을 전송하기까지 대기 시간
  group_interval: 10s            # 동일 그룹 내 추가 알림이 다시 전송되는 최소 간격
  repeat_interval: 1h            # 동일 알림을 반복 전송하는 간격 (1시간)
  receiver: 'web.hook'           # 기본 수신 대상 (아래 receivers에서 정의됨)

receivers:    # 알림 전송 대상 (Webhook, Slack 등)
  - name: 'web.hook'                   # 수신자 이름 (web.hook)
    webhook_configs:
      - url: 'http://localhost:5001/'  # 알림이 전송될 Webhook 주소 (실제 환경에서는 Slack, Discord 등 외부 서비스 사용 가능)

inhibit_rules: # 덜 중요한 알림을 자동으로 억제
  - source_match:                # 심각한(critical) 알림이 발생한 경우
      severity: 'critical'
    target_match:                # 동일 alertname을 가진 경고(warning)는 전송 억제
      severity: 'warning'        # 예: HighMemoryUsage가 critical 수준으로 발생하면,
    equal: ['alertname']         #     같은 alertname의 warning 수준 알림은 따로 전송하지 않음.

✅ 요약 정리

항목 설명
global.resolve_timeout 알림이 해결된 후 상태 전파까지 대기 시간
route.group_by 알림을 그룹화할 기준 (alertname)
group_wait 알림 발생 후 묶어서 전송할 때까지 대기 시간
group_interval 같은 그룹 알림이 다시 전송되는 최소 간격
repeat_interval 같은 알림이 다시 전송되는 간격 (기본: 1시간)
receiver 알림이 전송될 대상 이름
webhook_configs.url 알림 전송 대상의 주소 (테스트용 서버 등)
inhibit_rules 경고가 심각(Critical) 경고에 의해 억제되는 조건

2-3. 기존 FastAPI 애플리케이션에 Prometheus 지원 추가  (기존 파일 수정)

※ 기존 파일을 수정하는 파트이므로, 무슨 코드를 추가했고 추가했는지에 대해 중점적으로 작성했다.

■  fastapi/requirements.txt 파일 수정

prometheus-client==0.11.0

fastapi/requirements.txt 수정내역

🔍 왜 수정이 필요할까?

✅ FastAPI 애플리케이션에 Prometheus 메트릭을 노출하기 위해.

Prometheus는 앱 내부의 상태를 주기적으로 수집해서 모니터링하는 시스템.
이를 위해 앱은 반드시 /metrics 같은 엔드포인트에서 Prometheus가 이해할 수 있는 형식의 메트릭을 노출해야 한다.

이를 가능하게 해주는 Python 라이브러리가 바로 prometheus-client 다.


  fastapi/main.py 파일 수정

1️⃣ 메트릭 정의 추가 및 /metrics 엔드 포인트 추가

  • Counter: 요청 수를 상태코드, 메서드, 경로별로 기록
  • Histogram: 요청 시간 분포 기록
  • Prometheus는 /metrics 경로를 폴링하여 메트릭 수집
  • FastAPI 애플리케이션과 분리된 ASGI 앱으로 구성

fastapi/main.py - 수정내역 1

2️⃣ 요청 처리 시 메트릭 기록 추가

  • /metrics 요청은 자기 자신이므로 무시
  • 그 외 모든 요청은 지표로 기록

fastapi/main.py - 수정내역 2


2-4. Docker Compose 파일 수정 (기존 파일 수정)

  docker-compose.yml 파일 수정

1️⃣ load-test-app 추가
Flask 기반 테스트 앱
Prometheus 메트릭 노출, 요청 지연, CPU/메모리 부하 테스트 가능
✅ 목적: 실제 요청 시뮬레이션을 위한 독립 테스트 환경
2️⃣ nginx 포트 변경 (8080 → 8081)
cAdvisor와 포트 충돌을 피하기 위해 조정


3️⃣  prometheus 추가
메트릭 수집 시스템
Loki와 별개로 CPU, 메모리, 요청 지연 등 추적 가능
✅ 목적: FastAPI + load-test-app + 시스템 전체 성능 추적


4️⃣ alertmanager 추가
Prometheus에서 정의한 규칙에 따라 알림 전송 담당Slack, Email 등과 연동 가능
✅ 목적: 응답 지연/에러율 증가 등 이상 징후에 대한 자동 경고
5️⃣  cadvisor 추가
Docker 컨테이너별 CPU, 메모리, 디스크 I/O 등 추적시각화 및 Alert 설정에 사용
✅ 목적: 컨테이너 수준 리소스 모니터링


6️⃣  node-exporter 추가
호스트 시스템 수준 메트릭 수집 (CPU, Load, Filesystem 등)
Prometheus 기본 대상 중 하나
✅ 목적: 시스템 전체 리소스 사용 상태 확인
7️⃣  grafana 서비스 개선
기존엔 Loki만 연결
Prometheus를 추가하면서 시계열 메트릭 시각화 가능
8️⃣ 볼륨 정의 추가
컨테이너 재시작 시에도 메트릭 & 알림 설정 유지

🎯 전체 설계 목적 요약

목표 관련 서비스
✅ 로그 수집 Loki + Promtail
✅ 로그 시각화 Grafana + Loki
✅ 시스템/앱 성능 측정 Prometheus, Node Exporter, cAdvisor
✅ 부하 테스트 load-test-app
✅ 이상 탐지 및 알림 Prometheus + Alertmanager
✅ 종합 관측 대시보드 Grafana (Loki + Prometheus 둘 다 사용 가능)

4. 실행

docker-compose 빌드 및 실행

# 기존 서비스가 실행 중이면 중지
docker-compose down -v
docker-compose build --no-cache

# 컨테이너 빌드 및 실행
docker-compose up -d

# 서비스 상태 확인
docker-compose ps

 

5. Prometheus 메트릭 확인

1. Prometheus 웹 인터페이스 접속
    - 웹 브라우저에서 'http://localhost:9090' 접속
2. 메트릭 확인
    - Status > Targets 메뉴에서 모든 대상이 UP 상태인지 확인
    - Graph 탭에서 PromQL 쿼리를 실행하여 메트릭 확인

 

6. Grafana 통합 대시보드 구성

1. Grafana 접속
    - 웹 브라우저에서 'http://localhost:3000' 접속
2. Prometheus 데이터 소스 추가

Prometheus 접속 화면

 

3. Node Exporter 대시보드 가져오기

 

4. 커스텀 FastAPI 애플리케이션 대시보드 생성

패널을 2개 추가해보자.

  • 패널1. FastAPI 요청: fastapi requests
    rate(fastapi_requests_total[5m])
  • 패널2. FastAPI 응답 시간: fastapi request latency
    rate(fastapi_request_latency_seconds_sum[5m]) / rate(fastapi_request_latency_seconds_count[5m])

완성하고 요청을 보내보면 다음과 같이 보인다

커스텀 FastAPI 애플리케이션 대시보드 완성~

 

5. 부하 테스트 애플리케이션 대시보드 생성

패널을 4개 추가해보자.

  • 패널1. 부하 테스트 앱 요청: load app requests
    rate(load_app_requests_total[5m])
  • 패널2. 부하 테스트 앱 응답 시간: load app request latency
    rate(load_app_request_latency_seconds_sum[5m]) / rate(load_app_request_latency_seconds_count[5m])
  • 패널3. 부하 테스트 앱 메모리 사용량: load app memory usage
    load_app_memory_usage_bytes / 1024 / 1024
  • 패널4. 부하 테스트 앱 메모리 사용량: load app memory usage
    load_app_cpu_usage_percent

부하 테스트 애플리케이션 대시보드

6. 통합 로그와 메트릭 대시보드 생성

하나의 패널에 여러 쿼리를 입력해서 여러 실행 결과를 한번에 볼 수도 있고,

프로메테우스의 Time Series, Loki의 로그를 혼합해서 대시보드를 구성할 수도 있다.

로그 + 메트릭를 혼합한 커스텀 대시보드 생성

 

7. 대시보드 저장

대시보드는 꼭 저장을 해줍시다. 저장을 안하고 새로고침하면 날아갑니다.


7. 부하 테스트 및 모니터링

Grafana로 구성한 대시보드의 변화를 확인하기 위해 ChatGPT로 테스트 시나리오를 생성해 요청을 보냈다.

.bat 파일로 저장해서 실행하면 된다.

 

✅ Windows CMD용 테스트 시나리오

1️⃣ FastAPI 요청 시뮬레이션 (패널1, 패널2)

@echo off
echo === FastAPI 요청 시뮬레이션 시작 ===

:: 빠른 요청 (정상 요청)
for /L %%i in (1,1,20) do (
  curl http://localhost:8081/ >nul
  timeout /t 1 >nul
)

:: 에러 요청 (500 응답)
for /L %%i in (1,1,5) do (
  curl http://localhost:8081/error >nul
  timeout /t 1 >nul
)

 

2️⃣ Load Test App 요청 시뮬레이션 (패널1~4)

@echo off
echo === Load Test App 요청 시뮬레이션 시작 ===

:: 빠른 요청
for /L %%i in (1,1,20) do (
  curl http://localhost:5000/ >nul
  timeout /t 1 >nul
)

:: 느린 요청
for /L %%i in (1,1,10) do (
  curl http://localhost:5000/slow >nul
  timeout /t 1 >nul
)

:: 에러 요청
for /L %%i in (1,1,5) do (
  curl http://localhost:5000/error >nul
  timeout /t 1 >nul
)

 

3️⃣ 메모리 & CPU 부하 테스트 (패널3, 패널4)

@echo off
echo === Load Test App 요청 시뮬레이션 시작 ===

:: 빠른 요청
for /L %%i in (1,1,20) do (
  curl http://localhost:5000/ >nul
  timeout /t 1 >nul
)

:: 느린 요청
for /L %%i in (1,1,10) do (
  curl http://localhost:5000/slow >nul
  timeout /t 1 >nul
)

:: 에러 요청
for /L %%i in (1,1,5) do (
  curl http://localhost:5000/error >nul
  timeout /t 1 >nul
)

트러블 슈팅

대시보드 구성 중에 Loki 패널을 하나 만들었는데 로그가 나오지 않고 다음과 같은 에러가 떴다.

  • Data is missing a number field

 

원인)

“Data is missing a number field” 에러는 숫자(메트릭)가 필요한 시각화 유형에 로그 데이터를 연결했기 때문에 나오는 에러라고 한다. Loki 패널을 추가할때 [오른쪽 상단 메뉴 (⋮) 클릭 → Add → Visualization] 이런 순서로 추가했는데, 시각화(Visualization)유형이 Time Series로 기본 선택되어 있었다.

 

해결)

Loki는 로그(logs) 를 다루는 시스템이기 때문에, 시각화 유형을 Logs로 선택해야 한다.

 


본 후기는 [카카오엔터프라이즈x스나이퍼팩토리] 카카오클라우드로 배우는 AIaaS 마스터 클래스 (B-log) 리뷰로 작성 되었습니다.