When scaling reverse proxy edge nodes to large multi-core bare-metal servers (e.g. 64 or 128 physical CPU cores), traditional socket listening models experience severe lock contention.
During a 100,000 QPS HTTPS load spike, CPU utilization across Nginx worker processes showed severe imbalance. Worker Core 0 was pegged at 100% CPU waiting on kernel socket accept locks, while adjacent worker cores sat underutilized.
# perf top trace on 64-Core Proxy Host 38.40% [kernel] _raw_spin_lock_bh (tcp_v4_rcv / inet_csk_accept) 22.10% [kernel] __lock_text_start (accept_mutex lock contention) 14.20% nginx ngx_event_accept
In traditional Nginx setups, all worker processes share a single listening socket. When a TCP SYN packet arrives, the kernel wakes up all sleeping workers to contend for the socket lock (the classic Thundering Herd Problem). Even with Nginx's accept_mutex enabled, workers spend precious CPU cycles acquiring spinlocks instead of processing HTTP frames.
[ Incoming TCP SYN Packets ]
│
▼
┌───────────────────────────────────────────────────────────┐
│ Linux Kernel 4-Tuple Hash (IP:Port + Client IP:Port) │
└──────────────┬─────────────────────────────┬──────────────┘
│ (Direct Queue Routing) │
▼ ▼
┌──────────────────────┐ ┌──────────────────────┐
│ Worker 1 Socket │ │ Worker 2 Socket │
│ (Dedicated Backlog) │ │ (Dedicated Backlog) │
└──────────────────────┘ └──────────────────────┘
(ZERO Lock Contention) (ZERO Lock Contention)
The Linux SO_REUSEPORT socket option allows every Nginx worker process to create its own independent listening socket bound to identical IP and port numbers. The Linux kernel uses a 4-tuple hash algorithm to distribute incoming SYN packets directly into per-worker receive queues with ZERO spinlock contention!
Configure SO_REUSEPORT socket sharding and disable legacy accept_mutex inside your Nginx configuration:
events {
worker_connections 20480;
use epoll;
# CRITICAL: Disable accept_mutex as kernel handles socket sharding
accept_mutex off;
}
http {
server {
# Enable kernel-level socket sharding per worker
listen 80 reuseport;
listen 443 ssl http2 reuseport;
server_name highqps.zhabrosima.com;
ssl_certificate /etc/nginx/certs/zhabrosima.crt;
ssl_certificate_key /etc/nginx/certs/zhabrosima.key;
location / {
proxy_pass http://backend_cluster;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
}
}
Verify that every worker process owns a dedicated listening socket using CLI tools:
ss)# Display independent listen sockets on port 443 and worker PIDs
ss -tlnp | grep :443
# Key Check: Verify multiple PIDs own separate sockets on the same IP:Port
When restarting Nginx or reloading configurations (nginx -s reload), SO_REUSEPORT sockets belonging to dying worker processes can drop pending TCP SYN connections in the backlog ring.
BPF_MAP_TYPE_REUSEPORT_SOCKARRAY eBPF program to route TCP SYN packets seamlessly away from terminating sockets to active replacement workers.worker_shutdown_timeout 15s; to allow old workers to finish draining active HTTP/2 streams before closing sharded sockets.We conducted a 100,000 QPS benchmark on a 64-core AMD EPYC server:
| Socket Architecture | Throughput (QPS) | p99 Latency | Spinlock CPU Overhead |
|---|---|---|---|
| Single Socket (accept_mutex) | 42,100 QPS | 142.50 ms | 38.4% CPU Spinlock |
| SO_REUSEPORT Socket Sharding | 118,400 QPS | 4.20 ms | 0.0% Spinlock |
| Performance Impact | +181.2% Throughput Gain | -97.0% Latency Drop | 100% Lock Elimination |
stddev(rate(process_cpu_seconds_total[5m])) by (job)rate(node_netstat_TcpExt_ListenOverflows[5m])