26S02m

QK·5일 전

대규모 클러스터(128대 이상) 환경에서 kubectl describe를 쓰면 속도가 매우 느리므로, kubectl의 JSON 출력을 파싱하거나 Kubernetes API를 직접 호출해 노드 Allocatable 대비 Pod CPU Requests 합산치, 할당률(%), 잔여 Core를 산출하는 Python 스크립트와 경량 Bash 스크립트입니다.


Python 기반 노드별 잔여 CPU Core 산출 스크립트 (calc_node_cpu.py)

별도의 무거운 패키지 없이 로컬 kubectl 권한(~/.kube/config)을 활용하여 실행 가능하며, 단위(m, 정수) 변환 및 종료된 Pod(Succeeded/Failed) 자동 제외 로직이 포함되어 있습니다.

#!/usr/bin/env python3
import json
import subprocess
import sys

def parse_cpu_to_millicores(val: str) -> int:
    """CPU 문자열('64', '500m' 등)을 millicores 정수로 변환"""
    if not val:
        return 0
    val = str(val).strip()
    if val.endswith('m'):
        return int(val[:-1])
    return int(float(val) * 1000)

def main():
    # 1. Node 정보 수집 (Allocatable CPU)
    print("[*] Fetching node specifications...", file=sys.stderr)
    try:
        nodes_raw = subprocess.check_output(
            ["kubectl", "get", "nodes", "-o", "json"], stderr=subprocess.PIPE
        )
        nodes_data = json.loads(nodes_raw)
    except subprocess.CalledProcessError as e:
        print(f"Error fetching nodes: {e.stderr.decode()}", file=sys.stderr)
        sys.exit(1)

    node_stats = {}
    for item in nodes_data.get("items", []):
        node_name = item["metadata"]["name"]
        allocatable_cpu = item["status"]["allocatable"].get("cpu", "0")
        alloc_m = parse_cpu_to_millicores(allocatable_cpu)
        node_stats[node_name] = {
            "allocatable_m": alloc_m,
            "requested_m": 0,
        }

    # 2. 모든 Namespace의 Pod 정보 수집
    print("[*] Fetching active pods and CPU requests...", file=sys.stderr)
    try:
        pods_raw = subprocess.check_output(
            ["kubectl", "get", "pods", "-A", "-o", "json"], stderr=subprocess.PIPE
        )
        pods_data = json.loads(pods_raw)
    except subprocess.CalledProcessError as e:
        print(f"Error fetching pods: {e.stderr.decode()}", file=sys.stderr)
        sys.exit(1)

    for item in pods_data.get("items", []):
        status = item.get("status", {})
        phase = status.get("phase", "")
        # 종료되었거나 실패한 Pod 제외
        if phase in ["Succeeded", "Failed"]:
            continue

        spec = item.get("spec", {})
        node_name = spec.get("nodeName")
        if not node_name or node_name not in node_stats:
            continue

        # App 컨테이너 requests 합산
        pod_req_m = 0
        for container in spec.get("containers", []):
            resources = container.get("resources", {})
            requests = resources.get("requests", {})
            cpu_req = requests.get("cpu", "0")
            pod_req_m += parse_cpu_to_millicores(cpu_req)

        # Init 컨테이너 고려 (Kubernetes 표준: max(initContainers, sum(appContainers)))
        init_req_m = 0
        for init_c in spec.get("initContainers", []):
            req = init_c.get("resources", {}).get("requests", {}).get("cpu", "0")
            init_req_m = max(init_req_m, parse_cpu_to_millicores(req))

        effective_req_m = max(pod_req_m, init_req_m)
        node_stats[node_name]["requested_m"] += effective_req_m

    # 3. 산출 결과 계산 및 출력 (남은 Core 적은 순 정렬)
    results = []
    for node, data in node_stats.items():
        alloc_m = data["allocatable_m"]
        req_m = data["requested_m"]
        remain_m = alloc_m - req_m
        usage_ratio = (req_m / alloc_m * 100.0) if alloc_m > 0 else 0.0

        results.append({
            "node": node,
            "alloc_cores": alloc_m / 1000.0,
            "req_cores": req_m / 1000.0,
            "ratio_pct": usage_ratio,
            "remain_cores": remain_m / 1000.0
        })

    # 잔여 CPU가 가장 부족한 노드부터 오름차순 정렬
    results.sort(key=lambda x: x["remain_cores"])

    print(f"\n{'NODE NAME':<40} {'ALLOC(Core)':>12} {'REQUEST(Core)':>14} {'USAGE(%)':>10} {'REMAIN(Core)':>14}")
    print("-" * 94)
    for r in results:
        print(f"{r['node']:<40} {r['alloc_cores']:>12.2f} {r['req_cores']:>14.2f} {r['ratio_pct']:>9.1f}% {r['remain_cores']:>14.2f}")

if __name__ == "__main__":
    main()

Bash + jq 경량 1-Liner 스크립트

파이썬 없이 터미널에서 즉시 노드별 남은 Core와 할당률을 확인해야 할 때 유용합니다.

kubectl get nodes -o json | jq -r '
  .items[] | 
  .metadata.name as $node | 
  (.status.allocatable.cpu | if endswith("m") then (rtrimstr("m") | tonumber) else (tonumber * 1000) end) as $alloc |
  "\($node) \($alloc)"
' | while read node alloc_m; do
  req_m=$(kubectl get pods -A --field-selector spec.nodeName=$node -o json | jq '
    [ .items[] | select(.status.phase != "Succeeded" and .status.phase != "Failed") | 
      .spec.containers[].resources.requests.cpu // "0" | 
      if endswith("m") then (rtrimstr("m") | tonumber) else (tonumber * 1000) end 
    ] | add // 0
  ')
  
  remain_cores=$(awk "BEGIN {printf \"%.2f\", ($alloc_m - $req_m) / 1000}")
  ratio=$(awk "BEGIN {printf \"%.1f\", ($req_m / $alloc_m) * 100}")
  alloc_cores=$(awk "BEGIN {printf \"%.2f\", $alloc_m / 1000}")
  req_cores=$(awk "BEGIN {printf \"%.2f\", $req_m / 1000}")
  
  printf "%-40s | Alloc: %6s Core | Req: %6s Core (%5s%%) | Remain: %6s Core\n" "$node" "$alloc_cores" "$req_cores" "$ratio" "$remain_cores"
done

운영 팁

  • Allocatable 기준 적용: capacity가 아닌 allocatable을 기준으로 삼아 kubelet, OS 예약 영역(kube-reserved, system-reserved)을 제외한 실제 워크로드 스케줄링 가능 여유 Core만 계산합니다.
  • Init Container 스펙 계산: Python 스크립트는 쿠버네티스 기본 스케줄러 알고리즘대로 max(sum(appContainers), max(initContainers)) 규칙을 반영하여 오차를 없앴습니다.
profile
engineer

0개의 댓글