Wire kube-apiserver and kubelet tracing to a Jaeger collector on docker-29, deploy Grafana Alloy in-cluster to ship logs/metrics, and stand up Mimir + Loki on docker-30 as their backing stores. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
9.6 KiB
Pod-creation latency tracing with OpenTelemetry
Context
Goal: measure end-to-end latency of kubectl apply → pod running, broken down by component. The approach is to enable native OpenTelemetry tracing in Kubernetes 1.32 components (kube-apiserver, kubelet, etcd) and ship spans to a Jaeger backend running outside the cluster on docker-29. With all three reporting under the same trace id (object-resource correlation), Jaeger will let us see how much time is spent in admission/etcd, on the wire, and inside the kubelet's pod sync loop.
Constraints / known gaps:
- kube-scheduler has no native OTLP tracing yet; there will be a gap between "object stored" and "kubelet sees assignment". We can infer it from event timestamps (
kubectl get events) or.status.conditionsPodScheduled/Initialized/ContainersReady/Ready timestamps. - Cluster is K8s 1.32, kubeadm-bootstrapped via Terraform/cloud-init. Per Phase-3 decision, the canonical source-of-truth is the Terraform cloud-init; changes there cover future rebuilds. To activate tracing on the currently running nodes we also need a one-time manual application step (documented at the bottom).
Backend: Jaeger on docker-29
New directory docker-29/tracing/ containing docker-compose.yaml. Uses Jaeger all-in-one v1 with Badger on-disk storage and the built-in OTLP receiver enabled.
# docker-29/tracing/docker-compose.yaml
services:
jaeger:
image: jaegertracing/all-in-one:1.62
container_name: jaeger
restart: unless-stopped
environment:
SPAN_STORAGE_TYPE: badger
BADGER_EPHEMERAL: "false"
BADGER_DIRECTORY_VALUE: /badger/data
BADGER_DIRECTORY_KEY: /badger/key
BADGER_SPAN_STORE_TTL: 4h # auto-expire traces older than 4h
BADGER_MAINTENANCE_INTERVAL: 5m # GC cycle that reclaims disk
COLLECTOR_OTLP_ENABLED: "true"
volumes:
- ./badger:/badger
ports:
- "4317:4317" # OTLP gRPC (apiserver + kubelet exporters)
- "4318:4318" # OTLP HTTP (optional)
- "16686:16686" # Jaeger UI
Endpoint that the cluster will send to: 192.168.0.29:4317. Jaeger UI: http://192.168.0.29:16686.
The OTLP receiver in Jaeger all-in-one accepts plaintext OTLP/gRPC by default — matches endpoint: ...:4317 on the K8s side with no TLS. Acceptable for a home lab on a private LAN.
Storage sizing and retention
Two complementary levers control how much disk Badger uses:
- TTL —
BADGER_SPAN_STORE_TTL=4hdeletes spans older than 4 hours; Badger's value-log GC reclaims disk onBADGER_MAINTENANCE_INTERVAL(default 5m). This is the hard upper bound on retention. - Sampling rate — controls how many spans get written in the first place. Set in the apiserver/kubelet TracingConfiguration (
samplingRatePerMillion).
Rough estimate for this cluster (1 master + 2 workers, mostly idle, Flux reconciling continuously):
| Sampling | Span ingest rate | Disk footprint at 4h TTL |
|---|---|---|
100% (1000000) |
~500–3000 spans/s | ~6–40 GB steady-state |
1% (10000) |
~5–30 spans/s | ~60–400 MB steady-state |
0.1% (1000) |
~0.5–3 spans/s | ~6–40 MB steady-state |
Note: 100% sampling on this cluster is sized for short experiments, not long-term operation. The 4h TTL prevents runaway growth if I forget to lower it, but Badger has no built-in size cap — only TTL + GC. If a hard size cap is needed, mount ./badger/ from a fixed-size LVM volume or zfs dataset with a quota.
Recommended progression:
- Start with
samplingRatePerMillion: 1000000(100%) andTTL=4hso the first measurements are easy to find. - After the first round of experiments, drop sampling to
10000(1%) for steady-state and adjustTTLupward if a longer history is useful.
kube-apiserver tracing
Two changes, both in the Terraform cloud-init / referenced manifest:
1. Write the TracingConfiguration file (master.tf cloud-init)
Add to write_files in kubernetes-kvm-terraform/master.tf:
- path: /etc/kubernetes/tracing-config.yaml
content: |
apiVersion: apiserver.config.k8s.io/v1beta1
kind: TracingConfiguration
endpoint: 192.168.0.29:4317
samplingRatePerMillion: 1000000 # 100% sampling for measurement work
The apiserver static pod already mounts /etc/kubernetes read-only via the k8s-config volume, so the file becomes visible inside the container automatically — no new volume mount needed.
2. Add the apiserver flag
Edit kubernetes-kvm-terraform/files/manifests/kube-apiserver.yaml — add one new flag in the command: list (alphabetical placement after --tls-private-key-file works; ordering isn't enforced):
- --tracing-config-file=/etc/kubernetes/tracing-config.yaml
APIServerTracing is GA in 1.32 — no feature-gate flag required.
kubelet tracing
Add a tracing: block to the existing KubeletConfiguration document inside the kubeadm-config heredoc in kubernetes-kvm-terraform/master.tf (around line 158):
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
cgroupDriver: systemd
tracing:
endpoint: 192.168.0.29:4317
samplingRatePerMillion: 1000000
KubeletTracing is beta and on by default in 1.32 — no feature gate needed.
Kubeadm stores this KubeletConfiguration in the kubelet-config ConfigMap in kube-system, so workers joining via kubeadm join pick the tracing block up automatically. The two existing workers (kube-node-32, kube-node-33) need a one-time manual nudge (see below) because they were already joined before the change.
etcd tracing (optional, second-step)
etcd supports OTLP via flags --experimental-enable-distributed-tracing, --experimental-distributed-tracing-address=192.168.0.29:4317, and --experimental-distributed-tracing-sampling-rate=1000000. Enabling these requires editing the etcd static-pod manifest on the master — propose deferring until after the apiserver+kubelet path is verified, so we keep the first iteration narrow.
Files to change
| File | Change |
|---|---|
docker-29/tracing/docker-compose.yaml |
new — Jaeger all-in-one + Badger |
kubernetes-kvm-terraform/master.tf |
add tracing-config.yaml to write_files; add tracing: block to KubeletConfiguration |
kubernetes-kvm-terraform/files/manifests/kube-apiserver.yaml |
add --tracing-config-file=/etc/kubernetes/tracing-config.yaml |
kubernetes-kvm-terraform/files/tracing-config.yaml |
new — reference copy of the TracingConfiguration (mirrors the inline content in master.tf, matches the existing auth-config.yaml convention) |
No changes to nodes-on-homer.tf / nodes-on-beelink.tf are needed — workers receive kubelet config from the cluster ConfigMap on join.
Applying to the currently running cluster
The Terraform changes cover future rebuilds. To activate tracing now without rebuilding:
- Start Jaeger on docker-29:
cd docker-29/tracing && docker compose up -d(creates./badger/for storage). - On the master (kube-master-31, 192.168.0.31):
scpor write/etc/kubernetes/tracing-config.yamlwith the TracingConfiguration shown above.- Edit
/etc/kubernetes/manifests/kube-apiserver.yamlto add--tracing-config-file=/etc/kubernetes/tracing-config.yaml. Kubelet will detect the manifest change and restart the apiserver static pod within seconds. - Watch:
crictl ps | grep apiserver, thenkubectl get --raw /livez.
- Update the kubelet-config ConfigMap in
kube-system:kubectl -n kube-system edit cm kubelet-configand add the sametracing:block underkubelet:.
- On every node (master + both workers): edit
/var/lib/kubelet/config.yamlto append thetracing:block, thensystemctl restart kubelet. Pods will not be evicted by a kubelet restart, but expect a brief NotReady blip per node — stagger the restarts.
Verification
End-to-end test once everything is live:
- UI reachable: open
http://192.168.0.29:16686and check "Services" dropdown populates withapiserver(it will appear only after the first traced request). - Generate traffic that exercises the full path:
kubectl run trace-probe --image=registry.k8s.io/pause:3.10 --restart=Never kubectl wait --for=condition=Ready pod/trace-probe --timeout=60s kubectl delete pod trace-probe - In Jaeger UI, search service
apiserver, operationKubernetesAPI(or similar) — you should see a trace covering admission/etcd write. Search servicekubelet, operationsyncPod— kubelet-side pod sync spans should be visible. - Cross-check the gap (scheduler) by comparing pod
metadata.creationTimestamp, thePodScheduledconditionlastTransitionTime, and the kubelet span start. The diff between those values quantifies the scheduler portion that we cannot trace natively. - Confirm Badger persistence:
docker compose restart jaeger, reload UI — earlier traces should still be present.
Notes / risks
- 100% sampling rate is fine for measurement; once we're done we should drop to e.g.
samplingRatePerMillion: 10000(1%) so storage doesn't explode. - Badger volume
./badger/on docker-29 will grow with span volume; size budget worth watching but not urgent. tracing.endpointis plaintext OTLP/gRPC and traverses the LAN — fine for a home lab but explicitly not encrypted.- If apiserver fails to come back after the manifest edit, the most common cause is a typo in the new flag; revert is straightforward — keep a copy of the original
/etc/kubernetes/manifests/kube-apiserver.yamlin/root/before editing.