Linux CPU Spike Troubleshooting: From Alert to Root Cause
A comprehensive guide to diagnosing Linux server CPU spikes using systematic evidence chains: verify alert scope, identify processes and threads, distinguish user/kernel/interrupt time, analyze memory and I/O pressure, leverage perf and language runtimes, check container quotas, correlate deployments and cron jobs, and validate fixes with repeatable measurements.
Confirm Alert Scope
Monitoring CPU metrics can represent host utilization, per-core percentage, process cumulative time, container quota ratio, or load average. On an 8-core host a single saturated thread shows ~12.5% host utilization but 100% in top 's process view. Record UTC/local time, alert window, core count, and monitoring formula.
date --iso-8601=seconds
timedatectl status
nproc
lscpu
uptime load averagereflects runnable + uninterruptible task queue length, not CPU percentage. High load may stem from disk I/O blocking; correlate with run queue and CPU states.
Observe overall CPU breakdown: mpstat -P ALL 1 10 Focus on %usr, %sys, %iowait, %irq, %soft, %steal, %idle. High %iowait means CPU idle with pending I/O, not compute exhaustion; high %steal on VMs indicates host contention.
Use vmstat for run queue, paging, context switches:
vmstat 1 10 rconsistently above logical CPUs with low idle supports CPU queuing; high b and wa point to I/O; non-zero si/so indicates paging pressure. Single samples are insufficient.
Find the Consuming Process
Sample per-process user/kernel/I/O/context-switch: pidstat -u -r -d -w 1 10 High %usr suggests computation, serialization, regex, or GC; high %system warrants syscall, network, kernel path inspection; many voluntary/involuntary context switches may indicate locks, excess threads, or scheduler contention.
Stable process snapshot sorted by CPU:
ps -eo pid,ppid,user,stat,psr,pcpu,pmem,etimes,comm,args \
--sort=-pcpu | head -n 30 ps %CPUis lifetime average, possibly masking short spikes; complement with pidstat interval sampling. Sanitize command lines before saving.
Identify service ownership and cgroup:
systemctl status <service> --no-pager
systemctl show <service> \
-p MainPID -p ControlGroup -p CPUUsageNSec -p CPUQuotaPerSecUSec
cat /proc/<pid>/cgroupIf MainPID differs from high-CPU PID, it may be a worker child, container, or external process. Do not kill based on name alone.
Drill Down to Threads
Multi-threaded processes require thread-level identification: pidstat -t -u -p <pid> 1 10 Only persistently high-CPU TIDs merit further sampling; transient threads may have exited. Record timestamps.
View thread names and scheduler state:
ps -L -p <pid> \
-o pid,tid,psr,stat,pcpu,comm,wchan:32 --sort=-pcpu | head -n 30
cat /proc/<pid>/task/<tid>/status wchanis meaningful only for sleeping threads; running hot threads show -. Application-set thread names can directly map to thread pools, GC, or business workers.
Java thread dumps use hex nid. Convert TID:
printf '0x%x
' <tid>
jcmd <pid> Thread.print -l > <dir>/threads-$(date +%s).txtUse JDK matching target JVM, run as same user. Thread dumps briefly enter safepoint and contain business stacks; control frequency and file permissions.
Capture three consecutive thread stacks to confirm stable hotspot:
#!/usr/bin/env bash
set -euo pipefail
PID="<pid>"
OUT_DIR="<dir>"
install -d -m 0700 "${OUT_DIR}"
for i in 1 2 3; do
jcmd "${PID}" Thread.print -l > "${OUT_DIR}/thread-${i}.txt"
if (( i < 3 )); then sleep 5; fi
doneSame thread RUNNABLE in same business function across multiple dumps constitutes code hotspot evidence; a single dump cannot rule out sampling chance.
Distinguish User, Kernel, and Interrupt
When system time is high, examine syscall summary. strace adds overhead; limit to 20 seconds, test on low-risk instance first: sudo timeout 20 strace -f -c -p <pid> Many futex calls may indicate lock waits or thread sync, not automatically root cause; many read/write/sendto/recvfrom need file descriptor, network, and stack correlation.
Check interrupt distribution:
watch -n 1 'cat /proc/interrupts | head -n 30'
cat /proc/softirqsIf a NIC queue interrupt grows rapidly on one core while %soft is high, inspect RSS/RPS, driver, traffic, and drops. Exit watch with Ctrl-C.
View NIC stats and softirq pressure:
ip -s link show dev <nic>
ethtool -S <nic> | grep -Ei 'drop|miss|error|timeout|queue'
sar -n DEV,EDEV 1 10Driver stat fields vary; use actual output. Watch error counters as deltas, not absolute values.
Check irqbalance and interrupt affinity:
systemctl status irqbalance --no-pager
grep -H . /proc/irq/<irq>/smp_affinity_list
ethtool -l <nic> <irq>from /proc/interrupts. Manual affinity changes may conflict with irqbalance; treat as performance policy change, backup, canary, and prepare rollback.
Check Memory, I/O, and Reclaim Illusions
Memory pressure forces CPU into reclaim, compaction, or swap:
free -h
sar -B 1 10
sar -W 1 10
cat /proc/pressure/memoryRising pgscan, pgsteal, swap in/out, and memory PSI together support reclaim pressure. High cache alone is not memory shortage; Linux actively uses page cache.
Check I/O latency and queues:
iostat -xz 1 10
cat /proc/pressure/ioHigh %util on parallel devices doesn't necessarily mean saturation; combine with await, queue depth, throughput, device type. If CPU alert coincides with high iowait, optimizing CPU code may be ineffective.
Examine kernel logs for OOM, hung task, hardware errors:
sudo journalctl -k --since '-30 minutes' --no-pager \
| grep -Ei 'oom|out of memory|hung task|soft lockup|hard lockup|mce|thermal|throttl|rcu'Log timestamps must align with alert window. Thermal throttling can make same workload consume more CPU time; check hardware sensors and platform monitoring.
Use perf to Find Function Hotspots
perfrequires matching kernel tools, symbols, and permissions. Verify version and kernel limits:
perf --version
uname -r
sysctl kernel.perf_event_paranoid kernel.kptr_restrictDo not permanently lower security limits for sampling. If policy forbids perf, use language runtime profilers or reproduce in pre-production.
30-second, 99Hz call-stack sampling:
sudo perf record -F 99 -g -p <pid> -- sleep 30
sudo perf report --stdio --sort comm,dso,symbol | head -n 8099Hz reduces sync bias with common timer frequencies. perf.data can be large with symbols; collect in controlled directory. Hot function share must align with process/thread CPU contemporaneous evidence.
Count only syscalls and scheduler events:
sudo perf stat -p <pid> \
-e task-clock,context-switches,cpu-migrations,page-faults,cycles,instructions \
-- sleep 20Low IPC (instructions/cycles) may indicate cache misses, branch mispredicts, or stalls, but cannot alone determine root cause. Virtualized counters may be constrained.
If scheduler latency suspected, briefly record sched events:
sudo perf sched record -- sleep 10
sudo perf sched latency --sort max | head -n 40This is system-wide; busy hosts incur higher overhead and data volume. Canary first; do not run continuously during sustained peaks.
Language Runtime Specific Evidence
Java: examine GC and compilation activity:
jstat -gcutil <pid> 1000 10
jcmd <pid> VM.flags
jcmd <pid> Compiler.queue
jcmd <pid> GC.heap_info jstatfields vary by GC algorithm. Frequent Full GC, high GC thread CPU, and heap occupancy changes must co-occur to support GC root cause; do not increase heap just because GC appears in logs.
Short Java Flight Recorder for production diagnosis, but has overhead; verify JDK version and licensing:
jcmd <pid> JFR.start \
name=cpu-investigation settings=profile duration=60s \
filename=<dir>/cpu-investigation.jfr
jcmd <pid> JFR.check settings=profilehas higher overhead than default. JFR files may contain class names, paths, business events; secure transfer and storage.
Python: py-spy if installed and ptrace permitted per org policy. Limit sampling:
sudo timeout 30 py-spy top --pid <pid> --rate 50Do not install unknown binaries in production. If tool unavailable, prefer built-in profiling or reproduce in isolation.
Container and cgroup Throttling
High CPU inside container requires quota verification. cgroup v2 cpu.stat shows usage and throttling:
CGROUP_PATH="$(systemctl show -p ControlGroup --value <service>)"
sudo cat "/sys/fs/cgroup${CGROUP_PATH}/cpu.max"
sudo cat "/sys/fs/cgroup${CGROUP_PATH}/cpu.stat"
sudo cat "/sys/fs/cgroup${CGROUP_PATH}/cpu.pressure" cpu.max maxmeans no quota; otherwise "quota period" in microseconds. Watch nr_throttled and throttled_usec deltas; high throttling doesn't imply high host CPU.
Kubernetes resource config and current usage:
kubectl -n <ns> get pod <pod> -o jsonpath='{.spec.containers[*].resources}{"
"}'
kubectl -n <ns> top pod <pod> --containers
kubectl -n <ns> describe pod <pod> kubectl toprelies on Metrics Server, provides recent aggregates, unsuitable for reconstructing ended second-level spikes. Need Prometheus historical metrics.
Container CPU usage query (adjust to actual exporter/cAdvisor metrics):
sum by (namespace, pod, container) (
rate(container_cpu_usage_seconds_total{
namespace="<ns>",pod="<pod>",container!=""}
[5m])
)Result in CPU cores. For relative limit, join with kube_pod_container_resource_limits{resource="cpu"} and handle missing limits.
Throttling ratio (check actual exposed metrics):
sum(rate(container_cpu_cfs_throttled_periods_total{namespace="<ns>",pod="<pod>"}[5m]))
/
sum(rate(container_cpu_cfs_periods_total{namespace="<ns>",pod="<pod>"}[5m]))Metric may vary across cgroup v2/cAdvisor versions; verify first. Before raising limit, confirm node allocatable CPU and neighbor impact.
Correlate Deployments, Traffic, and Cron Jobs
CPU rises often coincide with releases, config changes, cache invalidation, batch jobs, or traffic shifts. Check systemd logs and service start times:
systemctl show <service> -p ActiveEnterTimestamp -p ExecMainStartTimestamp -p MainPID
sudo journalctl -u <service> --since '-1 hour' --no-pager
sudo journalctl --since '-1 hour' _PID=<pid> --no-pager | tail -n 200Log errors must be correlated with frequency and time; an error can be both cause and symptom of overload.
Check timers:
systemctl list-timers --all
sudo grep -RIn --exclude='*.bak' . /etc/cron.d /etc/cron.hourly 2>/dev/null
sudo crontab -lBackups, compression, log rotation, AV scans, stats jobs may cause periodic CPU. Do not disable without confirming business purpose and catch-up plan.
Check recent installs and config changes:
sudo dnf history list | head -n 20
sudo find /etc -xdev -type f -mmin -120 -printf '%TY-%Tm-%Td %TH:%TM %p
' \
2>/dev/null | sortFile timestamps only prove modification occurred, not causation. Root cause requires diff content, temporal correlation, and reproducible behavior.
Automated Scene Collection
Script collects 30-second baseline evidence without modifying system state; command lines and logs may contain sensitive data, output directory permissions 700:
#!/usr/bin/env bash
set -euo pipefail
OUT_DIR="<dir>/cpu-$(date -u +%Y%m%dT%H%M%SZ)"
DURATION=30
install -d -m 0700 "${OUT_DIR}"
date --iso-8601=seconds > "${OUT_DIR}/time.txt"
uptime > "${OUT_DIR}/uptime.txt"
lscpu > "${OUT_DIR}/lscpu.txt"
ps -eo pid,ppid,user,stat,psr,pcpu,pmem,etimes,comm,args --sort=-pcpu \
> "${OUT_DIR}/processes.txt"
mpstat -P ALL 1 "${DURATION}" > "${OUT_DIR}/mpstat.txt" &
p1=$!
pidstat -u -r -d -w 1 "${DURATION}" > "${OUT_DIR}/pidstat.txt" &
p2=$!
vmstat 1 "${DURATION}" > "${OUT_DIR}/vmstat.txt" &
p3=$!
wait "${p1}" "${p2}" "${p3}"
journalctl -k --since '-15 minutes' --no-pager > "${OUT_DIR}/kernel.txt"
printf 'saved=%s
' "${OUT_DIR}"Parallel sampling adds slight load; if host extremely busy, shorten duration or collect single items first. Script excludes perf, strace, packet capture due to separate risk assessment.
Compute checksums for integrity:
find <dir> -maxdepth 1 -type f -print0 \
| sort -z \
| xargs -0 sha256sum > <dir>/SHA256SUMSReplace <dir> with exact generated directory to avoid scanning unrelated data.
Remediation Choices and Risks
If single-instance code hotspot confirmed, drain from load balancer, preserve scene, analyze on other instances if capacity allows. If all instances rise with traffic, prioritize rate limiting, caching, scaling; if downstream slowness causes retry storms, limit retries and restore dependency. Raising CPU quota only relieves throttling, cannot fix infinite loops.
Killing process is high-risk: interrupts requests, loses in-memory state, may be immediately restarted by systemd. Verify replicas, health checks, graceful stop timeout, persistent state. Prefer service manager:
sudo systemctl show <service> \
-p MainPID -p Restart -p TimeoutStopUSec -p KillMode
sudo systemctl stop <service>
sudo systemctl status <service> --no-pagerBefore execution, complete traffic drain and confirm capacity. After, verify requests, logs, ports, dependencies. Rollback is systemctl start <service> and re-add to traffic; if start fails, check journal, do not loop restart.
Temporary systemd CPU quota protects host but adds latency to service; must canary:
sudo systemctl set-property --runtime <service> CPUQuota=200%
systemctl show <service> -p CPUQuotaPerSecUSec
cat /proc/pressure/cpu 200%≈ two CPU cores quota. --runtime reverts on reboot, suitable for emergency validation. Rollback with
systemctl set-property --runtime <service> CPUQuota=infinity, then recheck service latency and host CPU.
If rolling back app version, save current version, config diff, thread stacks, profiler results. Kubernetes rollback example:
kubectl -n <ns> rollout history deployment/<deploy>
kubectl -n <ns> rollout undo deployment/<deploy> --to-revision=<rev>
kubectl -n <ns> rollout status deployment/<deploy> --timeout=10m
kubectl -n <ns> get pods -l app=<label> -o wideRollback recreates pods; check PDB, capacity, DB migration compatibility, irreversible data changes. If version includes non-backward-compatible schema migration, do not rollback binary alone.
Validate Root Cause, Not Just "CPU Dropped"
Sound conclusion must answer: which host/container, which process/thread, user vs kernel, which function/wait path, start time, correlated change, which metrics moved as expected after action. Example: "Java worker high CPU" is not root cause; "three thread dumps and 30s perf both point to same parser function, call volume rose after release, rollback restored function sample share and CPU simultaneously" is complete evidence chain.
After fix, repeat collection of mpstat, pidstat, thread/function evidence, business latency covering original peak window. Confirm pressure not shifted to I/O, DB, downstream, other nodes. Retain commands, time windows, versions, config diffs, rollback results to turn incident into alert thresholds, capacity planning, release validation rules.
When Process Gone or Spike Ended
After short spike, top and /proc/<pid> cannot reconstruct scene. Rely on historical monitoring, journald, process accounting, pre-deployed low-overhead continuous sampling. Do not dismiss alert based on current normal state; do not treat single monitoring point as full duration.
If sysstat enabled, query historical sar files:
sar -u -f /var/log/sa/sa<day> -s <start> -e <end>
sar -q -f /var/log/sa/sa<day> -s <start> -e <end>
sar -w -f /var/log/sa/sa<day> -s <start> -e <end> <day>usually two-digit day; path/format depend on sysstat config/version; use ls /var/log/sa and sar --help to confirm. Historical sampling granularity (e.g., 10 min) cannot prove 20-second spike internals.
Check if process killed by OOM, systemd, or human:
sudo journalctl --since '<start>' --until '<end>' --no-pager \
| grep -Ei 'killed process|oom-kill|Main process exited|Stopped|Started|sudo|session opened'
last -x | head -n 30 last -xfrom wtmp, may be rotated. Human attribution needs sudo/audit logs and change records, not just login time guesses.
If process accounting enabled, acct / psacct records exited process command and cumulative CPU, but enabling adds disk writes and only provides data after activation:
systemctl status psacct --no-pager
lastcomm | head -n 50
sa --sort-cpu | head -n 30Tool installation, service name, log retention vary by distro. Cannot reconstruct thread stacks or function hotspots; only helps discover short-lived high-CPU commands.
Scheduler, NUMA, and CPU Frequency
On multi-socket NUMA, cross-node memory access increases latency and CPU cycles. Inspect topology and allocation before pinning:
numactl --hardware
numastat -p <pid>
taskset -cp <pid>High remote memory ratio is a clue; still need application allocation and CPU affinity correlation. Manual taskset may break service manager, container orchestration, or irq distribution; apply via formal config, canary, record original values.
Check CPU frequency and governor:
cpupower frequency-info
grep -H . /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor 2>/dev/null | headSome VMs don't expose cpufreq. Frequency drops may come from power policy, thermal, or platform limits; changing governor alters power/thermal design, requires platform team evaluation.
Scheduler latency may also come from real-time priority tasks. List policies and priorities:
ps -eLo pid,tid,cls,rtprio,pri,ni,psr,stat,pcpu,comm \
--sort=-rtprio,-pcpu | head -n 40 FF/ RR real-time threads misconfigured can starve normal tasks. Lowering real-time priority is behavioral change; confirm process purpose, watchdogs, vendor recommendations first.
Steal Time in VMs
VM %steal persistently rising means vCPU wanted to run but hypervisor descheduled. Adding replicas inside guest may worsen contention. Evidence must include mpstat / sar steal time, co-located VM trends, cloud platform host metrics, instance spec limits.
sar -u ALL 1 10
grep -E '^cpu ' /proc/stat
systemd-detect-virtFor burstable instances, check CPU credit/credit balance; dynamic metric from cloud monitoring. Without platform evidence, list steal as suspicion, cannot assert host oversubscription.
Packet Rate, Not Bandwidth
Small-packet high PPS can drive softirq CPU high while bandwidth low. Compare packet rate, byte rate, drops, softnet stats:
sar -n DEV 1 10
awk '{for (i=1;i<=NF;i++) printf "%s%s", strtonum("0x" $i), (i==NF?ORS:OFS)}' \
/proc/net/softnet_stat | head /proc/net/softnet_statfield meanings per kernel docs; second column often used for drop cumulative; must watch delta and verify against current kernel. Above awk uses gawk strtonum; if unavailable, keep raw hex.
Limit 20-second packet header capture for protocol distribution:
sudo timeout 20 tcpdump -ni <nic> -s 128 -c 20000 \
-w <dir>/cpu-spike.pcapPacket capture involves sensitive data and consumes CPU/disk. -s 128 limits per-packet length, -c limits count; verify compliance, space, host headroom before execution. Delete or archive per retention policy after analysis.
Security Events and Anomalous Processes
Unknown high-CPU process could be runaway script or security incident. Do not immediately delete binary or wipe logs. First isolate risk, preserve hash, path, parent process, network connections, open files, then hand to security process.
sudo readlink -f /proc/<pid>/exe
sudo sha256sum /proc/<pid>/exe
sudo tr '\0' ' ' < /proc/<pid>/cmdline; echo
sudo lsof -nP -p <pid> | head -n 100
sudo ss -tpn | grep "pid=<pid>,"Command line and connections may be sensitive. If process image deleted, /proc/.../exe shows (deleted); still do not terminate without forensics. If lateral movement or crypto-mining suspected, isolate network per incident response and preserve evidence.
Pre-position Observability for Next Spike
Second-level spikes need second-or-finer metrics. Prometheus host CPU query per actual node_exporter metrics:
100 * (1 - avg by (instance) (
rate(node_cpu_seconds_total{mode="idle"}[2m])
))Short window reacts fast but noisy; alert can require high utilization AND high run queue OR business latency degradation. Multi-core process hotspots need process exporter, eBPF profiler, or runtime metrics.
Record CPU pressure (metric names per actual exporter):
rate(node_pressure_cpu_waiting_seconds_total[5m])PSI reflects time tasks wait for CPU, closer to "is CPU really scarce" than raw utilization. Different node_exporter versions may not expose it; check /metrics.
Production can run low-overhead collection script via systemd timer, but not heavy commands every second. Service and timer example:
# /etc/systemd/system/cpu-snapshot.service
[Unit]
Description=Collect a lightweight CPU snapshot
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/cpu-snapshot
Nice=10
IOSchedulingClass=idle # /etc/systemd/system/cpu-snapshot.timer
[Unit]
Description=Run CPU snapshot every minute
[Timer]
OnBootSec=2min
OnUnitActiveSec=1min
AccuracySec=5s
Persistent=false
[Install]
WantedBy=timers.targetBefore deploy, review script for permissions, disk quota, rotation, sensitive data; canary on single host. Disable rollback with systemctl disable --now cpu-snapshot.timer; does not auto-delete collected files, handle per retention policy.
Practical Triage Order
Facing ongoing high CPU: first save timestamp and overall state; use pidstat to find process/thread; parallel confirm business impact; based on usr/sys/soft/steal choose perf, strace, interrupt, or platform path; then correlate deployments and traffic. Facing ended spike: prioritize historical metrics, journal, release records, accounting data. Facing unresponsive host: first protect business via rate limiting, drain, or failover, then preserve scene on remaining healthy replicas.
Any root cause report must separate facts, inferences, and items to verify. Facts are command, log, metric results in explicit time window; inferences are mechanisms they support; verification is expected changes from fix or reproduction experiment. This avoids unactionable conclusions like "CPU high because traffic high".
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Raymond Ops
Linux ops automation, cloud-native, Kubernetes, SRE, DevOps, Python, Golang and related tech discussions.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
