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
Generated
+1
@@ -182,6 +182,7 @@ dependencies = [
|
|||||||
"bytes",
|
"bytes",
|
||||||
"flate2",
|
"flate2",
|
||||||
"stats_alloc",
|
"stats_alloc",
|
||||||
|
"tikv-jemallocator",
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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 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};
|
||||||
|
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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 [
|
||||||
|
|||||||
Reference in new issue
Block a user