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:
1 parent
d0f092ff06
commit
cbc1dc7e44
6 files changed
+234
-9
No files matched your search
@@ -16,6 +16,9 @@ bytes.workspace = true
|
||||
flate2 = "1"
|
||||
stats_alloc = { version = "0.1.10", optional = true }
|
||||
|
||||
[target.'cfg(not(target_env = "msvc"))'.dependencies]
|
||||
tikv-jemallocator.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
bgp-integration-tests.workspace = true
|
||||
bgp-rib.workspace = true
|
||||
@@ -42,3 +45,7 @@ harness = false
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[[bench]]
|
||||
name = "mesh"
|
||||
harness = false
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -17,6 +17,13 @@
|
||||
pub mod memory;
|
||||
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::net::{Ipv4Addr, Ipv6Addr};
|
||||
|
||||
|
||||
@@ -646,6 +646,7 @@ fn parse_import(import: &proto::ImportPolicy) -> Result<ImportPolicy, ImportProb
|
||||
accept_aspa_invalid: import.accept_aspa_invalid,
|
||||
accept_as_sets: import.accept_as_sets,
|
||||
accept_third_party_next_hop: import.accept_third_party_next_hop,
|
||||
accept_loopback_next_hop: false,
|
||||
accept_blackholes: import.accept_blackholes,
|
||||
own_prefixes: Arc::default(),
|
||||
originated: Arc::default(),
|
||||
|
||||
@@ -91,6 +91,10 @@ pub struct ImportPolicy {
|
||||
/// who never asked for it. A route server's routes always have its
|
||||
/// clients as next hops, and are taken either way.
|
||||
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
|
||||
/// have the kernel drop the traffic to them (RFC 7999).
|
||||
pub accept_blackholes: bool,
|
||||
@@ -199,6 +203,7 @@ impl Default for ImportPolicy {
|
||||
accept_aspa_invalid: false,
|
||||
accept_as_sets: false,
|
||||
accept_third_party_next_hop: false,
|
||||
accept_loopback_next_hop: false,
|
||||
accept_blackholes: false,
|
||||
own_prefixes: Arc::default(),
|
||||
originated: Arc::default(),
|
||||
@@ -228,6 +233,7 @@ impl ImportPolicy {
|
||||
accept_aspa_invalid: true,
|
||||
accept_as_sets: true,
|
||||
accept_third_party_next_hop: true,
|
||||
accept_loopback_next_hop: true,
|
||||
accept_blackholes: false,
|
||||
..Self::default()
|
||||
}
|
||||
@@ -727,10 +733,10 @@ impl ImportPolicy {
|
||||
let mut is_peer = false;
|
||||
for address in named {
|
||||
let usable = !address.is_unspecified()
|
||||
&& !address.is_loopback()
|
||||
&& !address.is_multicast()
|
||||
&& address != IpAddr::V4(Ipv4Addr::BROADCAST)
|
||||
&& address != from.local_address;
|
||||
&& (self.accept_loopback_next_hop
|
||||
|| (!address.is_loopback() && address != from.local_address));
|
||||
if !usable {
|
||||
return Err(Rejection::NextHopUnusable);
|
||||
}
|
||||
@@ -1548,13 +1554,7 @@ mod tests {
|
||||
);
|
||||
// Nowhere at all, whatever the policy.
|
||||
let lenient = ImportPolicy::permissive();
|
||||
for nowhere in [
|
||||
"0.0.0.0",
|
||||
"127.0.0.1",
|
||||
"224.0.0.5",
|
||||
"255.255.255.255",
|
||||
"192.0.2.1",
|
||||
] {
|
||||
for nowhere in ["0.0.0.0", "224.0.0.5", "255.255.255.255"] {
|
||||
for policy in [&policy, &lenient] {
|
||||
assert_eq!(
|
||||
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.
|
||||
assert_eq!(check(&lenient, &v4("192.0.2.3"), &EXTERNAL), Ok(()));
|
||||
for from in [
|
||||
|
||||
Reference in new issue
Block a user