prometheus & grafana

강정우·약 12시간 전

Dev_Ops

목록 보기
27/28
post-thumbnail

nginx-rtmp 미디어 서버 모니터링 구축기 (Prometheus + Grafana)

libnginx-mod-rtmp + ffmpeg 기반 HLS 미디어 서버에 Prometheus와 Grafana를 붙여 생사 여부를 실시간으로 파악하는 대시보드를 구축한 과정을 기록한다.


아키텍처 개요

nginx-rtmp (/stat) ──→ Python Exporter (9101)  ─┐
node_exporter (9100)                              ├──→ Prometheus (9090) ──→ Grafana (3000)
ffmpeg 프로세스 상태                              ─┘

세 가지 수집 레이어로 구성된다.

  • node_exporter : 서버 CPU/메모리/디스크/네트워크 등 시스템 지표
  • 커스텀 Python exporter : nginx-rtmp /stat XML을 파싱해 스트림 상태를 Prometheus 포맷으로 변환
  • Prometheus : 위 두 exporter를 주기적으로 스크랩해 시계열 DB에 저장
  • Grafana : Prometheus를 데이터소스로 연결해 대시보드 시각화

1단계 — Prometheus 설치

사용자 및 디렉토리 준비

sudo useradd --no-create-home --shell /bin/false prometheus

sudo mkdir -p /etc/prometheus /var/lib/prometheus
sudo chown prometheus:prometheus /etc/prometheus /var/lib/prometheus

바이너리 다운로드

cd /tmp
wget https://github.com/prometheus/prometheus/releases/download/v3.4.1/prometheus-3.4.1.linux-amd64.tar.gz
tar xvf prometheus-3.4.1.linux-amd64.tar.gz
cd prometheus-3.4.1.linux-amd64

sudo cp prometheus promtool /usr/local/bin/
sudo chown prometheus:prometheus /usr/local/bin/prometheus /usr/local/bin/promtool

console 패키지는 3 버전에서 더이상 제공되지 않습니다.
왜냐하면 관리, 유지보수가 어렵고 그냥 모든 사람들도 grafana 와 같은 third-party library 와 함께 사용하는 게 국룰이 되어버렸기 때문에 tar 로 압축을 풀어봐도 console 패키지가 없으니 스킵!

prometheus.yml 작성

sudo nano /etc/prometheus/prometheus.yml
global:
  scrape_interval:     15s
  evaluation_interval: 15s
  scrape_timeout:      15s

scrape_configs:
  - job_name: 'prometheus'
    static_configs:
      - targets: ['localhost:9090']

  - job_name: 'node'
    static_configs:
      - targets: ['localhost:9100']

  - job_name: 'nginx_rtmp'
    static_configs:
      - targets: ['localhost:9101']
    scrape_interval: 10s
sudo chown prometheus:prometheus /etc/prometheus/prometheus.yml

systemd 서비스 등록

sudo nano /etc/systemd/system/prometheus.service
[Unit]
Description=Prometheus Monitoring
Wants=network-online.target
After=network-online.target

[Service]
User=prometheus
Group=prometheus
Type=simple
ExecStart=/usr/local/bin/prometheus \
    --config.file=/etc/prometheus/prometheus.yml \
    --storage.tsdb.path=/var/lib/prometheus/ \
    --storage.tsdb.retention.time=30d \
    --web.console.templates=/etc/prometheus/consoles \
    --web.console.libraries=/etc/prometheus/console_libraries \
    --web.listen-address=0.0.0.0:9090 \
    --web.enable-lifecycle

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now prometheus

--web.enable-lifecycle 플래그를 추가해두면 재시작 없이 설정 리로드가 가능하다.

# 설정 변경 후 무중단 리로드
curl -X POST http://localhost:9090/-/reload

2단계 — node_exporter 설치

cd /tmp
wget https://github.com/prometheus/node_exporter/releases/download/v1.9.0/node_exporter-1.9.0.linux-amd64.tar.gz
tar xvf node_exporter-1.9.0.linux-amd64.tar.gz

sudo useradd --no-create-home --shell /bin/false node_exporter
sudo install -m 755 node_exporter-1.9.0.linux-amd64/node_exporter /usr/local/bin/node_exporter
sudo chown node_exporter:node_exporter /usr/local/bin/node_exporter

Tip. 기존에 node_exporter가 실행 중인 상태에서 cp로 덮어쓰려 하면 Text file busy 오류가 발생한다. 이때는 install 명령을 쓰거나 서비스를 먼저 중단한 뒤 복사한다.

# 방법 1: install 명령 (서비스 중단 불필요)
sudo install -m 755 node_exporter /usr/local/bin/node_exporter

# 방법 2: 서비스 중단 후 복사
sudo systemctl stop node_exporter
sudo cp node_exporter /usr/local/bin/
sudo systemctl start node_exporter
sudo nano /etc/systemd/system/node_exporter.service
[Unit]
Description=Node Exporter
Wants=network-online.target
After=network-online.target

[Service]
User=node_exporter
Group=node_exporter
Type=simple
ExecStart=/usr/local/bin/node_exporter \
    --collector.systemd \
    --collector.processes

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now node_exporter

3단계 — nginx-rtmp 커스텀 Exporter

nginx-rtmp의 /stat XML 엔드포인트를 파싱해 Prometheus 포맷으로 9101번 포트에 노출하는 Python 스크립트다.

패키지 설치

# apt로 시스템 전역 설치 (pip install은 systemd 서비스에서 인식 못함)
sudo apt install -y python3-lxml python3-requests
sudo apt install -y python3-prometheus-client
# apt에 없다면:
# sudo pip3 install prometheus_client --break-system-packages

pip3 install을 일반 사용자 권한으로 실행하면 시스템 Python에 반영되지 않는다. systemd 서비스는 시스템 Python을 바라보므로 반드시 apt 또는 sudo pip3로 설치해야 한다.

설치 후 한 번에 검증:

python3 -c "import prometheus_client, requests, lxml; print('모두 OK')"

exporter 스크립트

sudo mkdir -p /opt/nginx_rtmp_exporter
sudo nano /opt/nginx_rtmp_exporter/exporter.py
#!/usr/bin/env python3
"""
nginx-rtmp → Prometheus Exporter
포트 9101에서 메트릭 노출
"""

import time
import requests
import logging
from lxml import etree
from prometheus_client import start_http_server, Gauge

logging.basicConfig(level=logging.INFO, format='%(asctime)s %(levelname)s %(message)s')

RTMP_STAT_URL  = "http://localhost:8888/stat"
SCRAPE_INTERVAL = 10

# ── 메트릭 정의 ────────────────────────────────────────────────────────────
rtmp_up = Gauge('nginx_rtmp_up', 'nginx-rtmp 서버 접근 가능 여부 (1=정상, 0=장애)')

rtmp_stream_count = Gauge('nginx_rtmp_stream_count', '현재 활성 스트림 수', ['app'])
rtmp_clients      = Gauge('nginx_rtmp_clients',      '스트림별 클라이언트 수', ['app', 'stream'])
rtmp_bw_in        = Gauge('nginx_rtmp_bw_in_bps',    '입력 대역폭 (bps)',     ['app', 'stream'])
rtmp_bw_out       = Gauge('nginx_rtmp_bw_out_bps',   '출력 대역폭 (bps)',     ['app', 'stream'])
rtmp_bytes_in     = Gauge('nginx_rtmp_bytes_in_total','누적 수신 바이트',      ['app', 'stream'])
rtmp_ffmpeg_count = Gauge('nginx_rtmp_ffmpeg_processes', '실행 중인 ffmpeg 프로세스 수')
hls_segment_count = Gauge('nginx_rtmp_hls_segment_count', 'HLS .ts 세그먼트 파일 수', ['stream'])

# ── 수집 함수 ──────────────────────────────────────────────────────────────
def count_ffmpeg_processes() -> int:
    import subprocess
    try:
        r = subprocess.run(['pgrep', '-c', 'ffmpeg'], capture_output=True, text=True)
        return int(r.stdout.strip()) if r.returncode == 0 else 0
    except Exception:
        return 0

def count_hls_segments(hls_root: str = '/var/hls') -> dict:
    import os
    counts = {}
    try:
        for entry in os.scandir(hls_root):
            if entry.is_dir():
                counts[entry.name] = len([f for f in os.listdir(entry.path) if f.endswith('.ts')])
    except FileNotFoundError:
        pass
    return counts

def collect():
    try:
        resp = requests.get(RTMP_STAT_URL, timeout=5)
        resp.raise_for_status()
        rtmp_up.set(1)
    except Exception as e:
        logging.warning(f"stat 엔드포인트 접근 실패: {e}")
        rtmp_up.set(0)
        return

    try:
        root = etree.fromstring(resp.content)
        for server in root.findall('.//server'):
            for app in server.findall('application'):
                app_name = app.findtext('name', default='unknown')
                live = app.find('live')
                if live is None:
                    continue
                streams = live.findall('stream')
                rtmp_stream_count.labels(app=app_name).set(len(streams))
                for stream in streams:
                    name   = stream.findtext('name', default='unknown')
                    rtmp_clients.labels(app=app_name, stream=name).set(len(stream.findall('.//client')))
                    rtmp_bw_in.labels(app=app_name,   stream=name).set(int(stream.findtext('bw_in',    '0')))
                    rtmp_bw_out.labels(app=app_name,  stream=name).set(int(stream.findtext('bw_out',   '0')))
                    rtmp_bytes_in.labels(app=app_name,stream=name).set(int(stream.findtext('bytes_in', '0')))
    except Exception as e:
        logging.error(f"XML 파싱 오류: {e}")

    rtmp_ffmpeg_count.set(count_ffmpeg_processes())
    for sname, cnt in count_hls_segments().items():
        hls_segment_count.labels(stream=sname).set(cnt)

if __name__ == '__main__':
    start_http_server(9101)
    logging.info("nginx-rtmp exporter 가동 — :9101")
    while True:
        collect()
        time.sleep(SCRAPE_INTERVAL)

systemd 서비스 등록

sudo nano /etc/systemd/system/nginx_rtmp_exporter.service
[Unit]
Description=nginx-rtmp Prometheus Exporter
After=network.target

[Service]
User=prometheus
ExecStart=/usr/bin/python3 /opt/nginx_rtmp_exporter/exporter.py
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now nginx_rtmp_exporter

# 정상 동작 확인
curl http://localhost:9101/metrics | grep nginx_rtmp

4단계 — 수집 타겟 확인

설정 변경 후에는 반드시 리로드하고 타겟 상태를 점검한다.

curl -X POST http://localhost:9090/-/reload

sleep 30 && curl -s http://localhost:9090/api/v1/targets \
  | python3 -m json.tool \
  | grep -E '"job"|"health"|"lastError"'

세 job 모두 "health": "up" 이어야 정상이다.

"job": "nginx_rtmp"  →  "health": "up"
"job": "node"        →  "health": "up"
"job": "prometheus"  →  "health": "up"

주의. Prometheus를 재시작하지 않고 prometheus.yml만 수정하면 변경 사항이 반영되지 않는다. --web.enable-lifecycle 플래그가 있다면 위처럼 reload API를 활용하면 되고, 없다면 sudo systemctl restart prometheus로 재시작해야 한다.


5단계 — Grafana 설치

sudo apt install -y apt-transport-https software-properties-common
wget -q -O - https://apt.grafana.com/gpg.key \
  | sudo gpg --dearmor -o /usr/share/keyrings/grafana.gpg
echo "deb [signed-by=/usr/share/keyrings/grafana.gpg] https://apt.grafana.com stable main" \
  | sudo tee /etc/apt/sources.list.d/grafana.list

sudo apt update && sudo apt install grafana -y
sudo systemctl enable --now grafana-server

6단계 — nginx 리버스 프록시 + 리다이렉트 루프 해결

서브경로(/grafana/)로 Grafana를 서빙하려면 nginx 설정grafana.ini 두 곳을 모두 올바르게 맞춰야 한다. 한쪽이라도 어긋나면 301 무한 루프가 발생한다.

자주 하는 실수들

실수증상
server_nameyour-domain.com 플레이스홀더로 방치요청이 엉뚱한 서버 블록으로 라우팅됨
기존 도메인 블록과 별도 server 블록 중복 생성블록 충돌로 예측 불가 동작
proxy_pass http://localhost:3000/ (끝에 /)prefix /grafana/가 제거되어 Grafana가 /grafana/로 redirect → 루프
grafana.inidomain, root_url, serve_from_sub_path 주석 상태Grafana가 localhost로 redirect

nginx — 기존 서버 블록에 location 추가

별도 conf 파일을 만들지 말고, 해당 도메인의 기존 블록에 아래 location을 추가한다.

http {

        ##
        # Basic Settings
        ##

		...
        
        server {
                listen 8888;
                allow 127.0.0.1;
                deny all;

                location /stat {
                        rtmp_stat all;
                        rtmp_stat_stylesheet stat.xsl;
                }
        }

}
location /grafana/ {
    proxy_pass            http://localhost:3000;   # 끝 슬래시 없음!
    proxy_set_header      Host $host;
    proxy_set_header      X-Real-IP $remote_addr;
    proxy_set_header      X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header      X-Forwarded-Proto https;  # $scheme 대신 하드코딩
    proxy_redirect        off;
}

proxy_pass http://localhost:3000/ (슬래시 있음)으로 설정하면 /grafana/login 요청이 http://localhost:3000/login으로 전달된다. Grafana는 /login을 받으면 /grafana/login으로 301을 내리고, nginx가 다시 /login으로 벗겨서 보내는 무한 루프가 생긴다. 슬래시를 빼면 /grafana/login 그대로 http://localhost:3000/grafana/login으로 전달된다.

grafana.ini

sudo nano /etc/grafana/grafana.ini
[server]
protocol            = http
domain              = your-domain.com          # 실제 도메인
root_url            = https://your-domain.com/grafana/
serve_from_sub_path = true

주석 기호(;)를 반드시 제거해야 적용된다.

적용

sudo nginx -t && sudo systemctl reload nginx
sudo systemctl restart grafana-server

브라우저에서 https://your-domain.com/grafana/ 접속 후 초기 계정 admin / admin으로 로그인.


7단계 — Prometheus 데이터소스 연결

Grafana UI에서 Connections → Data Sources → Add → Prometheus 선택 후:

항목
URLhttp://localhost:9090
AccessServer (default)

8단계 — 대시보드 주요 PromQL

# 서버 생존 여부
nginx_rtmp_up

# 활성 스트림 수
nginx_rtmp_stream_count{app="live"}

# 전체 시청자 합계
sum(nginx_rtmp_clients)

# 입력 비트레이트 (Mbps)
sum(nginx_rtmp_bw_in_bps) / 1000000

# ffmpeg 프로세스 수
nginx_rtmp_ffmpeg_processes

# CPU 사용률 (%)
100 - (avg by(instance)(rate(node_cpu_seconds_total{mode="idle"}[1m])) * 100)

# 메모리 사용률 (%)
(1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) * 100

# 네트워크 수신 (Mbps)
rate(node_network_receive_bytes_total{device!="lo"}[1m]) * 8 / 1000000

포트 정리 및 보안 그룹 권장 설정

포트용도외부 오픈
9090Prometheus❌ 내부 전용
9100node_exporter❌ 내부 전용
9101rtmp exporter❌ 내부 전용
3000Grafana✅ nginx 프록시 경유
80 / 443nginx

Prometheus와 각 exporter는 외부에 노출할 이유가 없다. NHN Cloud 등 클라우드 보안 그룹에서 9090/9100/9101은 차단하고, Grafana는 nginx 리버스 프록시를 통해서만 접근하도록 구성하는 것을 권장한다.


트러블슈팅 요약

exporter가 9101에서 응답하지 않음

sudo journalctl -u nginx_rtmp_exporter -n 30

ModuleNotFoundError: No module named 'lxml'sudo apt install -y python3-lxml python3-requests python3-prometheus-client

Prometheus 타겟이 1개뿐

설정 파일 수정 후 reload를 빠뜨린 경우.

curl -X POST http://localhost:9090/-/reload

Grafana 접속 시 500 에러

nginx 에러 로그와 curl -sIL 리다이렉트 체인을 먼저 확인한다.

sudo tail -50 /var/log/nginx/error.log
curl -sIL --max-redirs 5 https://your-domain.com/grafana/

301 무한 루프

  • proxy_pass 끝 슬래시 제거 (http://localhost:3000 ← 슬래시 없음)
  • grafana.inidomain, root_url, serve_from_sub_path 주석 해제 및 올바른 값 입력
  • proxy_set_header X-Forwarded-Proto https 하드코딩 (변수 대신)
profile
智(지)! 德(덕)! 體(체)!

0개의 댓글