Zabbix 인프라 모니터링 구축 – 06. 노드 수집: CPU·메모리·네트워크 전 지표 수집 (Node Collector)

2026.09.28

·

5편에서 클러스터 전체 상태를 수집했다면, 이번 편에서는 각 노드의 상세 리소스를 수집합니다. 클러스터 API가 노드 수와 온라인 여부만 알려준다면, 노드 API는 실제 리소스 사용량을 알려줍니다.


수집 대상 API 경로

수집 항목API 경로
노드 목록 (온라인 여부 포함)/api2/json/cluster/resources?type=node
노드 상세 리소스/api2/json/nodes/{node}/status
네트워크 트래픽/api2/json/nodes/{node}/netstat

노드 목록은 클러스터 API로 조회하고, 상세 리소스와 netstat은 해당 노드에 직접 요청해야 합니다. 4편에서 설명한 NODE_IPS 매핑이 여기서 사용됩니다.


노드 LLD 구현

노드가 추가되거나 이름이 바뀔 때마다 Zabbix 아이템을 수동으로 만드는 것은 번거롭습니다. LLD로 노드를 자동 발견하도록 구성합니다.

Sponsored

def do_node_discovery():
    data = api("/api2/json/cluster/resources?type=node")
    lld = [
        {"{#NODE}": n["node"], "{#IP}": NODE_IPS.get(n["node"], "")}
        for n in data
    ]
    subprocess.run(
        ["docker", "exec", "zabbix-server",
         "zabbix_sender", "-z", "127.0.0.1", "-p", "10051",
         "-s", ZABBIX_HOST,
         "-k", "proxmox.node.discovery",
         "-o", json.dumps(lld)],
        capture_output=True, timeout=30
    )

Discovery JSON 예시:

[
  {"{#NODE}": "pve11", "{#IP}": "10.0.92.11"},
  {"{#NODE}": "pve12", "{#IP}": "10.0.92.12"},
  {"{#NODE}": "pve13", "{#IP}": "10.0.92.13"}
]

Zabbix의 Discovery Rule에 이 키(proxmox.node.discovery)를 등록하고, Item Prototype에 {#NODE} 매크로를 사용하면 노드별 아이템이 자동 생성됩니다.


노드 리소스 수집

노드 목록을 먼저 조회한 뒤, 온라인 노드에 한해서만 상세 API를 호출합니다:

def do_collect_nodes():
    lines = []
    nodes_data = api("/api2/json/cluster/resources?type=node")
    for n in nodes_data:
        node   = n["node"]
        online = 1 if n.get("status") == "online" else 0
        lines.append(sl(ZABBIX_HOST, f"proxmox.node.status[{node}]", online))
        lines.append(sl(ZABBIX_HOST, f"proxmox.node.hastate[{node}]",
                        1 if n.get("hastate") == "online" else 0))
        if not online:
            continue  # 오프라인 노드는 상세 수집 생략
        ip     = NODE_IPS.get(node)
        detail = api(f"/api2/json/nodes/{node}/status", node_ip=ip)
        if not detail:
            continue
        mem     = detail.get("memory", {})
        rootfs  = detail.get("rootfs", {})
        swap    = detail.get("swap", {})
        loadavg = detail.get("loadavg", ["0", "0", "0"])
        lines += [
            sl(ZABBIX_HOST, f"proxmox.node.cpu[{node}]",
               round(n.get("cpu", 0) * 100, 2)),
            sl(ZABBIX_HOST, f"proxmox.node.mem.used[{node}]",   mem.get("used", 0)),
            sl(ZABBIX_HOST, f"proxmox.node.mem.total[{node}]",  mem.get("total", 0)),
            sl(ZABBIX_HOST, f"proxmox.node.rootfs.used[{node}]",  rootfs.get("used", 0)),
            sl(ZABBIX_HOST, f"proxmox.node.rootfs.total[{node}]", rootfs.get("total", 0)),
            sl(ZABBIX_HOST, f"proxmox.node.uptime[{node}]",     detail.get("uptime", 0)),
            sl(ZABBIX_HOST, f"proxmox.node.load1[{node}]",      loadavg[0]),
            sl(ZABBIX_HOST, f"proxmox.node.load5[{node}]",      loadavg[1]),
            sl(ZABBIX_HOST, f"proxmox.node.load15[{node}]",     loadavg[2]),
            sl(ZABBIX_HOST, f"proxmox.node.swap.used[{node}]",  swap.get("used", 0)),
            sl(ZABBIX_HOST, f"proxmox.node.swap.total[{node}]", swap.get("total", 0)),
        ]

CPU 값 변환: 클러스터 리소스 API의 cpu 값은 0.0~1.0 범위의 소수입니다. * 100으로 퍼센트로 변환합니다.

메모리/스토리지 단위: used, total 값은 바이트(byte) 단위입니다. Zabbix에 그대로 저장하고, 아이템의 Units를 B로 설정하면 웹 UI에서 자동으로 GB/MB로 변환해 표시합니다.


네트워크 트래픽 수집과 타입 오류

netstat API는 노드의 모든 인터페이스 트래픽을 반환합니다. 전체 합산 값을 수집합니다:

netstats = api(f"/api2/json/nodes/{node}/netstat", node_ip=ip)
if netstats:
    total_in = total_out = 0
    for iface in netstats:
        total_in  += int(iface.get("in", 0) or 0)
        total_out += int(iface.get("out", 0) or 0)
    lines += [
        sl(ZABBIX_HOST, f"proxmox.node.netin[{node}]",  total_in),
        sl(ZABBIX_HOST, f"proxmox.node.netout[{node}]", total_out),
    ]

여기서 중요한 함정이 있습니다.

Proxmox netstat API의 in과 out 값은 문자열(string)로 반환됩니다. 숫자처럼 보이지만 타입이 str입니다. 그냥 더하면 문자열 연결이 발생합니다:

# 잘못된 코드 — 문자열 연결 발생
total_in += iface.get("in", 0)  # "1234" + "5678" = "12345678"
# 올바른 코드
total_in += int(iface.get("in", 0) or 0)

or 0 처리가 추가된 이유는 값이 None이거나 빈 문자열일 경우 int() 변환 오류가 발생할 수 있어서입니다.


대용량 데이터 전송 방식

노드 수가 많아지면 zabbix_sender에 stdin으로 데이터를 전달하는 방식이 한계에 부딪힐 수 있습니다. Node Collector에서는 임시 파일을 경유하는 방식을 사용합니다:

def send(lines):
    if not lines:
        return
    tmp = "/tmp/zabbix_node_prod.txt"
    with open(tmp, "w") as f:
        f.write("\n".join(lines) + "\n")
    # 호스트의 파일을 컨테이너로 복사
    subprocess.run(
        ["docker", "cp", tmp, "zabbix-server:/tmp/zabbix_node_prod.txt"],
        capture_output=True, timeout=10
    )
    # 컨테이너 내부에서 파일로 전송
    proc = subprocess.run(
        ["docker", "exec", "zabbix-server",
         "zabbix_sender", "-z", "127.0.0.1", "-p", "10051",
         "-i", "/tmp/zabbix_node_prod.txt"],
        capture_output=True, text=True, timeout=30
    )
    sys.stderr.write(f"[INFO] sender: {proc.stdout.strip()}\n")

docker cp로 호스트의 파일을 컨테이너 내부로 복사한 뒤, zabbix_sender -i 옵션으로 파일을 직접 읽게 합니다. stdin 파이프보다 안정적으로 대량 데이터를 처리할 수 있습니다.


수집 아이템 목록

Item Prototype으로 등록할 키 목록입니다. {#NODE}는 LLD 매크로입니다:

키설명단위
proxmox.node.status[{#NODE}]온라인 여부 (1/0)–
proxmox.node.hastate[{#NODE}]HA 상태 (1/0)–
proxmox.node.cpu[{#NODE}]CPU 사용률%
proxmox.node.mem.used[{#NODE}]메모리 사용량B
proxmox.node.mem.total[{#NODE}]메모리 전체B
proxmox.node.rootfs.used[{#NODE}]루트 디스크 사용량B
proxmox.node.rootfs.total[{#NODE}]루트 디스크 전체B
proxmox.node.uptime[{#NODE}]업타임s
proxmox.node.load1[{#NODE}]Load Average 1분–
proxmox.node.load5[{#NODE}]Load Average 5분–
proxmox.node.load15[{#NODE}]Load Average 15분–
proxmox.node.swap.used[{#NODE}]Swap 사용량B
proxmox.node.swap.total[{#NODE}]Swap 전체B
proxmox.node.netin[{#NODE}]네트워크 수신 누적B
proxmox.node.netout[{#NODE}]네트워크 송신 누적B

네트워크 수신/송신은 누적 카운터입니다. Zabbix 아이템 타입을 Delta (speed per second)로 설정하면 초당 트래픽(bps)으로 자동 변환됩니다.


트리거 설계

# 노드 오프라인 — High
last(/proxmox-cluster-prod/proxmox.node.status[{#NODE}]) = 0
# CPU 과부하 (최근 3회 평균 90% 초과) — Average
avg(/proxmox-cluster-prod/proxmox.node.cpu[{#NODE}],#3) > 90
# 메모리 사용률 95% 초과 — Average
last(/proxmox-cluster-prod/proxmox.node.mem.used[{#NODE}])
  / last(/proxmox-cluster-prod/proxmox.node.mem.total[{#NODE}]) * 100 > 95
# 루트 디스크 사용률 85% 초과 — Warning
last(/proxmox-cluster-prod/proxmox.node.rootfs.used[{#NODE}])
  / last(/proxmox-cluster-prod/proxmox.node.rootfs.total[{#NODE}]) * 100 > 85

crontab 등록

# Proxmox PRD Node Collector (2분)
*/2 * * * * /usr/bin/python3 /data/zabbix/scripts/proxmox_node_collector_prod.py --node-collect >> /var/log/zabbix-collector.log 2>&1
# Proxmox PRD Node Discovery (10분)
*/10 * * * * /usr/bin/python3 /data/zabbix/scripts/proxmox_node_collector_prod.py --node-discovery >> /var/log/zabbix-collector.log 2>&1

Discovery는 자주 실행할 필요가 없습니다. 노드 구성이 바뀌는 경우가 드물기 때문에 10분 주기로 충분합니다.


마치며

이번 편에서 다룬 핵심 포인트입니다:

  • 노드 LLD로 동적 아이템 자동 생성
  • 오프라인 노드 감지 시 상세 수집 생략으로 불필요한 오류 방지
  • netstat in/out 값이 문자열 — int() 변환 필수
  • 대량 데이터는 docker cp + 파일 방식으로 안정적 전송
  • 네트워크 카운터는 Delta 타입으로 초당 속도 변환

다음 편에서는 물리 디스크 SMART 데이터 수집을 다룹니다. SAS와 SATA/NVMe의 응답 형식이 달라 파싱 방법이 다르고, HBA 교체 후 stale value로 인한 오탐 사례도 구체적으로 설명합니다.


MORE POSTS

다른 글 보기

guniq 인사이트 썸네일 — 중소기업 OpenLDAP 통합 인증 구축 – 00. 왜 OpenLDAP인가: 비용과 자유, 그리고 표준의 중요성
테크 랩

중소기업 OpenLDAP 통합 인증 구축 – 00. 왜 OpenLDAP인가: 비용과 자유, 그리고 표준의 중요성

MS Active Directory 대신 OpenLDAP을 선택한 이유, 오픈소스 디렉토리 서버의 장단점, 그리고 연동 유연성을 결정짓는 LDAP 표준 준수의 중요성을 실제 운영 경험 기반으로 정리합니다.
2026.10.12
guniq 인사이트 썸네일 — Zabbix 인프라 모니터링 구축 – 15. 실전 이슈: 운영 중 마주친 오탐·누락·설정 오류 총정리 (트러블슈팅)
테크 랩

Zabbix 인프라 모니터링 구축 – 15. 실전 이슈: 운영 중 마주친 오탐·누락·설정 오류 총정리 (트러블슈팅)

Zabbix 모니터링 구축과 운영 중 실제로 마주친 문제들을 총정리합니다. Media Type 기본 비활성화, 트리거 표현식 포맷 변경, stale value 오탐, crontab 초기화, netstat 타입 오류 등 삽질의 기록과 해결책을 공유합니다.
2026.10.09
guniq 인사이트 썸네일 — Zabbix 인프라 모니터링 구축 – 14. 환경 분리: DEV·PRD를 하나의 Zabbix로 운영하기 (호스트그룹·권한)
테크 랩

Zabbix 인프라 모니터링 구축 – 14. 환경 분리: DEV·PRD를 하나의 Zabbix로 운영하기 (호스트그룹·권한)

개발과 운영 환경을 하나의 Zabbix 서버로 운영하면서 호스트 그룹, 사용자 권한, Slack 알림 채널을 분리하는 방법을 다룹니다. API 토큰, 스크립트, 로그까지 환경별 분리 전략 전체를 설명합니다.
2026.10.08

프로젝트 문의 환영합니다

기획부터 개발, 운영까지 함께 만들어 드립니다.

무료 3분 자가진단

우리 회사, 자체 클라우드가 답일까?

AWS vs 자체 인프라 · 11개 항목 3분 체크