Skip to content

Commit 7ac2b86

Browse files
seedspiritclaude
andcommitted
refactor(BA-5744): migrate kernel live_stat from Valkey to Prometheus
Replace Valkey-backed `KernelStatistics.batch_load_by_kernel` with a Prometheus-backed loader. The new `LegacyLiveStatConverter` adapts `KernelLiveStatBatchResult` into the legacy `dict[metric_name, MetricValue]` shape so existing GQL/WebUI consumers stay compatible. `MetricValue` / `MovingStatValue` move from `common/types.py` to `common/metrics/types.py` next to `RATE_STAT_METRICS` / `DIFF_STAT_METRICS` classifications. Repository fan-out is reduced to a 3-query bundle (gauge / diff / rate); MAX/FIRST templates were prototyped but rolled back — see `docs/kernel-live-stat-followup-issues.md` for the remaining wire-level gaps (`stats.max`, `stats.avg`, capacity for accumulation metrics, plugin metric coverage). Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
1 parent f55366d commit 7ac2b86

15 files changed

Lines changed: 1442 additions & 62 deletions

File tree

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
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

Comments
 (0)