Späť na blog

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 traces
  • runqslower - Individuálne dlhé čakania
  • CFS throttle metriky - Container-špecifické queuing

Súvisiace články

Súvisiace články

Citujte tento článok

Ak na článok odkazujete, pridajte pôvodnú URL a uveďte autora.

Michal Drozd. "eBPF Run-Queue Latency: Hľadanie Off-CPU Bottlenecku". https://www.michal-drozd.com/sk/blog/ebpf-runqueue-latency-offcpu/ (Publikované 17. februára 2025).