Skip to content
LinkState
Go back

Autoscaling made the mesh unstable, or did it

Introduction to Outage Spikes During Rapid Upstream Scaling

Overview of Rapid Upstream Scaling

Rapid upstream scaling adds many backend instances (containers, VMs, serverless functions) in seconds or minutes to handle a traffic surge. While the control plane can provision instances quickly, the data plane often shows transient spikes in error rates or latency—visible as “outage spikes” on monitoring dashboards.

Common Causes of Outage Spikes

Observed spikes usually fall into four non‑exclusive categories:

  1. Handshake fan‑out – a burst of new TCP connections overwhelms the listener’s SYN‑queue or causes excessive SYN retransmits.
  2. Empty warm clusters – newly added instances lack warmed‑up caches (DB connection pools, JIT‑compiled code, OS page caches), so early requests suffer higher latency until the state is populated.
  3. Connection pool churn – clients create and destroy connections faster than pools can reuse sockets, leading to frequent full handshakes.
  4. Genuine application cold starts – the runtime or framework needs time to initialize (JVM class loading, interpreter startup, container image unpacking) before it can serve requests at peak throughput.

Each hypothesis yields a distinct signature in low‑level metrics; the sections below show how to detect them and which measurements disambiguate the competing explanations.


Handshake Fan‑Out

Definition and Explanation

When a load balancer or upstream service adds N new instances quickly, each instance begins accepting TCP connections. If clients open connections faster than the server can accept() them, the kernel listen backlog (set by net.core.somaxconn and the listen() backlog argument) fills. Excess SYN packets are dropped, triggering client SYN retransmits after the TCP retransmission timeout (≈1 s, then exponential backoff). Observable effects include a spike in SYN retransmits, increased connection‑establishment latency, and rises in ListenOverflows or ListenDrops counters.

Identifying Handshake Fan‑Out Issues

Metric / ToolWhat to Look ForTypical Threshold (indicative)
netstat -s or ss -sSYNs to LISTEN sockets dropped or ListenOverflows>0 /s during scaling event
TCP retransmits (tcpretrans)Sudden rise in retransmitted SYN packets>0.5 % of total SYNs
Client‑side connection latencyTime from SYN to SYN‑ACK > 200 ms (baseline < 20 ms)>2× baseline
eBPF tracepoint tcp_retransmit_synCount of SYN retransmits per listenerSpike correlated with instance add‑rate

If the spike aligns precisely with the moment new instances become listening (after the orchestrator marks them Ready) and disappears once the backlog drains, handshake fan‑out is a strong candidate.

Example CLI for Handshake Fan‑Out Troubleshooting

# 1. Check listen backlog setting
sysctl net.core.somaxconn
# Typical default: 128; increase for bursty workloads
sudo sysctl -w net.core.somaxconn=1024

# 2. Watch for drops in real time
watch -n 1 "cat /proc/net/snmp | grep -E 'ListenOverflows|ListenDrops'"

# 3. Capture SYN retransmits with bpftrace
sudo bpftrace -e '
tracepoint:tcp:tcp_retransmit_syn
{
    @[comm] = count();
}
interval:s:5
{
    print(@);
    clear(@);
}
'

# 4. Measure client‑side connection latency (curl)
curl -w "%{time_connect}\n" -o /dev/null -s http://backend.example.com/health

Concurrent rise and fall of ListenOverflows with the scaling event points to handshake fan‑out.


Empty Warm Clusters

Definition and Explanation

“Warm” means runtime caches—OS page cache for hot files, in‑memory application caches (Redis, Memcached, local LRU), JIT‑compiled code, or DB connection pools—are already populated. A new instance starts with these structures empty or cold; early requests must populate them, incurring extra latency (disk I/O, class loading, JIT compilation, cache misses). The effect is a temporary increase in request latency and CPU usage that decays as the caches warm.

Identifying Empty Warm Cluster Issues

Metric / ToolWhat to Look ForTypical Pattern
Page‑cache hit ratio (vmstat, pidstat -r)Low pgpgin/s relative to pgpgout/s on new instancesSpike in pgpgin during first 10‑30 s
Application‑level cache miss ratio (e.g., redis_stats hits/misses)Miss rate > 80 % initially, then decaysExponential decay with time constant τ
JIT compilation time (JVM -Xlog:gc*, -XX:+PrintCompilation)Burst of Compilation events shortly after startHigh CPU % in compiler threads
DB connection pool creation latencyPool create latency > baseline (often 10‑50 ms per conn)Spike in pool.create.time metric
CPU utilization profile (perf, eBPF)Increased time in vfs_read, ext4_file_read, java.lang.ClassLoader::loadClassCorrelates with request latency spike

If latency is highest on the first request to each new pod and drops sharply after a warm‑up period (often proportional to request rate), empty warm clusters are implicated.

Example CLI for Empty Warm Cluster Troubleshooting

# 1. Monitor page cache activity per container (cgroup v2)
cat /sys/fs/cgroup/<slice>/<container>/memory.stat | grep pgpgin
# Expect a burst of pgpgin when container starts

# 2. Enable JVM JIT logging (add to JAVA_OPTS)
-XX:+UnlockDiagnosticVMOptions -XX:+LogCompilation -XX:LogCompilationPath=/tmp/jit.log

# 3. Count JIT events
grep -c '^Compilation' /tmp/jit.log   # compilations per second

# 4. Application‑specific cache miss ratio (Prometheus example)
# Assuming metrics: myapp_cache_misses_total, myapp_cache_hits_total
sum(rate(myapp_cache_misses_total[30s])) /
  (sum(rate(myapp_cache_misses_total[30s])) + sum(rate(myapp_cache_hits_total[30s])))

# 5. Trace file‑read latency with bpftrace
sudo bpftrace -e '
tracepoint:syscalls:sys_enter_read
{
    @start[pid] = nsecs;
}
tracepoint:syscalls:sys_exit_read
/ @start[pid] /
{
    @lat[comm] = hist(nsecs - @start[pid]);
    delete(@start[pid]);
}
interval:s:5
{
    print(@lat);
    clear(@lat);
}
'

A high initial miss ratio that decays with a time constant matching the request rate points to empty warm clusters.


Connection Pool Churn

Definition and Explanation

Connection pools (client‑side or server‑side) reuse TCP sockets to avoid the cost of a full three‑way handshake and TLS negotiation. When the upstream scales rapidly, each new instance may be seen as a new endpoint by the client load balancer or service mesh, causing the client to create a fresh pool per instance. If the request rate per instance is low, sockets are frequently created and torn down, leading to churn. Churn manifests as:

Identifying Connection Pool Churn Issues

Metric / ToolWhat to Look ForTypical Indicator
ss -s (TCP states)High TIME_WAIT count relative to ESTABTIME_WAIT > 10× ESTAB during scaling
Client‑side connect() rate (eBPF tracepoint:syscalls:sys_enter_connect)Spike in connect calls per second> new‑instance‑count × target‑RPS
Ephemeral port exhaustion (`netstat -angrep :+wc -l`)Approaching limit of ip_local_port_range
TLS handshake time (if applicable)Increase in SSL_handshake latency> baseline × 2
Pool hit/miss ratio (exposed by client library)Miss ratio rising, hit ratio fallingMiss > 30 % during spike
net.ipv4.tcp_tw_reuse / tcp_tw_recycle effectsMay reduce TIME_WAIT but can cause spurious RSTsCheck kernel logs for TCP: time wait bucket table overflow

If the surge in connection establishment correlates with the creation of new upstream endpoints (rather than just overall traffic increase) and the client shows a high miss ratio in its pool, churn is likely.

Example CLI for Connection Pool Churn Troubleshooting

# 1. Watch TCP state distribution
watch -n 1 "ss -s | awk '/TCP:/ {print $0}'"

# 2. Count connect() syscalls per second (bpftrace)
sudo bpftrace -e '
tracepoint:syscalls:sys_enter_connect
{
    @[comm] = count();
}
interval:s:1
{
    print(@);
    clear(@);
}
'

# 3. Check ephemeral port usage
sysctl net.ipv4.ip_local_port_range
# Example output: 32768   60999
RANGE=$(sysctl -n net.ipv4.ip_local_port_range | awk '{print $1,$2}')
LOW=$(echo $RANGE | awk '{print $1}')
HIGH=$(echo $RANGE | awk '{print $2}')
USED=$(ss -tan | awk '{print $5}' | cut -d: -f2 | sort -n | uniq -c | awk '{sum+=$1} END{print sum}')
echo "used ports: $USED, range: $LOW-$HIGH"

# 4. Client‑side pool metrics (Prometheus example for Go's net/http client)
# Assuming metrics: go_http_client_pool_misses_total, go_http_client_pool_hits_total
sum(rate(go_http_client_pool_misses_total[1m])) /
  (sum(rate(go_http_client_pool_misses_total[1m])) + sum(rate(go_http_client_pool_hits_total[1m])))

# 5. TLS handshake latency (Envoy example)
# Envoy metric: server.tls_handshake_time
histogram_quantile(0.95, sum(rate(envoy_server_tls_handshake_time_bucket[5m])) by (le))

A high connect rate that matches the rate of new instance registration, coupled with a rising TIME_WAIT pool, points to connection pool churn.


Genuine Application Cold Starts

Definition and Explanation

A cold start occurs when the runtime environment must perform expensive initialization before it can process requests. Examples:

The latency is front‑loaded: the first request (or first few requests) experiences a delay that can be hundreds of milliseconds to several seconds, after which latency settles to a steady‑state value. Unlike empty warm clusters, the delay stems primarily from runtime or framework initialization, not missing caches.

Identifying Genuine Application Cold Start Issues

Metric / ToolWhat to Look ForTypical Pattern
Process start‑up time (from execve to main)Measured via perf trace -p <pid> -s sched:sched_process_exec or strace -T -e trace=execveSpike of > 200 ms on new instances
JVM class loading (-Xlog:class+load)Burst of Loaded events in first secondsHigh CPU in ClassLoader::loadClass
V8 isolate init (Node.js)process.uptime() near zero, high %CPU in v8::Isolate::InitializeCorrelates with first request latency
Go runtime init (GOTRACEBACK=crash)Spike in runtime·mstart and runtime·garbage collector initShort‑lived CPU burst
Container image pull/unpack (if using immutable images)containerd metrics: containerd_content_download_bytes_total spikeOccurs before process start
Request latency vs. instance agePlot latency against time since pod Ready; latency high only for age < T₀Clear cutoff

If latency correlates strictly with the age of the instance (not with request rate or cache state) and disappears after a deterministic warm‑up period (often ~1‑2 s for JVM, < 500 ms for Go), the cause is a genuine cold start.

Example CLI for Genuine Application Cold Start Troubleshooting

# 1. Measure execve-to-main latency with bpftrace
sudo bpftrace -e '
tracepoint:syscalls:sys_enter_execve
/str(args->filename) =~ /.*myapp.*//
{
    @start[pid] = nsecs;
}
tracepoint:syscalls:sys_exit_execve
/ @start[pid] /
{
    @lat[pid] = nsecs - @start[pid];
    delete(@start[pid]);
}
interval:s:5
{
    print(@lat);
    clear(@lat);
}
'

# 2. Enable JVM class‑loading trace (add to JAVA_OPTS)
-XX:+TraceClassLoading -XX:+TraceClassUnloading

# 3. View class‑loading stats for a running JVM
jcmd <pid> VM.classloader_stats

# 4. Node.js V8 startup snapshot (using --trace-init)
node --trace-init your_app.js

A latency spike that aligns with instance age and disappears after a short, predictable warm‑up window indicates a genuine application cold start.


Disambiguating the Competing Explanations

SymptomHandshake Fan‑OutEmpty Warm ClustersConnection Pool ChurnGenuine Cold Start
SYN retransmits / ListenOverflows↑↑ (correlates with instance add‑rate)–––
Connection latency (SYN‑SYN/ACK)↑ (backend side)–↑ (client side)–
Page‑cache miss / pgpgin burst–↑ (first 10‑30 s)––
Application cache miss ratio–↑ initially, then decays––
JIT compilation burst–↑ (early requests)––
connect() rate & TIME_WAIT––↑ (matches new endpoint creation)–
Ephemeral port exhaustion––↑ (approaches ip_local_port_range limit)–
Process start‑up time (execve→main)–––↑ (> 200 ms on new instances)
Runtime‑specific init traces (class loading, V8 isolate, Go runtime)–––↑ (burst in first seconds)
Latency vs. request rateLatency spikes with connection rate, independent of request rate per instanceLatency highest on first request per instance, decays with request rateLatency rises with connection creation rate, not request rateLatency highest on first request per instance, independent of request rate; drops after fixed time

**Key discrimin


Share this post on:

Previous Post
It Was Not the Forwarder, It Was CPU Throttling
Next Post
Sizing AF_XDP rings for short overload not averages