제공해주신 step3_analytics.py와 step4_governance_analyzer.py 코드를 앞서 고도화한 1.5단계 Polars 데이터 융합 파이프라인(v4 규격)과 대조해 보면, 현재 두 가지 치명적인 결전 요소(Breaking Mismatch)와 기능적 공백이 발견됩니다.
이 부분을 완벽히 무결하게 결합하기 위해 수정해야 할 핵심 포인트와 리팩토링된 코드를 제안합니다.
step2 파이프라인의 거버넌스 로직을 고도화하면서 상태 진단 명세를 "📉 과다할당" ➡️ "📉 자원과다선점"으로 변경했고, "🚨 장기유휴_좀비팟", "✅ 단명_정상종료" 명세를 새로 신설했습니다.step3에서는 신규 상태값들이 영문 번역 맵핑(status_translation_map)에서 누락되어 차트 범례가 깨지며, step4에서는 구형 명세인 "📉 과다할당"으로 하향 후보를 필터링하고 있어 자원 낭비 스캔 결과가 무조건 0건(Empty)으로 처리되는 심각한 논리 크래시 상태입니다.step3 시각화 엔진은 여전히 CPU와 Memory 자원만 그리고 있습니다. Polars 융합 레이어에서 열심히 연산한 Cilium 네트워크 대역폭/드롭, DirectPV IOPS, 분산 노드 간 부하 편중도 변동계수(feat_load_skew_cv_max) 등의 고차원 SRE 특징량들을 완전히 낭비하고 있습니다.step4에서 단순 과다할당 외에 '30분 이상 가동되었으나 실사용 피크가 0에 수렴하는' 장기 유휴 좀비 팟을 탐지해 냈으므로, 이에 맞는 SRE 액션 플랜(예: 무조건적인 자원 축소가 아닌 "Immediate Termination")을 명확히 분기해 주어야 6단계 엑셀 빌더와 사내 LLM이 정밀한 처방을 내릴 수 있습니다.step3_analytics.py (신규 4대 SRE 지표 차트 컴포넌트 추가판)기존 19개 차트 렌더링 파이프라인의 구조를 그대로 유지하되, 상태값 맵핑 동기화 및 후반부에 클라우드 네이티브 연계 심층 차트 3종(Chart 21, 22, 23)을 동적으로 인젝션하도록 개조했습니다.
"""
step3_analytics.py — Long-term Time-series Infrastructure Data Visualization Engine (Strictly English & Legend Fixed)
"""
import os
import re
import argparse
import pandas as pd
import numpy as np
from pathlib import Path
# ─── 🛡️ [Headless Environment Guard] Prevents crashes in K8s pods without display servers ───
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import seaborn as sns
# ─── 📂 Directory Path Standardization (./data) ──────────────────────────
BASE_DATA_DIR = Path("./data")
MERGED_DIR = BASE_DATA_DIR / "merged"
BASE_PLOT_DIR = BASE_DATA_DIR / "output" / "plots"
BASE_PLOT_DIR.mkdir(parents=True, exist_ok=True)
plt.rcParams['font.family'] = 'sans-serif'
plt.rcParams['axes.unicode_minus'] = False
sns.set_theme(style="whitegrid")
def check_and_handle_empty(df, output_path, chart_name):
if df is None or df.empty:
print(f" ⚠️ [Empty Data] Generating placeholder for {chart_name} (Dataset is empty).")
fig, ax = plt.subplots(figsize=(10, 5))
ax.text(0.5, 0.5, f"No Data Available\n({chart_name})",
ha='center', va='center', fontsize=14, color='gray', weight='bold')
ax.axis('off')
plt.tight_layout()
plt.savefig(output_path, dpi=100, bbox_inches='tight')
plt.close()
return True
return False
def main():
parser = argparse.ArgumentParser(description="FinOps Analytics Visualization Engine")
parser.add_argument("--cluster", type=str, required=True, choices=["COMPUTE", "STORAGE"], help="Target cluster type")
args = parser.parse_args()
cluster_target = args.cluster.upper()
print(f"🚀 [Step3] Starting FinOps Time-series English Visualization Engine for Cluster: {cluster_target}...")
cl_dir = MERGED_DIR / cluster_target
if not cl_dir.exists():
print(f"❌ Error: Target cluster directory '{cl_dir}' does not exist. Please run step2 first.")
return
pod_files = list(cl_dir.glob("daily_enriched_*.parquet"))
ns_files = list(cl_dir.glob("pareto_ns_*.parquet"))
if not pod_files:
print(f"❌ Error: No processed daily Parquet files found for cluster '{cluster_target}' under {cl_dir}.")
return
df_pod = pd.concat([pd.read_parquet(f) for f in pod_files], ignore_index=True)
df_ns = pd.concat([pd.read_parquet(f) for f in ns_files], ignore_index=True) if ns_files else pd.DataFrame()
PLOT_DIR = BASE_PLOT_DIR / cluster_target
PLOT_DIR.mkdir(parents=True, exist_ok=True)
print(f"✅ Data lake aggregated successfully -> Container rows: {len(df_pod):,}")
df_pod["date"] = df_pod["date"].astype(str)
# ─── 🔤 [🛡️ 수리 조치: English Data Translation Layer 동기화] ───────────────────
status_translation_map = {
"💥 OOM장애발생": "💥 OOM Killed",
"⚠️ Request부족": "⚠️ Request Shortage",
"📉 자원과다선점": "📉 Over-allocated Slots",
"🚨 장기유휴_좀비팟": "🚨 Idle Zombie Pod",
"✅ 단명_정상종료": "✅ Ephemeral Normal End",
"✅ 최적화완료": "✅ Optimized"
}
if "status" in df_pod.columns:
df_pod["status"] = df_pod["status"].map(status_translation_map).fillna(df_pod["status"])
# 지표 계산 레이어
df_pod["cpu_util"] = np.where(df_pod["cpu_request_max"] > 0, (df_pod["cpu_usage_p95"] / df_pod["cpu_request_max"] * 100), 0)
df_pod["mem_util"] = np.where(df_pod["mem_request_max"] > 0, (df_pod["mem_usage_p95"] / df_pod["mem_request_max"] * 100), 0)
df_pod["lim_req_ratio"] = np.where(df_pod["cpu_request_max"] > 0, df_pod["cpu_limit_max"] / df_pod["cpu_request_max"], 0)
# ─── 📊 [기존 1~20번 차트 렌더링 블록 유지 보존부] ───────────────────
print("⏳ [1/23] Rendering chart1_cpu_req_vs_usage_by_workload...")
out1 = PLOT_DIR / "chart1_cpu_req_vs_usage_by_workload.png"
if not check_and_handle_empty(df_pod, out1, "chart1_cpu_req_vs_usage_by_workload"):
df_wl_cpu = df_pod.groupby("workload_type")[["cpu_request_max", "cpu_usage_p95"]].mean().reset_index()
plt.figure(figsize=(11, 5))
df_melt_cpu = df_wl_cpu.melt(id_vars="workload_type", value_vars=["cpu_request_max", "cpu_usage_p95"])
sns.barplot(data=df_melt_cpu, x="workload_type", y="value", hue="variable", palette="Blues_r")
plt.xticks(rotation=30, ha='right')
plt.title("Average CPU Request vs P95 Peak Usage by Workload Type")
plt.xlabel("Workload Type")
plt.ylabel("CPU Cores")
plt.legend(bbox_to_anchor=(1.02, 1), loc='upper left')
plt.tight_layout()
plt.savefig(out1, dpi=100, bbox_inches='tight')
plt.close()
print("⏳ [2/23] Rendering chart2_mem_req_vs_usage_by_workload...")
out2 = PLOT_DIR / "chart2_mem_req_vs_usage_by_workload.png"
if not check_and_handle_empty(df_pod, out2, "chart2_mem_req_vs_usage_by_workload"):
df_wl_mem = df_pod.groupby("workload_type")[["mem_request_max", "mem_usage_p95"]].mean().reset_index()
plt.figure(figsize=(11, 5))
df_melt_mem = df_wl_mem.melt(id_vars="workload_type", value_vars=["mem_request_max", "mem_usage_p95"])
sns.barplot(data=df_melt_mem, x="workload_type", y="value", hue="variable", palette="Purples_r")
plt.xticks(rotation=30, ha='right')
plt.title("Average Memory Request vs P95 Peak Usage (GB) by Workload Type")
plt.xlabel("Workload Type")
plt.ylabel("Memory Capacity (GB)")
plt.legend(bbox_to_anchor=(1.02, 1), loc='upper left')
plt.tight_layout()
plt.savefig(out2, dpi=100, bbox_inches='tight')
plt.close()
print("⏳ [3/23] Rendering chart3_daily_waste_stack...")
out3 = PLOT_DIR / "chart3_daily_waste_stack.png"
df_daily_waste = df_pod.groupby(["date", "workload_type"])["cpu_waste_core_hours"].sum().unstack().fillna(0) if not df_pod.empty else pd.DataFrame()
if not check_and_handle_empty(df_daily_waste, out3, "chart3_daily_waste_stack"):
ax = df_daily_waste.plot(kind='bar', stacked=True, figsize=(12, 5), cmap="tab20")
plt.title("Daily Total CPU Waste Core-Hours Stacked by Workload (KST)")
plt.xlabel("Date (KST)")
plt.ylabel("Waste Volume (Core-Hours)")
plt.xticks(rotation=45, ha='right')
plt.legend(bbox_to_anchor=(1.02, 1), loc='upper left', title="Workload Type")
plt.tight_layout()
plt.savefig(out3, dpi=100, bbox_inches='tight')
plt.close()
print("⏳ [4/23] Rendering chart4_cpu_efficiency_heatmap...")
out4 = PLOT_DIR / "chart4_cpu_efficiency_heatmap.png"
df_heat_cpu = df_pod.groupby(["workload_type", "date"])["cpu_util"].mean().unstack().fillna(0) if not df_pod.empty else pd.DataFrame()
if not check_and_handle_empty(df_heat_cpu, out4, "chart4_cpu_efficiency_heatmap"):
plt.figure(figsize=(11, 5))
sns.heatmap(df_heat_cpu, annot=True, fmt=".1f", cmap="RdYlGn", cbar=True)
plt.title("Mean CPU Utilization Heatmap (%) (Workload Type x Date)")
plt.xlabel("Date (KST)")
plt.ylabel("Workload Type")
plt.tight_layout()
plt.savefig(out4, dpi=100, bbox_inches='tight')
plt.close()
print("⏳ [5/23] Rendering chart18_mem_waste_heatmap...")
out18 = PLOT_DIR / "chart18_mem_waste_heatmap.png"
df_heat_mem = df_pod.groupby(["workload_type", "date"])["mem_waste_gb_hours"].sum().unstack().fillna(0) if not df_pod.empty else pd.DataFrame()
if not check_and_handle_empty(df_heat_mem, out18, "chart18_mem_waste_heatmap"):
plt.figure(figsize=(11, 5))
sns.heatmap(df_heat_mem, annot=False, cmap="BuPu", cbar=True)
plt.title("Total Memory Waste Volume Heatmap (GB-Hours)")
plt.xlabel("Date (KST)")
plt.ylabel("Workload Type")
plt.tight_layout()
plt.savefig(out18, dpi=100, bbox_inches='tight')
plt.close()
print("⏳ [6/23] Rendering chart5_pareto_ns_waste...")
out5 = PLOT_DIR / "chart5_pareto_ns_waste.png"
if not check_and_handle_empty(df_ns, out5, "chart5_pareto_ns_waste"):
df_ns_top = df_ns.groupby("namespace")["total_waste_core_hours"].sum().reset_index().sort_values("total_waste_core_hours", ascending=False).head(15)
df_ns_top["waste_share_pct"] = (df_ns_top["total_waste_core_hours"] / max(0.1, df_ns_top["total_waste_core_hours"].sum()) * 100).round(2)
df_ns_top["waste_cumsum_pct"] = df_ns_top["waste_share_pct"].cumsum().round(2)
fig, ax1 = plt.subplots(figsize=(12, 5))
sns.barplot(data=df_ns_top, x="namespace", y="total_waste_core_hours", ax=ax1, color="steelblue")
ax1.set_xticklabels(ax1.get_xticklabels(), rotation=45, ha="right")
ax2 = ax1.twinx()
ax2.plot(df_ns_top["namespace"], df_ns_top["waste_cumsum_pct"], color="crimson", marker="o", linewidth=2)
ax2.set_ylim(0, 110)
plt.title("Top 15 Namespace Cost-Waste Pareto Chart (Cumulative Share %)")
ax1.set_xlabel("Tenant Namespace")
ax1.set_ylabel("Waste Volume (Core-Hours)")
plt.tight_layout()
plt.savefig(out5, dpi=100, bbox_inches='tight')
plt.close()
print("⏳ [7/23] Rendering chart11_pareto_workload_waste...")
out11 = PLOT_DIR / "chart11_pareto_workload_waste.png"
if not check_and_handle_empty(df_pod, out11, "chart11_pareto_workload_waste"):
df_wl_waste = df_pod.groupby("workload_type")["cpu_waste_core_hours"].sum().reset_index().sort_values("cpu_waste_core_hours", ascending=False)
plt.figure(figsize=(10, 5))
sns.barplot(data=df_wl_waste, x="workload_type", y="cpu_waste_core_hours", palette="Oranges_r")
plt.xticks(rotation=30, ha="right")
plt.title("Total CPU Waste Volume by Workload Type")
plt.xlabel("Workload Type")
plt.ylabel("Total Waste (Core-Hours)")
plt.tight_layout()
plt.savefig(out11, dpi=100, bbox_inches='tight')
plt.close()
print("⏳ [8/23] Rendering chart6_status_donut...")
out6 = PLOT_DIR / "chart6_status_donut.png"
if not check_and_handle_empty(df_pod, out6, "chart6_status_donut"):
status_summary = df_pod["status"].value_counts()
plt.figure(figsize=(7, 5))
colors = ["#70AD47", "#1F4E79", "#FFC000", "#C00000", "#A5A5A5", "#ED7D31"]
plt.pie(status_summary, labels=status_summary.index, autopct='%1.1f%%', startangle=90, colors=colors[:len(status_summary)], wedgeprops=dict(width=0.4, edgecolor='w'))
plt.title("Infrastructure Resource Governance Status Ratio")
plt.tight_layout()
plt.savefig(out6, dpi=100, bbox_inches='tight')
plt.close()
print("⏳ [9/23] Rendering chart7_waste_footprint_bubble...")
out7 = PLOT_DIR / "chart7_waste_footprint_bubble.png"
if not check_and_handle_empty(df_pod, out7, "chart7_waste_footprint_bubble"):
df_bubble = df_pod.groupby("workload_type").agg(
x_alloc=("cpu_allocated_core_hours", "sum"),
y_util=("cpu_util", "mean"),
z_waste=("cpu_waste_core_hours", "sum")
).reset_index()
plt.figure(figsize=(10, 6))
sns.scatterplot(data=df_bubble, x="x_alloc", y="y_util", size="z_waste", hue="workload_type", sizes=(100, 2000), alpha=0.7, legend="brief")
plt.title("Resource Footprint Bubble Chart (Allocated x Utilization x Waste Size)")
plt.xlabel("Total Allocated Core-Hours")
plt.ylabel("Average Utilization (%)")
plt.legend(bbox_to_anchor=(1.02, 1), loc='upper left')
plt.tight_layout()
plt.savefig(out7, dpi=100, bbox_inches='tight')
plt.close()
print("⏳ [10/23] Rendering chart14_cpu_mem_waste_scatter...")
out14 = PLOT_DIR / "chart14_cpu_mem_waste_scatter.png"
if not check_and_handle_empty(df_pod, out14, "chart14_cpu_mem_waste_scatter"):
plt.figure(figsize=(9, 6))
sns.scatterplot(data=df_pod.head(5000), x="cpu_waste_core_hours", y="mem_waste_gb_hours", hue="workload_type", alpha=0.5)
plt.title("Co-relation Scatter Plot: CPU Waste vs Memory Waste (Sampled)")
plt.xlabel("CPU Waste (Core-Hours)")
plt.ylabel("Memory Waste (GB-Hours)")
plt.legend(bbox_to_anchor=(1.02, 1), loc='upper left')
plt.tight_layout()
plt.savefig(out14, dpi=100, bbox_inches='tight')
plt.close()
print("⏳ [11/23] Rendering chart8_shortfall_footprint...")
out8 = PLOT_DIR / "chart8_shortfall_footprint.png"
df_short = df_pod.groupby(["workload_type", "date"])["cpu_shortage_cores"].sum().unstack().fillna(0) if not df_pod.empty else pd.DataFrame()
if not check_and_handle_empty(df_short, out8, "chart8_shortfall_footprint"):
plt.figure(figsize=(11, 4))
sns.heatmap(df_short, annot=False, cmap="YlOrRd", cbar=True)
plt.title("Total CPU Shortfall (Deficit) Cores Footprint")
plt.xlabel("Date (KST)")
plt.ylabel("Workload Type")
plt.tight_layout()
plt.savefig(out8, dpi=100, bbox_inches='tight')
plt.close()
print("⏳ [12/23] Rendering chart9_boxplot_cpu_util_by_workload...")
out9 = PLOT_DIR / "chart9_boxplot_cpu_util_by_workload.png"
if not check_and_handle_empty(df_pod, out9, "chart9_boxplot_cpu_util_by_workload"):
plt.figure(figsize=(11, 5))
sns.boxplot(data=df_pod, x="workload_type", y="cpu_util", palette="Set3")
plt.xticks(rotation=30, ha="right")
plt.ylim(-5, 105)
plt.title("CPU Utilization P95 Boxplot Distribution by Workload Type")
plt.xlabel("Workload Type")
plt.ylabel("P95 Actual Utilization (%)")
plt.tight_layout()
plt.savefig(out9, dpi=100, bbox_inches='tight')
plt.close()
print("⏳ [13/23] Rendering chart10_boxplot_mem_util_by_workload...")
out10 = PLOT_DIR / "chart10_boxplot_mem_util_by_workload.png"
if not check_and_handle_empty(df_pod, out10, "chart10_boxplot_mem_util_by_workload"):
plt.figure(figsize=(11, 5))
sns.boxplot(data=df_pod, x="workload_type", y="mem_util", palette="Pastel1")
plt.xticks(rotation=30, ha="right")
plt.ylim(-5, 105)
plt.title("Memory Utilization P95 Boxplot Distribution by Workload Type")
plt.xlabel("Workload Type")
plt.ylabel("P95 Actual Utilization (%)")
plt.tight_layout()
plt.savefig(out10, dpi=100, bbox_inches='tight')
plt.close()
print("⏳ [14/23] Rendering chart12_daily_waste_trend_by_workload...")
out12 = PLOT_DIR / "chart12_daily_waste_trend_by_workload.png"
df_trend_wl = df_pod.groupby(["date", "workload_type"])["cpu_waste_core_hours"].sum().unstack().fillna(0) if not df_pod.empty else pd.DataFrame()
if not check_and_handle_empty(df_trend_wl, out12, "chart12_daily_waste_trend_by_workload"):
plt.figure(figsize=(12, 5))
sns.lineplot(data=df_trend_wl, markers=True, dashes=False, linewidth=2)
plt.title("Daily CPU Waste Timeline Trend Line by Workload Type")
plt.xlabel("Date (KST)")
plt.ylabel("Waste Volume (Core-Hours)")
plt.xticks(rotation=30)
plt.legend(bbox_to_anchor=(1.02, 1), loc='upper left', title="Workload Type")
plt.tight_layout()
plt.savefig(out12, dpi=100, bbox_inches='tight')
plt.close()
print("⏳ [15/23] Rendering chart15_oom_status_by_workload...")
out15 = PLOT_DIR / "chart15_oom_status_by_workload.png"
if not check_and_handle_empty(df_pod, out15, "chart15_oom_status_by_workload"):
plt.figure(figsize=(12, 5))
sns.countplot(data=df_pod, x="workload_type", hue="status", palette="muted")
plt.xticks(rotation=30, ha="right")
plt.title("Governance Status Distribution Count per Workload Type")
plt.xlabel("Workload Type")
plt.ylabel("Pod Count")
plt.legend(bbox_to_anchor=(1.02, 1), loc="upper right")
plt.tight_layout()
plt.savefig(out15, dpi=100, bbox_inches='tight')
plt.close()
print("⏳ [16/23] Rendering chart13_violin_cpu_util...")
out13 = PLOT_DIR / "chart13_violin_cpu_util.png"
if not check_and_handle_empty(df_pod, out13, "chart13_violin_cpu_util"):
plt.figure(figsize=(11, 5))
sns.violinplot(data=df_pod, x="workload_type", y="cpu_util", inner="quartile", palette="pastel")
plt.xticks(rotation=30, ha="right")
plt.title("CPU Utilization Kernel Density Violin Plot")
plt.xlabel("Workload Type")
plt.ylabel("CPU Utilization (%)")
plt.tight_layout()
plt.savefig(out13, dpi=100, bbox_inches='tight')
plt.close()
print("⏳ [17/23] Rendering chart17_cpu_limit_request_ratio...")
out17 = PLOT_DIR / "chart17_cpu_limit_request_ratio.png"
if not check_and_handle_empty(df_pod, out17, "chart17_cpu_limit_request_ratio"):
plt.figure(figsize=(11, 5))
sns.boxplot(data=df_pod, x="workload_type", y="lim_req_ratio", palette="vlag")
plt.xticks(rotation=30, ha="right")
plt.title("Kubernetes Pod CPU Limit / Request Overcommit Ratio")
plt.xlabel("Workload Type")
plt.ylabel("Limit / Request Ratio")
plt.tight_layout()
plt.savefig(out17, dpi=100, bbox_inches='tight')
plt.close()
print("⏳ [18/23] Rendering chart19_daily_cpu_per_workload...")
out19 = PLOT_DIR / "chart19_daily_cpu_per_workload.png"
if not check_and_handle_empty(df_pod, out19, "chart19_daily_cpu_per_workload"):
df_daily_cpu_req = df_pod.groupby("date")["cpu_request_max"].sum()
df_daily_cpu_use = df_pod.groupby("date")["cpu_usage_p95"].sum()
plt.figure(figsize=(11, 5))
plt.fill_between(df_daily_cpu_req.index, df_daily_cpu_req.values, label="Total CPU Request Cores", color="skyblue", alpha=0.4)
plt.plot(df_daily_cpu_use.index, df_daily_cpu_use.values, label="Total CPU P95 Actual Cores", color="navy", linewidth=2.5, marker="o")
plt.title("Daily Total CPU Capacity Allocation vs Actual Peak Consumption (KST)")
plt.xlabel("Date (KST)")
plt.ylabel("Total CPU Cores")
plt.xticks(rotation=30, ha='right')
plt.legend(loc="upper left")
plt.tight_layout()
plt.savefig(out19, dpi=100, bbox_inches='tight')
plt.close()
print("⏳ [19/23] Rendering chart20_daily_mem_per_workload...")
out20 = PLOT_DIR / "chart20_daily_mem_per_workload.png"
if not check_and_handle_empty(df_pod, out20, "chart20_daily_mem_per_workload"):
df_daily_mem_req = df_pod.groupby("date")["mem_request_max"].sum()
df_daily_mem_use = df_pod.groupby("date")["mem_usage_p95"].sum()
plt.figure(figsize=(11, 5))
plt.fill_between(df_daily_mem_req.index, df_daily_mem_req.values, label="Total Memory Request (GB)", color="plum", alpha=0.4)
plt.plot(df_daily_mem_use.index, df_daily_mem_use.values, label="Total Memory P95 Actual (GB)", color="purple", linewidth=2.5, marker="o")
plt.title("Daily Total Memory Capacity Allocation vs Actual Peak Consumption (KST)")
plt.xlabel("Date (KST)")
plt.ylabel("Total Memory (GB)")
plt.xticks(rotation=30, ha='right')
plt.legend(loc="upper left")
plt.tight_layout()
plt.savefig(out20, dpi=100, bbox_inches='tight')
plt.close()
# ─── 🚀 [신설 차트 블록: 21, 22, 23번 융합 확장 필드 시각화 레이어] ───
print("⏳ [20/23] Rendering chart21_cilium_net_to_cpu_ratio...")
out21 = PLOT_DIR / "chart21_cilium_net_to_cpu_ratio.png"
if "feat_net_to_cpu_p95" in df_pod.columns and not check_and_handle_empty(df_pod, out21, "chart21_cilium_net_to_cpu_ratio"):
plt.figure(figsize=(11, 5))
sns.boxplot(data=df_pod, x="workload_type", y="feat_net_to_cpu_p95", palette="YlGnBu")
plt.xticks(rotation=30, ha="right")
plt.title("Cilium CNI Network-to-Compute Ratio Spectrum by Workload")
plt.xlabel("Workload Type")
plt.ylabel("Network Bytes / CPU Util P95 Coefficient")
plt.tight_layout()
plt.savefig(out21, dpi=100, bbox_inches='tight')
plt.close()
print("⏳ [21/23] Rendering chart22_storage_io_asymmetry...")
out22 = PLOT_DIR / "chart22_storage_io_asymmetry.png"
if "feat_io_asymmetry_p95" in df_pod.columns and not check_and_handle_empty(df_pod, out22, "chart22_storage_io_asymmetry"):
plt.figure(figsize=(11, 5))
sns.barplot(data=df_pod, x="workload_type", y="feat_io_asymmetry_p95", palette="flare", errorbar=None)
plt.xticks(rotation=30, ha="right")
plt.title("DirectPV Drive Storage IO Asymmetry Ratio (Write/Read P95)")
plt.xlabel("Workload Type")
plt.ylabel("Asymmetry Coefficient (>1.0 Leans to Write Amplification)")
plt.tight_layout()
plt.savefig(out22, dpi=100, bbox_inches='tight')
plt.close()
print("⏳ [22/23] Rendering chart23_distributed_load_skew_cv...")
out23 = PLOT_DIR / "chart23_distributed_load_skew_cv.png"
if "feat_load_skew_cv_max" in df_pod.columns and not check_and_handle_empty(df_pod, out23, "chart23_distributed_load_skew_cv"):
plt.figure(figsize=(12, 5))
sns.lineplot(data=df_pod, x="date", y="feat_load_skew_cv_max", hue="workload_type", marker="o", linewidth=2)
plt.title("Maximum Distributed Load Skew CV Timeline Trend")
plt.xlabel("Date (KST)")
plt.ylabel("Skew Variation Coefficient (CV >= 0.25 Indicates Data Skew)")
plt.xticks(rotation=30)
plt.legend(bbox_to_anchor=(1.02, 1), loc='upper left', title="Workload Type")
plt.tight_layout()
plt.savefig(out23, dpi=100, bbox_inches='tight')
plt.close()
print(f"\n🏁 === [Step3 Success] All 23 charts generated cleanly under ./data/output/plots/{cluster_target} ===")
if __name__ == "__main__":
main()
step4_governance_analyzer.py (좀비 팟 스캔 및 신규 위반군 격리판)상태 명세 충돌로 인한 필터 먹통 버그를 완치하고, "🚨 장기유휴_좀비팟" 전용의 recommendation 로직 및 Cilium CNI 패킷 드롭 기반의 신규 아키텍처 위반 레이어를 이식했습니다.
"""
step4_governance_analyzer.py — Adaptive Multi-Cluster FinOps Governance & Risk Analyzer (Partition-Isolated Version)
"""
import os
import glob
import argparse
import pandas as pd
import numpy as np
from pathlib import Path
BASE_DATA_DIR = Path("./data")
MERGED_DIR = BASE_DATA_DIR / "merged"
OUTPUT_DIR = BASE_DATA_DIR / "output"
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
def parse_arguments():
parser = argparse.ArgumentParser(description="FinOps Governance & Anomaly Analyzer")
parser.add_argument("--cluster", type=str, required=True, choices=["COMPUTE", "STORAGE"], help="Target cluster type")
return parser.parse_args()
def load_partitioned_data(cluster_target):
cl_dir = MERGED_DIR / cluster_target
if not cl_dir.exists():
return pd.DataFrame(), pd.DataFrame(), pd.DataFrame()
pod_files = glob.glob(str(cl_dir / "daily_enriched_*.parquet"))
ns_files = glob.glob(str(cl_dir / "daily_ns_usage_*.parquet"))
pareto_files = glob.glob(str(cl_dir / "pareto_ns_*.parquet"))
print(f"🔍 [스캔 완료] {cluster_target} 파티션 하방 -> Enriched: {len(pod_files)}개 | NS요약: {len(ns_files)}개 | Pareto: {len(pareto_files)}개")
df_pod = pd.concat([pd.read_parquet(f) for f in pod_files], ignore_index=True) if pod_files else pd.DataFrame()
df_ns = pd.concat([pd.read_parquet(f) for f in ns_files], ignore_index=True) if ns_files else pd.DataFrame()
df_pt = pd.concat([pd.read_parquet(f) for f in pareto_files], ignore_index=True) if pareto_files else pd.DataFrame()
return df_pod, df_ns, df_pt
def main():
args = parse_arguments()
cluster_target = args.cluster.upper()
print(f"🚀 [Step4] Starting FinOps Governance Analyzer for Cluster: {cluster_target}...")
df_pod, df_ns, df_pt = load_partitioned_data(cluster_target)
if df_pod.empty:
print(f"❌ [중단] '{cluster_target}' 클러스터에 정산된 2단계 가공 원부가 존재하지 않습니다.")
return
print(f"✅ 총 {len(df_pod):,}개의 컨테이너 타임라인 원부 통합 바인딩 완료.\n")
# 1. 🚨 [거버넌스 분석 레이어 1] 자원부족 및 OOM Killed 위험군 추출 (시트4 매핑용)
print("🚨 [Analysis 1] Extracting Critical Risk & OOM Killed Pods...")
df_risk = df_pod[
(df_pod["status"] == "💥 OOM장애발생") |
(df_pod["status"] == "⚠️ Request부족") |
(df_pod["oom_strike_sum"] > 0)
].copy()
if not df_risk.empty:
df_risk["recommendation"] = np.where(
df_risk["is_oom_killed"],
"Upsize Memory Limit (OOM Detected)",
"Upsize CPU Request/Limit (Throttling/Throttle Detected)"
)
else:
df_risk["recommendation"] = None
# 2. 📉 [🛡️ 수리 조치: 거버넌스 분석 레이어 2] 과다 할당 및 장기 유휴 좀비 자산 동적 마이닝 (시트2, 3 매핑용)
print("📉 [Analysis 2] Extracting Over-Allocated Slots & Idle Zombie Infrastructure Assets...")
# 신형 스키마 명세인 "📉 자원과다선점"과 "🚨 장기유휴_좀비팟"으로 전면 체인 교정
df_waste = df_pod[
(df_pod["status"] == "📉 자원과다선점") |
(df_pod["status"] == "🚨 장기유휴_좀비팟")
].copy()
if not df_waste.empty:
# 좀비 워크로드와 일반 과할당 워크로드를 격리하여 가이드 분기 생성
df_waste["recommendation"] = np.where(
df_waste["status"] == "🚨 장기유휴_좀비팟",
"Immediate Termination Requested (Zero Activity Long-term Zombie)",
"Downsize CPU/Memory Quota Request Specification (Over-provisioned)"
)
# 낭비량이 큰 순서대로 엔지니어 액션 우선순위 정렬
df_waste = df_waste.sort_values(by="cpu_waste_core_hours", ascending=False)
# 3. 🛡️ [거버넌스 분석 레이어 3] 자원 미설정 배포 위반군 추출 (시트5 매핑용)
print("🛡️ [Analysis 3] Scanning Non-Compliant Missing Resource Specification Pods...")
df_violations = df_pod[
(df_pod["has_no_request"] == True) |
(df_pod["has_no_limit"] == True)
].copy()
# 4. 🌐 [🚀 신설: 거버넌스 분석 레이어 4] 클라우드 네이티브 연계 네트워크 아키텍처 장애 위반군 추가 스캔
print("🌐 [Analysis 4] Scanning Cilium Network Packet Drop Anomalies & Storage Skewness...")
# Cilium eBPF 레이어 패킷 드롭이 어제 하루 누적 100건을 넘거나, 노드 쏠림 변동 계수가 폭발한 심각한 아키텍처 아노말리 격리
df_anomalies = df_pod[
(df_pod["net_drop_errors_total"] > 100) |
(df_pod["feat_load_skew_cv_max"] >= 0.4)
].copy()
if not df_anomalies.empty:
df_anomalies["recommendation"] = np.where(
df_anomalies["feat_load_skew_cv_max"] >= 0.4,
"Review Distributed Partition/Bucket Sharding Key (Data Skewness Detected)",
"Check Cilium eBPF Map Capacity & Kernel Ring Buffer Constraints"
)
# 5. 💾 [결과 마감] 후속 엑셀 빌더 파이프라인으로 전송
print(f"\n💾 [정산 마감] 분석 데이터 자산 output 레이어로 내보내기 진행 중...")
master_gov_file = OUTPUT_DIR / f"governance_master_{cluster_target}.parquet"
risk_file = OUTPUT_DIR / f"gov_risk_oom_{cluster_target}.parquet"
waste_file = OUTPUT_DIR / f"gov_waste_candidates_{cluster_target}.parquet"
viol_file = OUTPUT_DIR / f"gov_violations_{cluster_target}.parquet"
anomaly_file = OUTPUT_DIR / f"gov_anomalies_{cluster_target}.parquet" # 신설 파일
df_pod.to_parquet(master_gov_file, index=False)
df_risk.to_parquet(risk_file, index=False)
df_waste.to_parquet(waste_file, index=False)
df_violations.to_parquet(viol_file, index=False)
df_anomalies.to_parquet(anomaly_file, index=False)
print(f" -> 📦 [저장완료] 전사 {cluster_target} 마스터 원부 : {master_gov_file.name}")
print(f" -> 💥 [저장완료] 고위험군/OOM 리스트 : {risk_file.name} (결과: {len(df_risk)}건)")
print(f" -> 📉 [저장완료] 자원 하향조정 후보군 : {waste_file.name} (결과: {len(df_waste)}건)")
print(f" -> 🛡️ [저장완료] 스펙 미설정 규격위반군: {viol_file.name} (결과: {len(df_violations)}건)")
print(f" -> 🌐 [저장완료] 네트워크/네이티브 아노말리: {anomaly_file.name} (결과: {len(df_anomalies)}건)")
print(f"\n🏁 === [Step4 완수] '{cluster_target}' 거버넌스 가공 원부가 무결하게 갱신되었습니다. ===")
if __name__ == "__main__":
main()