Introduction
High‑frequency scraping of network interface statistics and TX/RX queues forces the kernel to copy counters from per‑CPU or per‑queue structures into user space on each poll. Each poll incurs CPU cycles for system‑call entry/exit, copying of 64‑bit counters, and any required locking (e.g., u64_stats_sync). It also generates memory traffic as counters are read from cache lines that may be dirty due to NAPI processing, and can cause contention if the scraper runs on the same core that handles NAPI poll loops. The tax scales roughly linearly with poll frequency until the overhead saturates a core or exhausts memory bandwidth, at which point packet forwarding begins to drop or latency spikes.
Benchmarking provides a repeatable method to isolate the cost of the scraping loop from the cost of actual traffic, yields metrics such as CPU % per kHz of poll and extra memory bandwidth per poll, and helps decide when to switch from polling to event‑driven mechanisms (netlink notifications, eBPF maps, XDP‑driven counters).
Setting Up the Benchmarking Environment
Choosing the Right Tools and Software
| Purpose | Tool | Reason |
|---|---|---|
| Traffic generator | pktgen (kernel) or trex (userspace) | Generates deterministic, high‑rate UDP/TCP flows with controllable burst size. |
| Interface/queue stats scraper | Custom C program using ethtool -S, sysfs (/sys/class/net/<if>/statistics/), or netlink RTM_GETLINK | Precise control over poll interval; avoids extra layers (e.g., snmpd). |
| CPU/memory monitoring | perf stat, pidstat, /proc/stat, /proc/<pid>/statm | Low‑overhead, kernel‑sourced counters. |
| Queue depth observation | tc -s qdisc show dev <if>, /sys/class/net/<if>/queues/<tx‑rx>-<n>/ | Direct exposure of NIC TX/RX ring occupancy. |
| Kernel tracing (optional) | bpftrace, tracepoint:net:net_dev_queue, tracepoint:skb:kfree_skb | Validate that scraper does not interfere with NAPI. |
| System under test | Two identical servers (Linux 5.15‑0‑generic, Intel Xeon E5‑2680 v4, 2 × 10 GbE Intel X710 NICs) – one as traffic sender, one as receiver/scraper. | Enables controlled, symmetric load. |
Configuring Linux and Routing Hosts for Benchmarking
- Disable frequency scaling
echo performance | tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor - Isolate cores for the scraper (and optionally the traffic generator) using
isolcpusandtaskset. Example kernel boot line:
Bind the scraper to CPU 2:isolcpus=2,3 nohz_full=2,3 rcu_nocbs=2,3taskset -c 2 ./scraper -i eth0 -i 1000 # 1 kHz poll - Turn off unnecessary offloads
ethtool -K eth0 tso off gso off gro off lro off - Verify NAPI weighting (default usually 64)
ethtool -c eth0 | grep -i napi - Set static IRQ affinity to keep NIC queues on dedicated cores (e.g., cores 4‑7)
echo 4-7 > /proc/irq/<irq-number>/smp_affinity_list - Mount debugfs for eBPF/bpftrace if needed
mount -t debugfs none /sys/kernel/debug
Installing and Configuring Necessary Packages
On Ubuntu 22.04 (or equivalent):
apt-get update
apt-get install -y build-essential linux-tools-common linux-tools-`uname -r` \
ethtool iproute2 pktgen-dkms trex \
perf bpftrace python3-psutil prometheus-node-exporter grafana
Compile the scraper (see Code Examples) with -O2 -march=native -flto.
Benchmarking CPU Tax
Using System Monitoring Tools to Track CPU Usage
- Per‑process CPU:
pidstat -p <scraper-pid> 1reports%CPUand voluntary/involuntary context switches. - System‑wide:
perf stat -e cycles,instructions,cache-references,cache-misses -p <scraper-pid> sleep 10gives cycles per second. - Core‑level:
mpstat -P ALL 1shows per‑core utilization; watch the isolated core’s%userrise with poll rate.
Running Benchmarking Tests with High‑Frequency Interface and Queue Scraping
Scraper loop (simplified C):
while (!stop) {
clock_gettime(CLOCK_MONOTONIC, &start);
/* read rx/tx packets, bytes, drops, errors from sysfs */
read_counter("/sys/class/net/eth0/statistics/rx_packets", &rx_pkt);
read_counter("/sys/class/net/eth0/statistics/tx_packets", &tx_pkt);
/* optional: per‑queue stats */
for (int q = 0; q < num_queues; ++q) {
char path[128];
snprintf(path, sizeof(path),
"/sys/class/net/eth0/queues/rx-%d/packets", q);
read_counter(path, &rx_q[q]);
}
clock_gettime(CLOCK_MONOTONIC, &end);
/* sleep remaining time to hit target frequency */
nanosleep(&rem, NULL);
}
Compile and run with varying -i (interval in µs):
# Baseline: 10 ms (100 Hz)
taskset -c 2 ./scraper -i 10000 -i eth0 &
# Sweep
for us in 1000 500 200 100 50 20 10; do
taskset -c 2 ./scraper -i $us -i eth0 &
scraper_pid=$!
sleep 5
pidstat -p $scraper_pid 1 3
kill $scraper_pid
done
While the scraper runs, the traffic generator pushes a constant 5 Gbps UDP stream (≈7.5 Mpps 64‑byte packets) to stress the NIC and NAPI.
Analyzing CPU Usage Patterns and Identifying Bottlenecks
-
Example observations on Xeon E5‑2680 v4, Linux 5.15:
- 100 Hz → scraper
%user≈ 0.8 % on core 2, negligible impact on NAPI cores. - 1 kHz →
%user≈ 7.5 %, context switches ↑ from 120/s to 1.2 k/s. - 10 kHz →
%user≈ 68 %, scraper saturates core 2; NAPI cores see a 3‑5 % rise in%softirqdue to increased lock contention onu64_stats_sync. - 20 kHz →
%user> 90 %, packet loss appears on the receiver (RX drops ↑ from 0 to ~0.02 % of packets) as the scraper steals cycles from the softirq handling NAPI poll loop.
- 100 Hz → scraper
-
Bottleneck identification:
perf topshows the hottest symbol as__u64_stats_sync(spinlock protecting per‑CPUu64_stats) followed byvfs_read(sysfs attribute reads). Lock contention grows with poll frequency because each read acquires the same seqlock for each counter. -
Memory tax: negligible (< 0.1 % of memory bandwidth) because each read is 8 bytes; the dominant cost is CPU cycles spent in the lock and syscall overhead.
Benchmarking Memory Tax
Monitoring Memory Usage with System Tools
- Resident Set Size (RSS):
ps -o rss,pid,comm -p <scraper-pid>. - Page faults:
pidstat -p <scraper-pid> -r 1reports minor/major faults. - Memory bandwidth:
likwid-perfctr -g MEM -c 2 -t 10s(if likwid installed) orperf stat -e mem_load_retired.l3_miss,mem_load_retired.l1_miss.
Running Memory‑Intensive Benchmarking Tests
To stress memory, increase the amount of data scraped per poll:
- Add per‑queue statistics for all 16 TX and 16 RX queues (32 counters × 8 bytes = 256 bytes per poll).
- Optionally read extended ethtool stats (
ethtool -S eth0) which can return > 1 KB of counters. - Run the same frequency sweep while monitoring RSS.
Analyzing Memory Usage Patterns and Identifying Limitations
- RSS growth: stays constant (~2 MiB) regardless of poll rate because the scraper does not allocate per‑sample buffers; it reuses a fixed stack array.
- Page faults: remain at zero after startup; no dynamic allocation → no fault pressure.
- Memory bandwidth: at 10 kHz polling of 1 KB per poll → 10 MB/s, far below the ~20 GB/s DDR4 bandwidth of the test machine. No measurable impact on overall memory utilization.
- Conclusion: Memory tax is insignificant compared to CPU tax for this workload; the limiting factor is CPU cycles spent in locking and syscall overhead, not memory bandwidth.
Troubleshooting Common Issues
Identifying and Resolving CPU‑Related Bottlenecks
Symptom: Rising %softirq on NAPI cores, increased packet loss, latency jitter.
Diagnosis:
perf topshows__u64_stats_syncandvfs_readas hot.pidstatshows high voluntary context switches on the scraper core.
Resolution:
- Batch reads: Open the directory once and use
getdents/readvto fetch allstatistics/*entries, reducing path lookups. - Use netlink:
RTM_GETLINKwithIFLA_STATS64returns a single struct containing all counters, eliminating per‑counter syscalls and the seqlock. - Increase poll interval to stay below measured CPU headroom (e.g., < 5 % of a core).
- Pin scraper to a core isolated from NAPI and enable
nohz_fullto reduce tick interference. - Consider eBPF map: Attach an XDP program that increments a per‑CPU map; the scraper reads the map via
bpf_map_lookup_elem(still a syscall but lock‑free).
Identifying and Resolving Memory‑Related Issues
Symptom: Unexpected RSS growth or OOM killer activation.
Diagnosis: Check pmap <pid> for anonymous allocations; look for leaks in the scraper (e.g., failure to free() buffers).
Resolution:
- Ensure all buffers are stack‑allocated or freed after use.
- If using
libnlorlibmnl, verify that objects are released (nlmsg_free,cache_free). - Use
valgrind --leak-check=fullduring development.
CLI Examples to Troubleshoot and Optimize Performance
# 1. Verify per‑core load while scraper runs at 5 kHz
mpstat -P ALL 1 | awk '$3=="2"{print $0}' # core 2 only
# 2. Identify hot syscalls
perf record -p $(pgrep scraper) -g sleep 10
perf report --stdio | grep -E "sys_read|vfs_read|__u64_stats_sync"
# 3. Switch to netlink‑based scraper and compare
taskset -c 2 ./scraper_netlink -i 200 -i eth0 &