Line-card BFD for plain Linux · part 2 of 12

The folklore is partly wrong

Part 1 measured FRR’s bfdd under a stress ladder and found it flapping 44 times in 120 seconds under timer pressure, with a p99 that looked perfectly healthy throughout.

Before writing any kernel code I wanted a second data point on the “userspace BFD cannot hold aggressive timers” claim. So I wrote the dumbest possible BFD daemon: 160 lines, one thread, a recv() with a 2ms timeout as the main loop clock, the RFC 5880 state machine, nothing else.

The plan was to watch it die the same way bfdd did, and then be justified in building the XDP path.

It did not die

Under the exact timer and hrtimer stress that flapped bfdd 44 times in 120 seconds, the naive loop ran clean. Zero flaps, p99 gap 13ms against a 30ms budget.

I ran it twice because I did not believe it.

Why, and it is not luck

It is scheduler mechanics.

A task that wakes 500 times a second, does microseconds of work, and sleeps again accumulates almost no vruntime. So whenever it wakes, CFS considers it the most deserving thing on the runqueue and it preempts the CPU hogs nearly instantly.

bfdd is the opposite shape. It is a heavyweight event loop serving zebra IPC, config machinery, and many timers, waking on exact intervals. The timer-subsystem stress was hitting precisely its wakeup path.

So “userspace” was never the problem as a category. The architecture of the wakeup path was. The folklore needed rewriting: userspace BFD reliability depends on winning a scheduling war, and your event-loop architecture decides how well armed you are.

Which raises the obvious next question. Is there a war no userspace architecture wins?

Yes: SCHED_FIFO

Four real-time priority hogs on four vCPUs, and the naive loop finally broke: 25 flaps in 60 seconds, max TX gap 324ms.

The kernel’s RT throttling reserves about 50ms per second for normal tasks when RT work saturates the CPUs. No amount of CFS-friendliness helps when you only exist for one breath per second.

This matters because it is not an artificial condition. On a loaded router, packet forwarding, softirq storms, and other RT-priority work outranking a BFD daemon is the normal state of affairs, not the exception.

The lesson hiding inside the failure

During the RT runs, the naive daemon’s own log showed zero detect timeouts. From its perspective, the peer’s packets kept arriving on schedule.

They had not. They had been queueing in the socket buffer while the daemon was starved. When it finally got CPU, it drained the backlog and its state machine saw a smooth stream of arrivals.

The peer, watching the actual wire, saw 300ms of silence and correctly declared the session dead.

Process logs lie under exactly the conditions you most need them. This is why every number in this series comes from tcpdump on the hypervisor bridge instead of from inside the guest, and it is a theme that recurs until the end of the project.

Next

The question had sharpened. Not “userspace versus kernel”, but “which transmit architectures survive RT starvation, and at what cost”. I tested five, and the most instructive result was the one that failed while producing the best p99 of any backend.