Having recently installed using 3.3.x (soon to upgrade to 3.4) I also noticed these stalls, typically close after restarts/big-reconfigures, ~100 and ~110 BGP protocol sessions in my instances.
Hendrik Visage
Director/Owner
HeViS.Co Systems t/a Envisage Cloud Solutions
hvisage@hevis.co.za
GSM/SMS/Signal: +27-84-612-5345
InstantMessenger: https://t.me/hvisage
On 14 Oct 2026, at 14:22, Pavel Trefný via Bird-users wrote:
Hi BIRD developers and users,
We would like to share a performance problem we encountered with BIRD 3.4.0
on Linux, our investigation, and an experimental patch series that substantially
improved behaviour on our test server. We can provide the patches for review.The investigation, patches and test runs were carried out with assistance
from OpenAI Codex, an AI coding agent, with us directing the experiments on
our server. The measurements below come from the actual test runs.Symptoms and environment
After moving from 3.2.4 to 3.4.0, we observed multi-second delays in basic
birdccommands and sustained utilisation of approximately one full CPU core
by the main thread. With 3.2.4, the CLI was much more responsive after
convergence. This was an operational comparison, not a controlled benchmark
of the two unmodified releases; startup kernel scans could also delay 3.2.4.We had also observed similar performance symptoms with the 3.3 series.
The profiles, patches and quantitative measurements below are for builds
based on 3.4.0; we have not profiled or tested these patches on 3.3.The server runs Linux on x86_64 with 16 logical CPUs and 12 BIRD worker threads.
It has 158 BGP protocols, of which 143 were established in our baseline,
and seven Kernel protocols. The Kernel channels reported approximately
3.79 million exports in aggregate. These exports span multiple tables and
must not be interpreted as 3.79 million distinct prefixes.CPU affinity alone did not solve the CLI delays: the CLI still depends on the
main loop, including its synchronous work. Stack sampling showed time spent
inmsl_cleanup()and synchronous Linux kernel scans through
nl_get_reply()/nl_get_scan()/krt_do_scan(). We therefore addressed
both allocator work and main-loop latency.Experimental changes
The series applies to clean upstream 3.4.0, in this order:
0001-bounded-allocator-cleanup.patchMake mslab cleanup resumable, with a budget of at most 256 page heads per
callback across full and partial cleanup. Partition the private full-page
list into 4096 buckets and use atomic dirty bits to select buckets affected
by publication or frees. This avoids repeatedly walking the entire
unchanged full-page population. Collect the published partial list only
when a possible empty-page transition requires it.The implementation retains the producer/consumer model and RCU grace
periods before republishing partial pages. It handles frees behind a
retained cursor and destruction with unfinished cleanup. Page headers and
the public API remain unchanged; the extra metadata is approximately
33 KiB per mslab. The patch includes regression tests.
0002-main-loop-quota.patchReduce the existing cooperative main-loop operation quota from 2048 to
128, giving other main-loop work more frequent opportunities to run during
route feed/export activity.
0003-async-kernel-and-cli.patchUse a dedicated nonblocking netlink socket for Linux per-table route dumps.
Collect and validate a complete dump before beginning refresh/prune, so an
interrupted, truncated or otherwise invalid dump is discarded and retried
instead of pruning against an incomplete snapshot. Receive at most eight
datagrams per callback with a 10 ms time budget; apply/replay at most
64 messages per callback with the same time budget. Individual operations
can still exceed the time budget.Serialize scan jobs, defer initial export until the scan completes, buffer
route notifications and replay them in FIFO order, and cancel jobs during
protocol shutdown. Snapshot and notification buffers are capped at
256 MiB and 8 MiB respectively, with abort/resync handling.Also remove an early
continuein the main-loop wakeup-pipe path that
skipped other ready sockets. Repeatedly rescheduled work could otherwise
keep the CLI from being serviced despite its socket being ready.The CLI remains in the main loop; we did not move it to a separate thread.
What profiling changed in our approach
Our first local batching prototype introduced excessive partial-list
recollection. We corrected that in an intermediate version, which we call
V3. That amplification was a defect in our prototype, not a demonstrated
defect in unmodified upstream 3.4.0. V3 nevertheless still consumed almost
one main-thread CPU core: profiling then identified repeated full-page list
walks as the dominant remaining cost. The bucket/dirty-bit approach above
is our final experimental version, V4.Measurements on this server
The quantitative baseline below is our already modified V3, immediately
before deploying V4, not stock 3.4.0. Both versions use the asynchronous scan
and quota changes. Routing policy, scan intervals and CPU affinity were
unchanged between them. This is a comparison of successive live runs on one
server, not a replay of identical traffic.
Version and observation window Main-thread CPU V3, 60 seconds after convergence 96.60% V4, first 60-second window after convergence 23.40% V4, second 60-second window after convergence 23.92% V4, longer window from 360 seconds after startup 27.56% Here, 100% means one logical CPU. CPU usage comes from the main thread's
/proc/PID/task/PID/stat, rather than percentages inferred from profiling.
Full-page cleanup accounted for 64.46% of userspace instruction-pointer
samples in the immediate V3 baseline and 8.51% in the first V4 profile.
These sample shares are separate from the CPU utilisation figures.During a 600-second V4 observation, all 120 CLI probes returned, with no
3-second timeouts. For the 73 probes from 360 seconds after startup, the
median was 4.09 ms, p95 6.06 ms and maximum 1269.45 ms. In that later window,
the whole process averaged 50.16% CPU and RSS was approximately 4.04–4.06 GiB.
Export counters continued to advance. All named BGP states matched the V3
baseline and all seven Kernel protocols were up with exports READY at the
final check.Validation and remaining limitations
We built with default
./configure;make -j2 bird checkpassed. New allocator
tests cover sparse frees among 60,000 objects, bounded cleanup work, slot
reuse, payload/accounting, frees behind a cursor and unfinished destruction.
The sparse-work regression fails on V3 and passes on V4. A four-thread test
with cross-thread frees passed, including 20 repeated runs on V4.Linux network-namespace tests passed for route ordering, cancellation,
interface failure, interrupted dumps and a large case with 400,000 IPv4
plus 100,000 IPv6 routes. We checked route counts, export changes, preservation
of existing routes on interrupted dumps, retry behaviour and clean shutdown.
Applying the series to the official 3.4.0 archive reproduced all eight changed
build-source files byte for byte.This remains experimental and the final V4 observation is short. Occasional
CLI delays around 1.27 seconds remain. Netlink write ACKs, interface/FDB scans,
legacy scans, shutdown and lock waits can still block. We have not established
long-term stability or verified every live forwarding route end to end.
An earlier prototype trial also logged two "Export flag inconsistency"
messages in Kernel channels; we have not established their cause or shown
that these patches resolve them.We would appreciate feedback on the allocator dirty-bucket design and RCU
handling, and on the snapshot/notification ordering and cancellation approach
for kernel scans. Is there an existing upstream approach we should follow?
We can share the three patches, test fixtures and measurement details, or
split the changes differently if that would make review easier.Best regards,
Pavel TrefnyPS (for Czech-speaking readers): My mu nerozumime, ale my mu verime :-)