bench: two routers exchanging their tables with ADD-PATH

The full-table benchmark measures one router taking in tables from its
peers. What two routers of one AS do with each other, each sending the
other every path it has (ADD-PATH both ways with all_paths, as a mesh
of routers that each hold the network's whole view is configured), was
measured nowhere: the exporter following the table's journal while the
sessions fill the table, at the size of the Internet. In production
that exchange took 70 seconds for a table that arrives from the
Internet in 20.

A new benchmark, mesh, starts two servers in one AS with distinct
router IDs, peers them over IPv6 loopback with ADD-PATH both ways and
all_paths, and feeds the first from four synthetic peers and the second
from two over IPv4 loopback (BENCH_PEERS, BENCH_PEERS_B,
BENCH_SCALE_DOWN). It prints how long each router takes to hold its
own peers' routes and then the other's, with the rate of the latter.
BENCH_DEADLINE prints the sessions' state if a router is not there in
time.

The benchmarks now allocate with jemalloc, as the daemon does, except
when memory is being counted: the C library's allocator put its own
locks in the profiles. Two routers on one host have each other's
addresses as next hops, loopback addresses, which the import check
refuses as it refuses a next hop that is this machine; the permissive
import policy, which only tests use and no configuration can ask for,
now takes them.

Testing: the next-hop test covers both policies; the benchmark runs on
macOS and in the Linux VM under perf.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Rayhaan JaufeerallyandClaude Fable 5.1 committed 2026-10-10 13:45:15 +02:00
1 parent d0f092ff06
commit cbc1dc7e44
6 files changed
+234 -9

No files matched your search

Generated
+1
View File
@@ -182,6 +182,7 @@ dependencies = [
"bytes", "bytes",
"flate2", "flate2",
"stats_alloc", "stats_alloc",
"tikv-jemallocator",
"tokio", "tokio",
] ]
+7
View File
@@ -16,6 +16,9 @@ bytes.workspace = true
flate2 = "1" flate2 = "1"
stats_alloc = { version = "0.1.10", optional = true } stats_alloc = { version = "0.1.10", optional = true }
[target.'cfg(not(target_env = "msvc"))'.dependencies]
tikv-jemallocator.workspace = true
[dev-dependencies] [dev-dependencies]
bgp-integration-tests.workspace = true bgp-integration-tests.workspace = true
bgp-rib.workspace = true bgp-rib.workspace = true
@@ -42,3 +45,7 @@ harness = false
[lints] [lints]
workspace = true workspace = true
[[bench]]
name = "mesh"
harness = false
+195
View File
@@ -0,0 +1,195 @@
//! Two routers of one AS, each taking in full tables from its own peers
//! and sending the other every path (ADD-PATH both ways, `all_paths`), as
//! a mesh of routers that each hold the network's whole view do. How
//! long until each router has the other's routes is what this measures:
//! the exporter reading the table's journal while sessions fill the
//! table, which nothing else exercises at this size.
//!
//! Run with `cargo bench -p bgp-bench --bench mesh`. `BENCH_PEERS`
//! (default 4) feed the first router and `BENCH_PEERS_B` (default 2) the
//! second; `BENCH_SCALE_DOWN` shrinks the table. The routers peer over
//! IPv6 loopback and the feeders come over IPv4 loopback.
use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr};
use std::sync::Arc;
use std::time::{Duration, Instant};
use bgp_bench::{Prefixes, Shape, announcements, encoded, env};
use bgp_integration_tests::peer_config;
use bgp_integration_tests::test_peer::TestPeer;
use bgp_integration_tests::test_server::TestServer;
use bgp_packet::open::SendReceive;
use bgp_server::config::PeerConfig;
use bgp_server::policy::{ExportPolicy, ImportPolicy};
/// An AS number that is in none of the synthetic paths.
const ASN: u32 = 4_200_000_000;
fn free_port() -> u16 {
std::net::TcpListener::bind((Ipv6Addr::LOCALHOST, 0))
.expect("a port")
.local_addr()
.expect("an address")
.port()
}
/// A router of the mesh, listening for feeders on IPv4 loopback and for
/// the other router on IPv6 loopback at `port`, which it dials at
/// `other`.
async fn router(name: &'static str, id: u8, port: u16, other: u16) -> TestServer {
TestServer::start_with(ASN, |c| {
// Two routers of one AS must not share a BGP identifier.
c.router_id = Ipv4Addr::new(192, 0, 2, id);
c.cluster_id = c.router_id;
c.listen_endpoints = vec![
SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 0),
SocketAddr::new(Ipv6Addr::LOCALHOST.into(), port),
];
c.peers = vec![PeerConfig {
add_path: Some(SendReceive::Both),
import: ImportPolicy::permissive(),
export: Arc::new(ExportPolicy {
learned_routes: true,
all_paths: true,
..ExportPolicy::default()
}),
..peer_config(
name,
SocketAddr::new(Ipv6Addr::LOCALHOST.into(), other),
ASN,
)
}];
c.connect_retry = Duration::from_millis(200);
})
.await
.expect("a router")
}
/// Connects `count` feeders to `router`, numbered from `first`, and
/// returns their sessions with the bytes each is to send.
async fn feeders(
router: &TestServer,
prefixes: &Prefixes,
shape: Shape,
first: u32,
count: u32,
) -> Vec<(TestPeer, Vec<u8>)> {
let mut sessions = Vec::new();
for peer in first..first + count {
let stream: Vec<u8> = encoded(&announcements(prefixes, shape, peer))
.iter()
.flat_map(|m| m.iter().copied())
.collect();
let mut session = TestPeer::connect(router.addr).await.expect("a connection");
session
.handshake(64512 + peer, Ipv4Addr::new(10, 0, 0, peer as u8), 90)
.await
.expect("a session");
sessions.push((session, stream));
}
sessions
}
fn main() {
let peers_a = env("BENCH_PEERS", 4) as u32;
let peers_b = env("BENCH_PEERS_B", 2) as u32;
let shape = Shape::INTERNET.scaled_down(env("BENCH_SCALE_DOWN", 1));
let per_peer = shape.prefixes();
println!(
"== Two routers of AS{ASN}, every path both ways: A fed by {peers_a} peers, B by {peers_b}, {} prefixes each",
per_peer
);
let prefixes = Prefixes::generate(shape);
let runtime = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.expect("a runtime");
runtime.block_on(async {
let (port_a, port_b) = (free_port(), free_port());
let a = router("b", 1, port_a, port_b).await;
let b = router("a", 2, port_b, port_a).await;
a.wait_for_sessions(1).await.expect("the mesh session at A");
b.wait_for_sessions(1).await.expect("the mesh session at B");
let feed_a = feeders(&a, &prefixes, shape, 1, peers_a).await;
let feed_b = feeders(&b, &prefixes, shape, 1 + peers_a, peers_b).await;
let own_a = per_peer * peers_a as usize;
let own_b = per_peer * peers_b as usize;
let started = Instant::now();
let mut sending = Vec::new();
for (mut session, stream) in feed_a.into_iter().chain(feed_b) {
sending.push(tokio::spawn(async move {
session.send_raw(&stream).await.expect("the router reads");
session
}));
}
let mut a_own = None;
let mut b_own = None;
let mut a_all = None;
let mut b_all = None;
while a_all.is_none() || b_all.is_none() {
tokio::time::sleep(Duration::from_millis(5)).await;
let (at_a, at_b) = (a.rib.len(), b.rib.len());
let now = started.elapsed();
if a_own.is_none() && at_a >= own_a {
a_own = Some(now);
}
if b_own.is_none() && at_b >= own_b {
b_own = Some(now);
}
if a_all.is_none() && at_a >= own_a + own_b {
a_all = Some(now);
}
if b_all.is_none() && at_b >= own_a + own_b {
b_all = Some(now);
}
if now > Duration::from_secs(env("BENCH_DEADLINE", 600) as u64) {
let sessions = |router: &TestServer| -> Vec<String> {
router
.registry
.list()
.iter()
.map(|s| {
format!(
"{} routes {} rejected {}",
s.peer,
s.counters.routes(),
s.counters.rejected()
)
})
.collect()
};
panic!(
"not all routes arrived: A has {at_a} {:?}, B has {at_b} {:?}",
sessions(&a),
sessions(&b)
);
}
}
let row = |what: &str, when: Option<Duration>, routes: usize| {
let when = when.unwrap_or_default();
println!(
"{what:<52} {:>8.3} s {:.2} M routes/s",
when.as_secs_f64(),
routes as f64 / when.as_secs_f64() / 1e6
);
};
row("A has its own peers' routes", a_own, own_a);
row("B has its own peers' routes", b_own, own_b);
row(
"A has B's routes too (B's exporter, A's session)",
a_all,
own_b,
);
row(
"B has A's routes too (A's exporter, B's session)",
b_all,
own_a,
);
for session in sending {
drop(session.await);
}
});
}
+7
View File
@@ -17,6 +17,13 @@
pub mod memory; pub mod memory;
pub mod mrt; pub mod mrt;
// The benchmarks allocate as the daemon does (bin/bgp/src/main.rs), with
// jemalloc; the C library's allocator would put its own locks in the
// profile. Not when memory is counted: that needs the counting one.
#[cfg(all(not(feature = "count-memory"), not(target_env = "msvc")))]
#[global_allocator]
static ALLOCATOR: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc;
use std::collections::HashSet; use std::collections::HashSet;
use std::net::{Ipv4Addr, Ipv6Addr}; use std::net::{Ipv4Addr, Ipv6Addr};
+1
View File
@@ -646,6 +646,7 @@ fn parse_import(import: &proto::ImportPolicy) -> Result<ImportPolicy, ImportProb
accept_aspa_invalid: import.accept_aspa_invalid, accept_aspa_invalid: import.accept_aspa_invalid,
accept_as_sets: import.accept_as_sets, accept_as_sets: import.accept_as_sets,
accept_third_party_next_hop: import.accept_third_party_next_hop, accept_third_party_next_hop: import.accept_third_party_next_hop,
accept_loopback_next_hop: false,
accept_blackholes: import.accept_blackholes, accept_blackholes: import.accept_blackholes,
own_prefixes: Arc::default(), own_prefixes: Arc::default(),
originated: Arc::default(), originated: Arc::default(),
+23 -9
View File
@@ -91,6 +91,10 @@ pub struct ImportPolicy {
/// who never asked for it. A route server's routes always have its /// who never asked for it. A route server's routes always have its
/// clients as next hops, and are taken either way. /// clients as next hops, and are taken either way.
pub accept_third_party_next_hop: bool, pub accept_third_party_next_hop: bool,
/// Take a next hop that is a loopback address, or one of our own: for
/// routers that share a host, as those of a test do. Not a setting;
/// only [`ImportPolicy::permissive`] has it.
pub accept_loopback_next_hop: bool,
/// Take routes with the BLACKHOLE community for single addresses, and /// Take routes with the BLACKHOLE community for single addresses, and
/// have the kernel drop the traffic to them (RFC 7999). /// have the kernel drop the traffic to them (RFC 7999).
pub accept_blackholes: bool, pub accept_blackholes: bool,
@@ -199,6 +203,7 @@ impl Default for ImportPolicy {
accept_aspa_invalid: false, accept_aspa_invalid: false,
accept_as_sets: false, accept_as_sets: false,
accept_third_party_next_hop: false, accept_third_party_next_hop: false,
accept_loopback_next_hop: false,
accept_blackholes: false, accept_blackholes: false,
own_prefixes: Arc::default(), own_prefixes: Arc::default(),
originated: Arc::default(), originated: Arc::default(),
@@ -228,6 +233,7 @@ impl ImportPolicy {
accept_aspa_invalid: true, accept_aspa_invalid: true,
accept_as_sets: true, accept_as_sets: true,
accept_third_party_next_hop: true, accept_third_party_next_hop: true,
accept_loopback_next_hop: true,
accept_blackholes: false, accept_blackholes: false,
..Self::default() ..Self::default()
} }
@@ -727,10 +733,10 @@ impl ImportPolicy {
let mut is_peer = false; let mut is_peer = false;
for address in named { for address in named {
let usable = !address.is_unspecified() let usable = !address.is_unspecified()
&& !address.is_loopback()
&& !address.is_multicast() && !address.is_multicast()
&& address != IpAddr::V4(Ipv4Addr::BROADCAST) && address != IpAddr::V4(Ipv4Addr::BROADCAST)
&& address != from.local_address; && (self.accept_loopback_next_hop
|| (!address.is_loopback() && address != from.local_address));
if !usable { if !usable {
return Err(Rejection::NextHopUnusable); return Err(Rejection::NextHopUnusable);
} }
@@ -1548,13 +1554,7 @@ mod tests {
); );
// Nowhere at all, whatever the policy. // Nowhere at all, whatever the policy.
let lenient = ImportPolicy::permissive(); let lenient = ImportPolicy::permissive();
for nowhere in [ for nowhere in ["0.0.0.0", "224.0.0.5", "255.255.255.255"] {
"0.0.0.0",
"127.0.0.1",
"224.0.0.5",
"255.255.255.255",
"192.0.2.1",
] {
for policy in [&policy, &lenient] { for policy in [&policy, &lenient] {
assert_eq!( assert_eq!(
check(policy, &v4(nowhere), &EXTERNAL), check(policy, &v4(nowhere), &EXTERNAL),
@@ -1563,6 +1563,20 @@ mod tests {
); );
} }
} }
// This machine: nowhere for traffic to go, unless the policy is
// the permissive one, for routers that share a host.
for ourselves in ["127.0.0.1", "192.0.2.1"] {
assert_eq!(
check(&policy, &v4(ourselves), &EXTERNAL),
Err(Rejection::NextHopUnusable),
"{ourselves}"
);
assert_eq!(
check(&lenient, &v4(ourselves), &EXTERNAL),
Ok(()),
"{ourselves}"
);
}
// Where it may be someone else: said so, a route server, our own AS. // Where it may be someone else: said so, a route server, our own AS.
assert_eq!(check(&lenient, &v4("192.0.2.3"), &EXTERNAL), Ok(())); assert_eq!(check(&lenient, &v4("192.0.2.3"), &EXTERNAL), Ok(()));
for from in [ for from in [