|
| 1 | +# BA-5744 후속 이슈 — kernel `live_stat` Prometheus 이행 잔여 갭 |
| 2 | + |
| 3 | +> BA-5744 (`live_stat` GQL resolver Valkey → Prometheus 이행) 머지 후, wire-level 베이스라인 (레거시 Valkey) 과 비교했을 때 남는 차이 항목들을 별도 작업 단위로 정리한다. 본 문서는 *문제 정의* 만 담는다 — 해결 옵션은 별도 BEP/티켓에서 본격 논의. |
| 4 | +> |
| 5 | +> 관련 배경: [`prometheus-metric-design-guide.md`](./prometheus-metric-design-guide.md) §8 |
| 6 | +
|
| 7 | +--- |
| 8 | + |
| 9 | +## 0. wire-level 갭 한눈에 보기 |
| 10 | + |
| 11 | +### 0-1. 필드별 비교 (A 라벨: 레거시 Valkey, B 라벨: 현재 Prometheus) |
| 12 | + |
| 13 | +| 필드 | A 라벨 (Valkey baseline) | B 라벨 (현재 Prometheus) | 상태 | 후속 §| |
| 14 | +|---|---|---|---|---| |
| 15 | +| `current` | `MovingStatistics` 마지막 값 | gauge `currents[0]` (RATE/DIFF 는 `currents[-1]`) | ✅ 동등 | — | |
| 16 | +| `capacity` | `measure.capacity or measure.value` (첫 샘플 박제) | Prometheus `value_type=capacity` 시계열, 누적 카운터엔 부재 → `"0"` | ❌ | §3 | |
| 17 | +| `pct` | agent 측 또는 derive | PCT 샘플 우선 + `current/capacity` derive 폴백 | ⚠️ capacity 가 0 인 메트릭은 derive 도 불가 → `"0"` | §3 (의존) | |
| 18 | +| `unit_hint` | agent emit | `METRIC_UNIT_HINTS` + 네이밍 컨벤션 + metric_name 폴백 | ✅ 동등 (plugin 도 컨벤션으로 회복) | — | |
| 19 | +| `stats.diff` | `MovingStatistics.diff` (per-window) | `rate(cpu_util[5s])` (per-second) | ⚠️ 단위 불일치 — cpu_util 만 적용. GQL consumer 는 `cpu_util.pct` 만 읽으므로 무해 | (인지된 갭) | |
| 20 | +| `stats.rate` | `MovingStatistics.rate` (bytes/5sec) | `rate(net_*[5s]) * 5` 핵 | ✅ wire-level 동등 | — | |
| 21 | +| `stats.max` | `MovingStatistics.max` (lifetime) | `"0"` 고정 | ❌ | §1 | |
| 22 | +| `stats.avg` | `MovingStatistics.avg` (lifetime) | `"0"` 고정 | ❌ | §2 | |
| 23 | +| `stats.min` / `stats.sum` | `MovingStatistics.min` / `.sum` | `"0"` 고정 | ❌ | §4 | |
| 24 | +| `stats.version` | legacy version int 또는 `None` | `None` 고정 | ⚠️ legacy 클라이언트가 version 분기 안 하면 무해 | §6 | |
| 25 | + |
| 26 | +### 0-2. 메트릭별 영향 범위 |
| 27 | + |
| 28 | +| 메트릭 부류 | `current` | `capacity` | `pct` | `stats.diff/rate` | `stats.max/avg/min/sum` | |
| 29 | +|---|---|---|---|---|---| |
| 30 | +| `mem` (gauge) | ✅ | ✅ | ✅ | n/a | ❌ "0" | |
| 31 | +| `cpu_util` (DIFF) | ✅ (rate result) | ✅ | ✅ | ✅ diff (단위 변경) | ❌ "0" | |
| 32 | +| `net_rx` / `net_tx` (RATE) | ✅ (rate result) | ❌ "0" | ❌ derive 불가 | ✅ rate | ❌ "0" | |
| 33 | +| `cpu_used` (accum) | ✅ | ❌ "0" | ❌ derive 불가 | n/a | ❌ "0" | |
| 34 | +| `io_read` / `io_write` (accum) | ✅ | ❌ "0" | ❌ derive 불가 | n/a | ❌ "0" | |
| 35 | +| `io_scratch_size` (gauge) | ✅ | ❌ agent emit 안 함 | ❌ derive 불가 | n/a | ❌ "0" | |
| 36 | +| `cuda_mem` / `cuda_util` (plugin) | ✅ | plugin emit 시 ✅ | plugin emit 시 ✅ | ❌ whitelist 누락 → "0" | ❌ "0" | |
| 37 | + |
| 38 | +가장 큰 실질 갭은 **누적 카운터 메트릭의 `capacity` 가 0 이 되어 `pct` derive 도 막히는 것** (`cpu_used`, `net_*`, `io_*`). WebUI 가 이 메트릭들의 pct 를 직접 읽고 있다면 §3 의 우선순위가 가장 높다. |
| 39 | + |
| 40 | +--- |
| 41 | + |
| 42 | +## 1. `stats.max` — lifetime max vs window max |
| 43 | + |
| 44 | +### 현상 |
| 45 | + |
| 46 | +레거시 `stats.max` 는 agent 프로세스가 살아 있는 동안의 **lifetime peak** 였다 (`agent/stats.py:205-251` 의 `MovingStatistics._max`). 본 PR 에서는 `max_over_time` 우회를 시도했으나 (1) window 외 lifetime peak 표현 불가 (2) plugin metric 화이트리스트 관리 부담 두 가지 한계로 롤백, 현재 wire 에는 `"0"` 이 고정으로 나간다. |
| 47 | + |
| 48 | +### 영향 |
| 49 | + |
| 50 | +| metric | 레거시 stats.max | 현재 동작 | |
| 51 | +|---|---|---| |
| 52 | +| `cpu_util` | lifetime max | ❌ `"0"` | |
| 53 | +| `mem` | lifetime max | ❌ `"0"` | |
| 54 | +| `io_scratch_size` | lifetime max | ❌ `"0"` | |
| 55 | +| `cuda_mem` (plugin) | lifetime max | ❌ `"0"` | |
| 56 | +| `cuda_util` (plugin) | lifetime max | ❌ `"0"` | |
| 57 | + |
| 58 | +### 검토되어야 할 옵션 (개략) |
| 59 | + |
| 60 | +- (a) agent 가 `stats.max` 를 명시적 metric (별도 Gauge) 으로 emit → manager 는 그대로 노출. lifetime peak 가 보존되지만 agent 변경 필요. |
| 61 | +- (b) PromQL `max_over_time([window])` + `label_replace(value_type→"max")` 우회 — 단, **window peak ≠ lifetime peak** 한계 + plugin coverage 화이트리스트 부담. |
| 62 | +- (c) "어떤 metric 에 max 가 의미 있나" 를 agent 측 metric metadata 로 노출하고 manager 가 그걸 읽어 동적 분기 — 화이트리스트 자체를 없애는 방향 (§5 와 한 묶음). |
| 63 | + |
| 64 | +--- |
| 65 | + |
| 66 | +## 2. `stats.avg` — 측정 자체 누락 |
| 67 | + |
| 68 | +### 현상 |
| 69 | + |
| 70 | +레거시 `stats.avg` 는 `MovingStatistics.avg = sum / count` — agent 프로세스 수명 동안의 **lifetime mean**. Prometheus 이행 후에는 manager 가 *어떤* avg 도 계산하지 않아 `stats.avg = "0"` (default) 로 wire 에 나간다. |
| 71 | + |
| 72 | +### 영향 |
| 73 | + |
| 74 | +| metric | 레거시 wire (stats_filter) | 현재 동작 | |
| 75 | +|---|---|---| |
| 76 | +| `cpu_util` | `stats.avg` 포함 (`{"avg","max"}`) | ❌ `"0"` | |
| 77 | +| `cuda_util` (plugin) | `stats.avg` 포함 (`{"avg","max"}`) | ❌ `"0"` | |
| 78 | +| 그 외 | `stats.avg` 없음 (stats_filter 미포함) | ✅ `"0"` 로 동등 | |
| 79 | + |
| 80 | +영향받는 건 **utilization 계열 metric (cpu_util, cuda_util) 의 stats.avg** 두 군데. |
| 81 | + |
| 82 | +### 검토되어야 할 옵션 (개략) |
| 83 | + |
| 84 | +- (a) PromQL 쿼리 추가: `avg_over_time(metric[window])` + `label_replace(value_type→"avg")`. §1 의 max 와 같은 패턴. 단 **window avg ≠ lifetime avg** 한계 동일. |
| 85 | +- (b) agent 가 `stats.avg` 를 명시적 metric 으로 emit (§1 (a) 와 묶음). |
| 86 | +- (c) cpu_util 한정 우회: `cpu_util.current` 자체가 이미 `rate(cpu_used[window])` 로 *window 평균 utilization* — converter 단에서 `cpu_util.current` 를 `cpu_util.stats.avg` 로 매핑하면 근사 가능. 하지만 cuda_util 에는 같은 트릭이 안 먹어 일관성이 깨짐. |
| 87 | + |
| 88 | +--- |
| 89 | + |
| 90 | +## 3. `capacity` — 누적 카운터 메트릭의 capacity 부재 |
| 91 | + |
| 92 | +### 현상 |
| 93 | + |
| 94 | +agent 가 `Measurement(value, capacity=...)` 호출 시, 누적 카운터/rate 류 메트릭 (`cpu_used`, `net_rx`, `net_tx`, `io_read`, `io_write`, `io_scratch_size`) 은 `capacity` 인자를 안 넘긴다. 레거시 `agent/stats.py:791` 은 `capacity = measure.capacity or measure.value` 로 **첫 샘플을 capacity 자리에 박제** 했지만, Prometheus 에는 이 우회가 없다. |
| 95 | + |
| 96 | +본 PR 에서 `first_over_time + label_replace` 로 합성 시도했으나 Prometheus 3.1.0 의 experimental function 제약으로 롤백 ([`prometheus-metric-design-guide.md`](./prometheus-metric-design-guide.md) §8-4). |
| 97 | + |
| 98 | +### 영향 |
| 99 | + |
| 100 | +| metric | 레거시 capacity | 현재 capacity | pct derive 가능? | |
| 101 | +|---|---|---|---| |
| 102 | +| `mem` | container memory limit (agent emit) | ✅ 그대로 | ✅ | |
| 103 | +| `cpu_util` | 명시 emit | ✅ 그대로 | ✅ | |
| 104 | +| `cpu_used` | 첫 샘플 박제 | ❌ `"0"` | ❌ | |
| 105 | +| `net_rx` / `net_tx` | 첫 샘플 박제 | ❌ `"0"` | ❌ | |
| 106 | +| `io_read` / `io_write` | 첫 샘플 박제 | ❌ `"0"` | ❌ | |
| 107 | +| `io_scratch_size` | 첫 샘플 박제 | ❌ `"0"` | ❌ | |
| 108 | + |
| 109 | +⚠️ 현재 wire 에서 가장 가시적인 갭. WebUI 가 위 메트릭의 pct 를 직접 읽고 있다면 즉시 영향. |
| 110 | + |
| 111 | +### 검토되어야 할 옵션 (개략) |
| 112 | + |
| 113 | +- (a) agent 가 metric 별 의미 있는 capacity 를 명시 emit (cpu cgroup quota / container memory limit / network bandwidth limit / scratch disk quota) — 본질 해결. |
| 114 | +- (b) manager 가 `(kernel_id, metric_name) → first_value` 를 영속 저장 (DB/Redis) → 레거시 핵의 단순 이동. |
| 115 | +- (c) `min_over_time` (단조 증가 카운터에서 first 와 동일, 안정 함수) — 레거시 핵의 PromQL 버전. |
| 116 | +- (d) 누적 카운터 메트릭의 `capacity` 필드를 omit — 가장 정직하지만 클라이언트 호환성 영향. |
| 117 | + |
| 118 | +설계 합의 필요 항목: `Measurement.capacity` 가 "리소스 한도" 인가 "최초 관측값" 인가, `pct = current/capacity` 의 의미, 누적 카운터에서의 pct 정의. |
| 119 | + |
| 120 | +--- |
| 121 | + |
| 122 | +## 4. `stats.min` / `stats.sum` — 측정 자체 누락 |
| 123 | + |
| 124 | +### 현상 |
| 125 | + |
| 126 | +`stats.avg` (§2) 와 동일 카테고리. 레거시 `MovingStatistics.{min, sum}` 은 lifetime 통계지만 Prometheus 이행 후 manager 는 계산 자체를 하지 않아 `"0"` 고정. |
| 127 | + |
| 128 | +### 영향 |
| 129 | + |
| 130 | +레거시 stats_filter 에서 `min` / `sum` 을 emit 하던 metric 만 영향. 현재 어느 metric 도 stats_filter 에 `min`/`sum` 을 넣지 않으므로 **wire-level 영향 없음** (양쪽 다 `"0"`). 다만 follow-up 시 §1 (max) / §2 (avg) 와 동일한 메커니즘으로 같이 처리되는 게 자연스러움. |
| 131 | + |
| 132 | +### 검토되어야 할 옵션 (개략) |
| 133 | + |
| 134 | +§1 (a)/(c) 와 동일 — agent 측 명시 metric emit, 또는 metric metadata 기반 분기. |
| 135 | + |
| 136 | +--- |
| 137 | + |
| 138 | +## 5. plugin metric coverage — 화이트리스트 관리의 한계 |
| 139 | + |
| 140 | +### 현상 |
| 141 | + |
| 142 | +`MAX_METRICS`, `RATE_STAT_METRICS`, `DIFF_STAT_METRICS`, `METRIC_UNIT_HINTS` 등 "어떤 metric 이 어떤 stats / 단위 적용 대상인가" 를 manager 측 화이트리스트로 관리한다. plugin metric (cuda_mem, cuda_util 등) 이 새로 추가될 때마다 manager 코드를 같이 수정해야 한다. |
| 143 | + |
| 144 | +### 영향 |
| 145 | + |
| 146 | +| metric | 영향 필드 | 현재 동작 | |
| 147 | +|---|---|---| |
| 148 | +| `cuda_mem` | `stats.diff` / `stats.rate` 분기 | ❌ whitelist 누락 → 모두 `"0"` | |
| 149 | +| `cuda_util` | `stats.diff` / `stats.rate` 분기 | ❌ whitelist 누락 → 모두 `"0"` | |
| 150 | +| `unit_hint` (전반) | plugin 신규 metric | ⚠️ `METRIC_UNIT_HINTS` 미등록 시 네이밍 컨벤션 폴백 (정확하지 않을 수 있음) | |
| 151 | + |
| 152 | +### 검토되어야 할 옵션 (개략) |
| 153 | + |
| 154 | +- (a) agent 측 metric metadata (`metric_type`, `unit_hint`, `is_rate`, `is_diff` 등) 를 노출 → manager 가 읽어 동적 분기. 화이트리스트 코드 제거. |
| 155 | +- (b) [`prometheus-metric-design-guide.md`](./prometheus-metric-design-guide.md) §6 의 A 옵션 (Counter / Gauge 분리) 와 한 묶음. metric type 자체를 Prometheus 메타데이터로 노출. |
| 156 | + |
| 157 | +--- |
| 158 | + |
| 159 | +## 6. `stats.version` — legacy 호환 필드 |
| 160 | + |
| 161 | +### 현상 |
| 162 | + |
| 163 | +레거시 wire 의 `stats.version` 은 `MovingStatValue.version` (legacy client compatibility) 필드. 현재 converter 는 `make_default_metric_value` 의 `None` 을 그대로 둔다. |
| 164 | + |
| 165 | +### 영향 |
| 166 | + |
| 167 | +WebUI / legacy client 가 `stats.version` 으로 분기 로직을 가지고 있는지 확인 필요. 분기 안 하면 무해, 분기한다면 legacy 값을 모사해야 함. |
| 168 | + |
| 169 | +### 검토되어야 할 옵션 (개략) |
| 170 | + |
| 171 | +- (a) WebUI / SDK consumer 코드 점검 후 `None` 으로 충분하면 closed-as-wontfix. |
| 172 | +- (b) 필요 시 converter 에서 고정 version int 를 emit. |
| 173 | + |
| 174 | +--- |
| 175 | + |
| 176 | +## 7. 참고 |
| 177 | + |
| 178 | +- [`prometheus-metric-design-guide.md`](./prometheus-metric-design-guide.md) §8 — 본 PR 진행 중 발견된 갭 일람 |
| 179 | +- 본 PR 에서 롤백된 코드 (§1 의 `_MAX_TEMPLATE`, §3 의 `_FIRST_TEMPLATE`) 는 git history 에서 참조 가능 |
| 180 | +- A/B 실측 비교 절차: `scripts/test-live-stat-equivalence.sh` + `docs/superpowers/test-plans/2026-04-26-BA-5824-live-stat-equivalence-manual-test.md` |
0 commit comments