eBPF Run-Queue Latency: Hľadanie Off-CPU Bottlenecku
Ked flamegraph tvrdil, ze CPU je ok, eBPF mi ukazal iny pribeh. “CPU je na 30% ale p99 latencia je hrozná.” Problém: vaše thready trávia viac času čakaním v scheduler run-queue než skutočným vykonávaním. Tradičné CPU profilery ukazujú len on-CPU čas, chýba im skutočný bottleneck.
Prostredie: Linux s kernelom 5.x+, vysoko konkurenčné multi-threaded aplikácie, overcommitted kontajnery
Problém
Neviditeľný Čas Čakania
Tradičné profilovanie ukazuje:
┌─────────────────────────────────────────────────────────────┐
│ Funkcia │ CPU % │ Samples │
├─────────────────────┼────────┼──────────────────────────────┤
│ processRequest() │ 25% │ 2500 │
│ parseJSON() │ 15% │ 1500 │
│ dbQuery() │ 10% │ 1000 │
│ Celkom │ 50% │ 5000 │
└─────────────────────┴────────┴──────────────────────────────┘
"CPU je len 50% využité, dosť rezervy!"
Realita s off-CPU analýzou:
┌─────────────────────────────────────────────────────────────┐
│ Stav │ Čas % │ Kde │
├─────────────────────┼─────────┼─────────────────────────────┤
│ ON-CPU (vykonáva) │ 30% │ Váš kód beží │
│ OFF-CPU (run-queue) │ 45% │ Čaká na CPU! │
│ OFF-CPU (I/O wait) │ 15% │ Disk/sieť │
│ OFF-CPU (locks) │ 10% │ Mutex kontencia │
└─────────────────────┴─────────┴─────────────────────────────┘
"45% času strávených len čakaním na CPU!"
Kedy Run-Queue Latency Špičkuje
Scenáre ktoré spôsobujú run-queue čakanie:
1. Container CPU limity (najčastejšie):
┌─────────────────────────────────────────────┐
│ Container: 2 CPU limit, 8 threadov │
│ 6 threadov vždy čaká v run-queue │
│ CFS throttling pridáva viac queue času │
└─────────────────────────────────────────────┘
2. CPU overcommitment:
┌─────────────────────────────────────────────┐
│ 64 vCPU na 8 fyzických jadrách │
│ Context switches medzi všetkými VM │
│ Každý switch = run-queue čas │
└─────────────────────────────────────────────┘
3. NUMA misplacement:
┌─────────────────────────────────────────────┐
│ Thread naplánovaný na vzdialenom NUMA node │
│ Migrácia poskakuje medzi nodami │
│ Každá migrácia = re-queue │
└─────────────────────────────────────────────┘
Príčina
CFS Scheduler Správanie
// Linux CFS (Completely Fair Scheduler) udržiava per-CPU run queues
// Keď sa thread prebudí, ide do run queue
struct cfs_rq {
struct rb_root_cached tasks_timeline; // Red-black tree runnable taskov
u64 min_vruntime; // Minimálny virtual runtime
// ...
};
// Run-queue latency = čas od stania sa runnable po skutočné bežanie
// Toto je NEVIDITEĽNÉ pre perf/flamegraphs pokiaľ to špecificky nemeriaš
// Kľúčový insight: Thread s 1ms skutočnej CPU práce
// môže mať 10ms run-queue latencie v preťažených systémoch
Container CFS Bandwidth Throttling
# Kontajnery používajú CFS bandwidth control
# cpu.cfs_quota_us / cpu.cfs_period_us = CPU limit
# Príklad: 2 CPU limit
/sys/fs/cgroup/cpu/container/cpu.cfs_quota_us: 200000 # 200ms
/sys/fs/cgroup/cpu/container/cpu.cfs_period_us: 100000 # per 100ms
# Ak tvojich 8 threadov sa pokúsi použiť 8 CPU času za 100ms:
# Dostanú 200ms CPU času, potom THROTTLED pre zvyšok periódy
# Throttled thready sedia v run-queue, pridávajú latenciu
# Skontroluj throttling:
cat /sys/fs/cgroup/cpu/container/cpu.stat
# nr_throttled: 15234 <- Koľkokrát throttled
# throttled_time: 892347123 <- Nanosekundy strávené throttled
Diagnostika
Krok 1: Skontroluj Run-Queue Dĺžku
# Jednoduchá kontrola: runnable procesy per CPU
sar -q 1 5
# runq-sz = priemerná run queue dĺžka
# Ak runq-sz > num_cpus konzistentne, máš queuing
# Viac detailov s vmstat
vmstat 1
# r stĺpec = procesy čakajúce na run time
# Malo by byť <= počet CPU
# Per-CPU pohľad
mpstat -P ALL 1
# Skontroluj %idle - ak nízke ale myslíš že máš rezervu, je to queuing
Krok 2: eBPF Run-Queue Latency Histogram
# Pomocou bcc-tools: runqlat
sudo runqlat -m 10
# Ukazuje histogram run-queue latencie v milisekundách
# Zdravé: väčšina samples < 1ms
# Problém: významné samples > 10ms
# Príklad výstupu (problematický):
msecs : count distribution
0 -> 1 : 1523 |************** |
2 -> 3 : 892 |******** |
4 -> 7 : 2341 |********************** | <- Príliš veľa!
8 -> 15 : 1876 |****************** | <- Oveľa viac!
16 -> 31 : 543 |***** |
32 -> 63 : 121 |* |
Krok 3: Per-Process Run-Queue Čas
# runqlat s process filtrom
sudo runqlat -p $(pgrep java) -m 10
# Alebo použi runqslower pre individuálne eventy
sudo runqslower 10000
# Ukazuje každý prípad kde run-queue wait > 10ms
# Výstup: TIME COMM PID LAT(us)
# 12:34:56 java 1234 15234
Krok 4: Off-CPU Flame Graph
# Zachyť off-CPU stacky s bcc
sudo offcputime -df -p $(pgrep myapp) 30 > out.stacks
# Vygeneruj flame graph
./flamegraph.pl --color=io --title="Off-CPU Time" out.stacks > offcpu.svg
# Hľadaj:
# - schedule() v kernel stackoch = run-queue wait
# - futex_wait = lock kontencia
# - io_schedule = I/O wait
Riešenie
Možnosť 1: Správne Dimenzuj Container CPU Limity
# PRED: Limit ktorý spôsobuje konštantný throttling
resources:
limits:
cpu: "2" # 2 CPU
requests:
cpu: "2"
# S 8 worker threadmi, 6 vždy čaká
# PO: Zladuj limit so skutočným paralelizmom
resources:
limits:
cpu: "4" # Povoľ viac paralelného vykonávania
requests:
cpu: "2" # Stále požaduj 2 pre scheduling
# Alebo zníž paralelizmus aby zodpovedal limitu:
# Nastav thread pool size = CPU limit
WORKER_THREADS=2
Možnosť 2: Vylaď CFS Parametre
# Zvýš CFS periódu pre menej časté throttling
# (obchoduje variance latencie za throughput)
# Per-container (Kubernetes toto priamo neexponuje)
echo 1000000 > /sys/fs/cgroup/cpu/container/cpu.cfs_period_us
# System-wide tuning
sysctl kernel.sched_min_granularity_ns=3000000
sysctl kernel.sched_wakeup_granularity_ns=4000000
Možnosť 3: CPU Pinning pre Latency-Kritické Thready
// Pripni kritické thready na špecifické CPU
import "golang.org/x/sys/unix"
func pinToCPU(cpuID int) error {
var mask unix.CPUSet
mask.Set(cpuID)
return unix.SchedSetaffinity(0, &mask)
}
// Latency-kritický handler pripnutý na dedikovaný CPU
go func() {
pinToCPU(0) // CPU 0 rezervovaný pre toto
for req := range criticalRequests {
handleCritical(req)
}
}()
# Kubernetes: Použi CPU manager pre garantované pinning
kubelet config:
cpuManagerPolicy: static
# Pod spec pre guaranteed QoS (umožňuje pinning)
resources:
limits:
cpu: "2"
memory: "4Gi"
requests:
cpu: "2"
memory: "4Gi"
Možnosť 4: NUMA-Aware Scheduling
# Skontroluj NUMA topológiu
numactl --hardware
# Pripni proces na jeden NUMA node
numactl --cpunodebind=0 --membind=0 ./myapp
# V Kubernetes použi topology manager
kubelet config:
topologyManagerPolicy: single-numa-node
Monitoring
groups:
- name: scheduler-latency
rules:
- alert: HighRunQueueLatency
expr: |
histogram_quantile(0.99,
rate(scheduler_runq_latency_bucket[5m])
) > 0.01
for: 5m
labels:
severity: warning
annotations:
summary: "p99 run-queue latencia > 10ms"
- alert: CPSThrottlingHigh
expr: |
rate(container_cpu_cfs_throttled_seconds_total[5m]) > 0.1
for: 5m
labels:
severity: warning
annotations:
summary: "Container CPU throttled > 10% času"
Checklist
## Run-Queue Latency Investigation
### Symptómy
- [ ] Nízke CPU využitie ale vysoká latencia
- [ ] Latency špičky nekorelujú s trafficom
- [ ] Tradičné profilery ukazujú "nič zlé"
- [ ] Výkon degraduje pod miernou záťažou
### Diagnostika
- [ ] Skontroluj run-queue dĺžku s sar/vmstat
- [ ] Použi runqlat pre latency histogram
- [ ] Vygeneruj off-CPU flame graph
- [ ] Skontroluj CFS throttling štatistiky
### Riešenia
- [ ] Správne dimenzuj container CPU limity
- [ ] Zníž thread count aby zodpovedal CPU limitu
- [ ] Zváž CPU pinning pre kritické cesty
- [ ] Zapni NUMA-aware scheduling
- [ ] Monitoruj CFS throttling metriky
Záver
Lekcia: CPU profilery ukazujú len čo sa deje kým tvoj kód beží. Off-CPU analýza odhaľuje kde tvoj kód čaká - a run-queue čas je často skrytá väčšina request latencie v kontajnerizovaných prostrediach.
Kľúčové nástroje:
runqlat- Histogram run-queue čakaníoffcputime- Off-CPU stack tracesrunqslower- Individuálne dlhé čakania- CFS throttle metriky - Container-špecifické queuing
Súvisiace články
- Java Profiling v Hardened K8s - Profilovanie v kontajneroch
- Go Timer Heap Pressure - Ďalšia latency investigácia
Súvisiace články
eBPF Off-CPU Analýza: Nájdenie Latencie Ktorú Metriky Nevidia
CPU je na 20% ale latencia je 500ms. Štandardné profilery neukazujú nič. Appka čaká, nepočíta. Ukážem ako použiť eBPF na nájdenie na čo čaká.
TCP TIME_WAIT Vyčerpanie Portov: Keď Connection Pooling Nestačí
Služba sa nemôže pripojiť k databáze - 'cannot assign requested address'. Príčina: ephemeral porty vyčerpané tisíckami socketov v TIME_WAIT stave.
tcpdump vidí SYN, ale služba timeoutuje: pasca listen backlogu
Klienti timeoutujú, tcpdump ukazuje SYN (niekedy aj SYN-ACK), ale aplikácia nič neloguje. Častý vinník: Linux listen/accept fronty, ktoré sa pri load-e alebo CPU starvation preplnia.
Linux ARP Cache Zastarané Záznamy: Blackhole Traffic Po Failoveri
Traffic ide na starý server po failoveri. Príčina: Linux ARP cache drží MAC adresu zlyhajúceho nodu, posiela pakety na nedosiahnuteľnú destináciu minúty.
Citujte tento článok
Ak na článok odkazujete, pridajte pôvodnú URL a uveďte autora.