From a79460118d285b06016361183425685161898f44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EC=98=81=EA=B8=B0?= Date: Tue, 14 Jul 2026 18:25:58 +0900 Subject: [PATCH 1/2] knowledge: ingest 2 verified insights (kubelet resource metrics on embedded k8s; host-to-pod memory limit sizing) --- .dev-loop/INGEST_REPORT.md | 90 ++++++++++++++++++ log.md | 2 + .../containers/resource-limits-and-probes.md | 8 +- wiki/infrastructure/index.md | 1 + ...ontainer-metrics-when-cadvisor-is-empty.md | 93 +++++++++++++++++++ 5 files changed, 192 insertions(+), 2 deletions(-) create mode 100644 .dev-loop/INGEST_REPORT.md create mode 100644 wiki/infrastructure/observability/container-metrics-when-cadvisor-is-empty.md diff --git a/.dev-loop/INGEST_REPORT.md b/.dev-loop/INGEST_REPORT.md new file mode 100644 index 0000000..e9bcc5a --- /dev/null +++ b/.dev-loop/INGEST_REPORT.md @@ -0,0 +1,90 @@ +# Knowledge flush — 2 insight(s) + +Cross-Check: every mechanism claim was independently re-verified against primary +sources (chart values.yaml, kubernetes source, kubernetes-mixin source, psycopg2 +docs) fetched live during this flush — not taken from the harvesting session. + +Queue note: a third queued row (QueryPie streaming / fetchmany + write_only, +hash `637877dfc33bc0f0`) was already flushed as **PR #1** (open) but had not +been removed from its session queue file; it was retired without re-ingesting. + +## Verified best-practice + +### Insight 1 — container_* metrics empty on OrbStack k8s despite kubelet targets up + +Claim chain and verification: + +1. **kube-prometheus-stack ships the kubelet resource-metrics scrape disabled, + with a stale default path.** + Source: `charts/kube-prometheus-stack/values.yaml` (prometheus-community/helm-charts, + fetched live) — `resource: false`, `resourcePath: "/metrics/resource/v1alpha1"`, + with the in-file comment "From kubernetes 1.18, /metrics/resource/v1alpha1 + renamed to /metrics/resource". → **verified** +2. **`/metrics/resource` exposes `container_cpu_usage_seconds_total` and + `container_memory_working_set_bytes` with labels `container, pod, namespace` + only — no `image` label.** + Source: `pkg/kubelet/metrics/collectors/resource_metrics.go` (kubernetes/kubernetes, + fetched live; both metrics STABLE). → **verified** +3. **The bundled Compute Resources dashboards cannot show these series.** + Source: `dashboards/resources/queries/pod.libsonnet` (kubernetes-monitoring/ + kubernetes-mixin, fetched live) — memory queries select `cadvisorSelector` + (`metrics_path="/metrics/cadvisor"`) AND `image!=""`; the resource-endpoint + series fail both. → **verified** +4. **OrbStack's kubelet cAdvisor endpoint emits only `machine_*`, zero + `container_*`, while the scrape reports success.** + First-hand probe (2026-07-14): `kubectl get --raw .../proxy/metrics/cadvisor` + → 0 `container_` lines, `machine_scrape_error 0`; `/metrics/resource` → + real working-set values. No official OrbStack doc states this; orbstack#1561 + corroborates the metrics-integration gap. → **field-tested** (marked as such + in the page body; the page's other rows are verified). + +### Insight 2 — "ran fine on the host" is not evidence a pod memory limit fits + +1. **Exceeding a container memory limit ⇒ OOMKill; limits must come from + measurement.** Already sourced on the existing page + (kubernetes.io manage-resources-containers). → **verified** (pre-existing) +2. **A default (client-side) psycopg2 cursor loads the entire result set + client-side, so memory scales with data size; server-side cursors transfer + controlled amounts.** Source: https://www.psycopg.org/docs/usage.html#server-side-cursors, + fetched live — "the Psycopg cursor usually fetches all the records returned + by the backend … a proportionally large amount of memory will be allocated + by the client" / "transfer to the client only a controlled amount of data". + → **verified** +3. **Concrete failure/measurement**: 800k-row extract OOMKilled in a 1Gi pod + after 20 min; measured 1.5Gi+ peak once raised to 3Gi (mac-server k8s + migration, 2026-07-13). → **field-tested** context, used as the motivating + example only. + +## Existing-layer check + +- Pages read: root `INDEX.md`, `infrastructure/index.md`, + `infrastructure/containers/resource-limits-and-probes.md`, + `infrastructure/observability/logs-metrics-signals.md`; PR #1's diff + (`databases/query-optimization/streaming-large-result-sets.md`) reviewed for + overlap with insight 2. +- **Insight 1**: no existing page covers scraping/metric-source selection for + container metrics. `logs-metrics-signals` is about instrumenting your own + service (different trigger) — not a duplicate. → new page, `related:` linked + both ways with `resource-limits-and-probes` (its "set limits from + measurement" rule depends on these very metrics) and one-way to + `logs-metrics-signals`. +- **Insight 2**: same trigger family as `resource-limits-and-probes` + ("pods OOMKilled") and same directive spirit ("measure, don't copy") → **merged** + into that page (1 edge-case row + 1 instead-of row + psycopg2 source), no new + page. The streaming *fix* itself lives in PR #1's databases page; a + `related:` link to it was **deliberately not added** because that page is not + on `main` yet (PR #1 unmerged) — flagged here instead so the owner can add + the link when/if PR #1 merges. +- Conflicts: none found. + +## Routing decision + +- **Insight 1** → `infrastructure/observability/container-metrics-when-cadvisor-is-empty.md` + (**new page**). Category `observability` fits (metrics collection/source + selection); no new category needed. Registered in `infrastructure/index.md` + with a load-when line keyed on "embedded/local k8s + container_* empty + + targets up"; `log.md` ingest entry appended. +- **Insight 2** → `infrastructure/containers/resource-limits-and-probes.md` + (**merge/revise**, no new page). Category `containers` already owns the + OOMKill/limit-sizing trigger. `last_verified` bumped to 2026-07-14; `log.md` + revise entry appended. diff --git a/log.md b/log.md index bcc206d..9d607da 100644 --- a/log.md +++ b/log.md @@ -32,3 +32,5 @@ Append-only. Format: `## [YYYY-MM-DD] /proxy/metrics/cadvisor | grep -c '^container_' + kubectl get --raw /api/v1/nodes//proxy/metrics/resource | grep -c '^container_' + ``` + + If cAdvisor shows 0 but `/metrics/resource` shows real values, switch the + scrape source instead of debugging the scrape. + +2. Enable the kubelet resource-metrics scrape in kube-prometheus-stack values, + and override the path — the chart's default still points at the pre-1.18 + versioned path, which 404s on current kubelets: + + ```yaml + kubelet: + serviceMonitor: + resource: true + resourcePath: "/metrics/resource" # chart default is /metrics/resource/v1alpha1 + ``` + +3. Chart these series on a custom dashboard, not the bundled + "Compute Resources" dashboards. `/metrics/resource` exposes + `container_cpu_usage_seconds_total` and `container_memory_working_set_bytes` + with only `container, pod, namespace` labels (no `image`), while the bundled + kubernetes-mixin queries select `metrics_path="/metrics/cadvisor"` and filter + `image!=""` — both conditions exclude the resource-endpoint series. + +4. Treat `/metrics/resource` as the core-usage subset it is: CPU seconds and + working-set bytes per container/pod/node (it is what metrics-server reads). + Filesystem, network, and throttling detail exists only in cAdvisor; if you + need those on such a cluster, they are simply unavailable from the kubelet. + +## Edge cases + +| Case | Then | +|------|------| +| Resource-metrics target shows **down** with 404 after enabling `resource: true` | The path override was missed — set `resourcePath: "/metrics/resource"` (the unversioned GA path) | +| Bundled dashboards stay empty after the scrape works | Expected — mixin queries require `metrics_path="/metrics/cadvisor"` and `image!=""`; build panels that query the series without those selectors | +| Verifying whether the same gap exists on another local distro | Run the two `kubectl get --raw` probes from step 1 first; only the endpoint contents distinguish "scrape broken" from "endpoint empty" | +| Production/managed cluster (EKS/GKE/AKS) missing `container_*` | Different failure — cAdvisor works there; check kubelet scrape auth, relabeling drops, and job selectors before touching `resourcePath` | + +## Instead of + +| If you are about to | Do this instead | Why | +|---------------------|-----------------|-----| +| Debug the scrape because targets are up but `container_*` is empty | Probe the endpoint bodies with `kubectl get --raw` (step 1) | A cAdvisor endpoint emitting only `machine_*` scrapes "successfully"; target health cannot detect an empty metric family | +| Enable `resource: true` alone and trust the chart default path | Also set `resourcePath: "/metrics/resource"` | The chart default `/metrics/resource/v1alpha1` predates the k8s 1.18 rename and 404s on current kubelets | +| Wait for the bundled Compute Resources dashboards to light up | Build a custom dashboard on the resource-endpoint series | Mixin queries hard-filter on `metrics_path="/metrics/cadvisor"` and `image!=""`, which the resource endpoint's label set can never satisfy | + +## Notes on evidence + +The mechanism rows above are verified against the primary sources below. The +OrbStack-specific claim — its kubelet cAdvisor endpoint emits `machine_*` but +zero `container_*` series — is **field-tested** (first-hand probe on OrbStack +1.x, 2026-07-14: cAdvisor `container_` count 0 with `machine_scrape_error 0`, +`/metrics/resource` returning real working-set values), corroborated by the +OrbStack tracker discussion of missing metrics integrations. + +## Sources + +- https://github.com/prometheus-community/helm-charts/blob/main/charts/kube-prometheus-stack/values.yaml — `kubelet.serviceMonitor.resource` disabled by default; `resourcePath` default `/metrics/resource/v1alpha1` with the "renamed in 1.18" comment +- https://github.com/kubernetes/kubernetes/blob/master/pkg/kubelet/metrics/collectors/resource_metrics.go — `/metrics/resource` exposes `container_cpu_usage_seconds_total` / `container_memory_working_set_bytes` with labels `container, pod, namespace` only (STABLE) +- https://github.com/kubernetes-monitoring/kubernetes-mixin/blob/master/dashboards/resources/queries/pod.libsonnet — bundled dashboard queries select `cadvisorSelector` + `image!=""` +- https://github.com/orbstack/orbstack/issues/1561 — OrbStack metrics-server/cAdvisor integration gaps (corroboration) From a3ada1b0e1e1dac28995974037b9bc0668c7c0a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EC=98=81=EA=B8=B0?= Date: Tue, 14 Jul 2026 18:28:56 +0900 Subject: [PATCH 2/2] knowledge: add Decision Log to ingest report --- .dev-loop/INGEST_REPORT.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.dev-loop/INGEST_REPORT.md b/.dev-loop/INGEST_REPORT.md index e9bcc5a..d56e156 100644 --- a/.dev-loop/INGEST_REPORT.md +++ b/.dev-loop/INGEST_REPORT.md @@ -1,5 +1,15 @@ # Knowledge flush — 2 insight(s) +## Decision Log + +- **의도**: dev-loop 큐의 pending 인사이트 2건(OrbStack kubelet 메트릭 소스 전환, host→pod 메모리 리밋 산정)을 1차 소스로 재검증해 위키에 반영. 이슈처럼 큰 변경이 아니라 지식 ingest — 페이지 1 신규 + 1 병합. +- **배제한 대안**: + - insight 1을 `observability/logs-metrics-signals.md`에 병합 → 배제: 그 페이지는 "내 서비스 계측"이 트리거고, 이 건은 "클러스터 메트릭 수집 소스 선택"이라 트리거가 다름(one case per page 규칙). + - insight 2를 신규 페이지로 생성 → 배제: `resource-limits-and-probes.md`가 이미 "OOMKilled + 측정 기반 리밋" 트리거를 소유 — 엣지케이스/Instead-of 행 병합이 중복 없는 형태. + - PR #1의 `streaming-large-result-sets` 페이지로의 `related:` 링크 → 보류: PR #1 미머지 상태라 main에 없는 페이지로의 링크는 깨질 수 있음(본문 Existing-layer check에 플래그). +- **리뷰어가 볼 곳**: 신규 페이지의 4개 소스가 각 주장과 대응하는지(values.yaml 기본값, resource_metrics.go 라벨셋, mixin `image!=""`+cadvisorSelector, orbstack#1561), OrbStack 주장이 field-tested로 정직하게 표기됐는지, `resource-limits-and-probes.md` 추가 2행이 기존 규칙 4("measure, don't copy")와 중복이 아닌 보강인지. +- [추정] 없음 — 모든 메커니즘 주장은 이번 flush에서 라이브로 가져온 1차 소스로 검증됨(본문 Verified best-practice 참조). + Cross-Check: every mechanism claim was independently re-verified against primary sources (chart values.yaml, kubernetes source, kubernetes-mixin source, psycopg2 docs) fetched live during this flush — not taken from the harvesting session.