export: a kilobyte less a route advertised with all_paths; memory accounted for
ci / check (push) Canceled after 0s
ci / docs (push) Canceled after 0s
ci / interop (push) Canceled after 0s
ci / fuzz (push) Canceled after 0s

A router feeding a collector every path (all_paths) was 4.8 GB resident
with a table that should take a third of that. A heap profile of the
daemon in the Linux VM, with four full tables and such a collector,
showed where it went: the exporter kept, for every route it had
advertised, the route's outgoing form, the parsed attribute list it was
dressed with, through an `Arc<Dressed>` in the per-route record. One
such form lived for every distinct attribute set advertised, about a
kilobyte each, for the life of the session, on top of the exporter's
cache of 262,144 of them. The record needs only to tell whether a route
changed, and the table's own attribute set, which is in memory anyway,
tells it that: the outgoing form is a function of the set, the peer the
route came from and the policy.

This change:

* `Sent` holds an `Arc<AttrSet>` instead of an `Arc<Dressed>`, and a
  route is sent again when its set differs. `Verdict::All` carries both
  while a flush is on.
* `DRESSED_KEPT` goes from 262,144 to 65,536: a kilobyte a set, so 64 MB
  rather than 256 MB a session at most; what a flush is sending holds
  its own.
* The status page's header shows the daemon's memory as jemalloc
  accounts for it (allocated and resident), `/api/status` has the six
  figures (`memory`: allocated, active, metadata, resident, mapped,
  retained), and `bgp_memory_bytes{kind}` exports them. jemalloc is built
  with statistics for this, and `bgp-web` reads them through
  `tikv-jemalloc-ctl`.
* `scripts/profile/full-table.sh` gains `COLLECTOR=1`, a collector
  session over IPv6 that takes both families with ADD-PATH, and `HEAP=1`,
  which builds the daemon with jemalloc's heap profiler (the new
  `heap-profiling` feature of the daemon crate, not for production) and
  turns the profile into a flame graph with jeprof; and it reports
  memory with the table in and again 30 s later. The feeder
  (`crates/bench/examples/feed.rs`) plays the collector with
  `FEED_COLLECTOR=1`.

Measured in the VM, four peers' full tables and a collector sent every
path (4.9 million routes advertised): 1,723 MiB resident before, 1,286
MiB after, with jemalloc's allocated figure from 1,500 MiB to 946 MiB.
Without the collector the same tables take 630 MiB, so the collector's
bookkeeping is now about 50 bytes a route advertised. Of the resident
figure, some 400 MiB was pages jemalloc had freed and not yet returned:
it returns them on allocator activity only, and a router that has its
tables in allocates little. jemalloc is now built with its background
thread on (the `background_threads` feature), which returns them on
time: 1,020 MiB resident 30 s after the load, against 1,286 MiB without
it.

Documentation: `memory` in the HTTP API reference, `bgp_memory_bytes`
in the metrics reference and the monitoring guide, the script's new
settings in the testing guide.

Testing: the ADD-PATH integration tests pass, including the collector
ones; the heap profile and the figures above are from
`PEERS=4 COLLECTOR=1 HEAP=1 scripts/profile/full-table.sh` in the VM.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Rayhaan JaufeerallyandClaude Fable 5.1 committed 2026-10-09 22:51:11 +02:00
1 parent d7d7d39bb9
commit b0dd357986
17 files changed
+281 -26

No files matched your search

Generated
+18
View File
@@ -315,6 +315,7 @@ dependencies = [
"nix", "nix",
"prometheus", "prometheus",
"serde", "serde",
"tikv-jemalloc-ctl",
"tokio", "tokio",
"tokio-stream", "tokio-stream",
"tracing", "tracing",
@@ -1098,6 +1099,12 @@ dependencies = [
"windows-targets", "windows-targets",
] ]
[[package]]
name = "paste"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]] [[package]]
name = "percent-encoding" name = "percent-encoding"
version = "2.3.2" version = "2.3.2"
@@ -1679,6 +1686,17 @@ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]]
name = "tikv-jemalloc-ctl"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a184c43b8ab2f41df2733b55556e3f5f632f4aeaa205b1bb018f574b7f5f142"
dependencies = [
"libc",
"paste",
"tikv-jemalloc-sys",
]
[[package]] [[package]]
name = "tikv-jemalloc-sys" name = "tikv-jemalloc-sys"
version = "0.7.1+5.3.1-0-g81034ce1f1373e37dc865038e1bc8eeecf559ce8" version = "0.7.1+5.3.1-0-g81034ce1f1373e37dc865038e1bc8eeecf559ce8"
+4 -2
View File
@@ -63,8 +63,10 @@ hashbrown = "0.17"
axum = "0.8.9" axum = "0.8.9"
tokio-stream = { version = "0.1.19", features = ["sync"] } tokio-stream = { version = "0.1.19", features = ["sync"] }
reqwest = { version = "0.13.5", default-features = false, features = ["json"] } reqwest = { version = "0.13.5", default-features = false, features = ["json"] }
# The daemon's memory allocator; see bin/bgp/src/main.rs. # The daemon's memory allocator; see bin/bgp/src/main.rs. With its
tikv-jemallocator = "0.7.0" # statistics, which the status page and the metrics show.
tikv-jemallocator = { version = "0.7.0", features = ["stats", "background_threads"] }
tikv-jemalloc-ctl = { version = "0.7.0", features = ["stats"] }
# `cargo build --profile profiling`: the release build, with the line tables # `cargo build --profile profiling`: the release build, with the line tables
# a profiler needs to name functions. See scripts/profile/. # a profiler needs to name functions. See scripts/profile/.
+5
View File
@@ -23,5 +23,10 @@ tracing-subscriber.workspace = true
[target.'cfg(not(target_env = "msvc"))'.dependencies] [target.'cfg(not(target_env = "msvc"))'.dependencies]
tikv-jemallocator.workspace = true tikv-jemallocator.workspace = true
[features]
# jemalloc's heap profiler, for scripts/profile/full-table.sh with
# HEAP=1: not for a production build, where it costs memory and time.
heap-profiling = ["tikv-jemallocator/profiling"]
[lints] [lints]
workspace = true workspace = true
+66 -1
View File
@@ -17,6 +17,12 @@
//! follow on from it. //! follow on from it.
//! - `FEED_HOLD_SECS`: how long the sessions stay open after the table is //! - `FEED_HOLD_SECS`: how long the sessions stay open after the table is
//! sent (default 30). //! sent (default 30).
//! - `FEED_COLLECTOR=1`: before the peers, one more session, as a route
//! collector would have: over IPv6, to `FEED_COLLECTOR_TO` (default
//! `[::1]:1790`) from `FEED_COLLECTOR_FROM` if set, offering to receive
//! several routes for a prefix (ADD-PATH) for both families and IPv4
//! routes with IPv6 next hops, and reading and dropping whatever it is
//! sent. Its AS number is 212232.
//! //!
//! What is printed: when the sessions are up and how long the writes took. //! What is printed: when the sessions are up and how long the writes took.
//! The writes end when the kernel's socket buffers have taken the last //! The writes end when the kernel's socket buffers have taken the last
@@ -28,6 +34,9 @@ use std::time::{Duration, Instant};
use bgp_bench::{Prefixes, Shape, announcements, encoded, env}; use bgp_bench::{Prefixes, Shape, announcements, encoded, env};
use bgp_integration_tests::test_peer::TestPeer; use bgp_integration_tests::test_peer::TestPeer;
use bgp_packet::open::{AddPathEntry, Capability, ExtendedNextHopEntry, SendReceive};
use bgp_primitives::constants::{Afi, AfiSafi, Safi};
use bgp_server::family::SUPPORTED;
use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpSocket; use tokio::net::TcpSocket;
use tokio::net::tcp::OwnedWriteHalf; use tokio::net::tcp::OwnedWriteHalf;
@@ -81,6 +90,61 @@ async fn main() {
started.elapsed().as_secs_f64() started.elapsed().as_secs_f64()
); );
// The collector first, so that it is there as the table arrives, as a
// router's collector session is after a restart.
let collector = env("FEED_COLLECTOR", 0) == 1;
let mut draining = Vec::new();
let mut collector_writer = None;
if collector {
let to: SocketAddr = std::env::var("FEED_COLLECTOR_TO")
.unwrap_or_else(|_| "[::1]:1790".into())
.parse()
.expect("FEED_COLLECTOR_TO is an IPv6 address and port");
let socket = TcpSocket::new_v6().expect("a socket");
if let Ok(from) = std::env::var("FEED_COLLECTOR_FROM") {
let address: std::net::Ipv6Addr = from.parse().expect("an IPv6 address");
socket
.bind(SocketAddr::new(address.into(), 0))
.expect("the collector's address is ours to connect from");
}
let stream = socket.connect(to).await.expect("the daemon listens");
let mut session = TestPeer::from_stream(stream);
let mut capabilities = vec![Capability::FourByteAsn { asn: 212_232 }];
capabilities.extend(SUPPORTED.into_iter().map(Capability::multiprotocol));
capabilities.push(Capability::AddPath {
entries: [AfiSafi::IPV4_UNICAST, AfiSafi::IPV6_UNICAST]
.into_iter()
.map(|family| AddPathEntry {
afi: family.afi,
safi: family.safi,
send_recv: SendReceive::Receive,
})
.collect(),
});
capabilities.push(Capability::ExtendedNextHop {
entries: vec![ExtendedNextHopEntry {
afi: Afi::IPV4,
safi: Safi::UNICAST,
nh_afi: Afi::IPV6,
}],
});
session
.handshake_with(212_232, Ipv4Addr::new(10, 0, 0, 200), 90, capabilities)
.await
.expect("the daemon takes the collector's session");
let (mut reader, writer) = session.into_stream().into_split();
draining.push(tokio::spawn(async move {
let mut sink = [0u8; 65536];
while let Ok(n) = reader.read(&mut sink).await {
if n == 0 {
break;
}
}
}));
println!("collector session established");
collector_writer = Some(writer);
}
let mut sessions = Vec::new(); let mut sessions = Vec::new();
for peer in 1..=peers { for peer in 1..=peers {
let socket = TcpSocket::new_v4().expect("a socket"); let socket = TcpSocket::new_v4().expect("a socket");
@@ -129,7 +193,7 @@ async fn main() {
}) })
}) })
.collect(); .collect();
let mut writers: Vec<OwnedWriteHalf> = Vec::new(); let mut writers: Vec<OwnedWriteHalf> = collector_writer.into_iter().collect();
for (index, sender) in senders.into_iter().enumerate() { for (index, sender) in senders.into_iter().enumerate() {
let (writer, took) = sender.await.expect("the sender runs"); let (writer, took) = sender.await.expect("the sender runs");
println!( println!(
@@ -155,4 +219,5 @@ async fn main() {
} }
} }
println!("sessions closed after {} s", hold.as_secs()); println!("sessions closed after {} s", hold.as_secs());
drop(draining);
} }
+16 -11
View File
@@ -253,8 +253,9 @@ const COALESCE: Duration = Duration::from_millis(50);
const RESYNCS_AT_ONCE: usize = 4; const RESYNCS_AT_ONCE: usize = 4;
/// How many routes ready to go out are remembered, by the attributes they /// How many routes ready to go out are remembered, by the attributes they
/// came with. A table has a few hundred thousand distinct sets. /// came with. A table has a few hundred thousand distinct sets, each a
const DRESSED_KEPT: usize = 262_144; /// kilobyte or so in this form; what a flush is sending holds its own.
const DRESSED_KEPT: usize = 65_536;
fn version_index(version: IpVersion) -> usize { fn version_index(version: IpVersion) -> usize {
match version { match version {
@@ -305,8 +306,11 @@ struct Sent {
/// The path identifier it went out with (ADD-PATH, RFC 7911), which /// The path identifier it went out with (ADD-PATH, RFC 7911), which
/// stays its own for as long as it is announced. /// stays its own for as long as it is announced.
id: u32, id: u32,
/// As it went out, to tell whether it has to go out again. /// The attributes it went out with, as the table has them: what it
dressed: Arc<Dressed>, /// goes out as follows from those, so a route whose set is the same
/// has not changed. The table holds the set anyway; the form it goes
/// out in is a kilobyte a set and is not kept here.
attrs: Arc<AttrSet>,
} }
/// What the peer is to end up with for a prefix. /// What the peer is to end up with for a prefix.
@@ -314,9 +318,10 @@ enum Verdict {
/// The best route as it goes out, or `None` for a prefix the peer is /// The best route as it goes out, or `None` for a prefix the peer is
/// not to have. /// not to have.
Best(Option<Arc<Dressed>>), Best(Option<Arc<Dressed>>),
/// Every route that goes out, with what tells it apart; empty for a /// Every route that goes out, with what tells it apart and the
/// prefix the peer is not to have. /// attributes it came with; empty for a prefix the peer is not to
All(Vec<(PathKey, Arc<Dressed>)>), /// have.
All(Vec<(PathKey, Arc<AttrSet>, Arc<Dressed>)>),
} }
/// What the peer is to end up with for each prefix heard of. /// What the peer is to end up with for each prefix heard of.
@@ -826,7 +831,7 @@ impl Exporter {
}; };
if let Some(dressed) = learned.dressed(&attrs, source, version, &egress) if let Some(dressed) = learned.dressed(&attrs, source, version, &egress)
{ {
wanted.push(((peer, path_id), dressed)); wanted.push(((peer, path_id), attrs, dressed));
} }
} }
} }
@@ -898,11 +903,11 @@ impl Exporter {
// again only if it changed; a new one gets the lowest // again only if it changed; a new one gets the lowest
// identifier not in use for the prefix. // identifier not in use for the prefix.
let mut has: Vec<Sent> = Vec::with_capacity(wanted.len()); let mut has: Vec<Sent> = Vec::with_capacity(wanted.len());
for (key, dressed) in wanted { for (key, attrs, dressed) in wanted {
let id = match had.iter().position(|sent| sent.key == key) { let id = match had.iter().position(|sent| sent.key == key) {
Some(at) => { Some(at) => {
let sent = had.swap_remove(at); let sent = had.swap_remove(at);
if Arc::ptr_eq(&sent.dressed, &dressed) { if Arc::ptr_eq(&sent.attrs, &attrs) {
has.push(sent); has.push(sent);
continue; continue;
} }
@@ -919,7 +924,7 @@ impl Exporter {
path_id: id, path_id: id,
}, },
); );
has.push(Sent { key, id, dressed }); has.push(Sent { key, id, attrs });
} }
withdrawn.extend(had.iter().map(|sent| Nlri { withdrawn.extend(had.iter().map(|sent| Nlri {
prefix, prefix,
+12
View File
@@ -211,6 +211,17 @@ pub static PEER_TCP: LazyLock<PeerTcp> = LazyLock::new(|| {
} }
}); });
/// The allocator's account of the daemon's memory, by `kind`: allocated,
/// active, metadata, resident, mapped, retained (jemalloc's `stats.*`).
pub static MEMORY: LazyLock<IntGaugeVec> = LazyLock::new(|| {
register_int_gauge_vec!(
"bgp_memory_bytes",
"The daemon's memory as its allocator accounts for it, by kind",
&["kind"]
)
.expect("metric registration")
});
pub static PEER_MAX_PREFIXES: LazyLock<IntGaugeVec> = LazyLock::new(|| { pub static PEER_MAX_PREFIXES: LazyLock<IntGaugeVec> = LazyLock::new(|| {
register_int_gauge_vec!( register_int_gauge_vec!(
"bgp_peer_max_prefixes", "bgp_peer_max_prefixes",
@@ -372,6 +383,7 @@ pub fn init() {
LazyLock::force(&PEER_UP); LazyLock::force(&PEER_UP);
LazyLock::force(&PEER_ADVERTISED); LazyLock::force(&PEER_ADVERTISED);
LazyLock::force(&PEER_TCP); LazyLock::force(&PEER_TCP);
LazyLock::force(&MEMORY);
BUILD_INFO BUILD_INFO
.with_label_values(&[env!("CARGO_PKG_VERSION")]) .with_label_values(&[env!("CARGO_PKG_VERSION")])
.set(1); .set(1);
+5
View File
@@ -24,5 +24,10 @@ tokio-stream.workspace = true
tracing.workspace = true tracing.workspace = true
nix = { workspace = true, features = ["hostname"] } nix = { workspace = true, features = ["hostname"] }
# The allocator's own account of memory, for the status page and the
# metrics. The allocator itself is the daemon's choice (bin/bgp).
[target.'cfg(not(target_env = "msvc"))'.dependencies]
tikv-jemalloc-ctl.workspace = true
[lints] [lints]
workspace = true workspace = true
+4
View File
@@ -39,6 +39,9 @@ fn unix_secs(t: SystemTime) -> u64 {
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
pub struct StatusView { pub struct StatusView {
/// The allocator's account of the daemon's memory; `None` where the
/// allocator keeps none.
pub memory: Option<crate::memory::MemoryUse>,
pub router_id: String, pub router_id: String,
pub local_asn: u32, pub local_asn: u32,
pub version: String, pub version: String,
@@ -606,6 +609,7 @@ impl IntoResponse for Unavailable {
pub async fn status(State(state): State<AppState>) -> Result<Json<StatusView>, Unavailable> { pub async fn status(State(state): State<AppState>) -> Result<Json<StatusView>, Unavailable> {
Ok(Json(StatusView { Ok(Json(StatusView {
memory: crate::memory::memory_use(),
router_id: state.info.router_id.to_string(), router_id: state.info.router_id.to_string(),
local_asn: state.info.local_asn, local_asn: state.info.local_asn,
version: state.info.version.clone(), version: state.info.version.clone(),
+1
View File
@@ -7,6 +7,7 @@
mod api; mod api;
mod events; mod events;
mod memory;
mod metrics; mod metrics;
use std::net::Ipv4Addr; use std::net::Ipv4Addr;
+46
View File
@@ -0,0 +1,46 @@
//! What the allocator says the daemon holds: jemalloc's own statistics,
//! for the status page and the metrics, where the kernel's resident size
//! alone would not say what is in use and what the allocator keeps back.
use serde::Serialize;
/// jemalloc's account of memory, in octets.
#[derive(Debug, Clone, Copy, Serialize)]
pub struct MemoryUse {
/// Octets in allocations the program holds.
pub allocated: u64,
/// Octets in the pages those allocations are in: `allocated` plus
/// the fragmentation within pages.
pub active: u64,
/// Octets of the allocator's own bookkeeping.
pub metadata: u64,
/// Octets in pages mapped and physically resident, as far as the
/// allocator knows: `active`, the metadata, and pages freed but not
/// yet returned to the kernel.
pub resident: u64,
/// Octets mapped, resident or not.
pub mapped: u64,
/// Octets of address space kept back after being unmapped, which
/// cost nothing physical.
pub retained: u64,
}
#[cfg(not(target_env = "msvc"))]
pub fn memory_use() -> Option<MemoryUse> {
use tikv_jemalloc_ctl::{epoch, stats};
// The statistics are refreshed only when the epoch is advanced.
epoch::advance().ok()?;
Some(MemoryUse {
allocated: stats::allocated::read().ok()? as u64,
active: stats::active::read().ok()? as u64,
metadata: stats::metadata::read().ok()? as u64,
resident: stats::resident::read().ok()? as u64,
mapped: stats::mapped::read().ok()? as u64,
retained: stats::retained::read().ok()? as u64,
})
}
#[cfg(target_env = "msvc")]
pub fn memory_use() -> Option<MemoryUse> {
None
}
+15 -1
View File
@@ -5,7 +5,8 @@ use axum::extract::State;
use axum::http::{StatusCode, header}; use axum::http::{StatusCode, header};
use axum::response::IntoResponse; use axum::response::IntoResponse;
use bgp_server::metrics::{ use bgp_server::metrics::{
LOCAL_ROUTES, PEER_TCP, RIB_ATTRIBUTE_SETS, RIB_JOURNAL_ENTRIES, RIB_PREFIXES, RIB_ROUTES, LOCAL_ROUTES, MEMORY, PEER_TCP, RIB_ATTRIBUTE_SETS, RIB_JOURNAL_ENTRIES, RIB_PREFIXES,
RIB_ROUTES,
}; };
use prometheus::{Encoder, TextEncoder}; use prometheus::{Encoder, TextEncoder};
@@ -25,6 +26,19 @@ pub async fn metricz(State(state): State<AppState>) -> impl IntoResponse {
RIB_ATTRIBUTE_SETS.set(gauge(state.rib.attrs().len())); RIB_ATTRIBUTE_SETS.set(gauge(state.rib.attrs().len()));
RIB_JOURNAL_ENTRIES.set(gauge(state.rib.journal_len())); RIB_JOURNAL_ENTRIES.set(gauge(state.rib.journal_len()));
sample_tcp(&state).await; sample_tcp(&state).await;
if let Some(m) = crate::memory::memory_use() {
let int = |n: u64| i64::try_from(n).unwrap_or(i64::MAX);
for (kind, octets) in [
("allocated", m.allocated),
("active", m.active),
("metadata", m.metadata),
("resident", m.resident),
("mapped", m.mapped),
("retained", m.retained),
] {
MEMORY.with_label_values(&[kind]).set(int(octets));
}
}
let mut buf = Vec::new(); let mut buf = Vec::new();
let encoder = TextEncoder::new(); let encoder = TextEncoder::new();
+3 -1
View File
@@ -165,7 +165,7 @@ input, select, button { color-scheme: light dark; }
<div> <div>
<div class="lefthand"> <div class="lefthand">
<b id="s-hostname">...</b>: router ID <b id="s-router-id">...</b>, AS <b id="s-asn">...</b>, Ferrix <span id="s-version">...</span><br> <b id="s-hostname">...</b>: router ID <b id="s-router-id">...</b>, AS <b id="s-asn">...</b>, Ferrix <span id="s-version">...</span><br>
Started <span id="s-started">...</span>, up <span id="s-uptime">...</span><br> Started <span id="s-started">...</span>, up <span id="s-uptime">...</span><span id="s-memory"></span><br>
<span class="dim">On this page: <a href="#peers">peers</a> · <a href="#rpki">RPKI</a> · <a href="#local">originated</a> · <a href="#kernel">kernel</a> · <a href="#received">received, and looking routes up</a> · <a href="#rejected">rejected</a> · <a href="#events">events</a></span><br> <span class="dim">On this page: <a href="#peers">peers</a> · <a href="#rpki">RPKI</a> · <a href="#local">originated</a> · <a href="#kernel">kernel</a> · <a href="#received">received, and looking routes up</a> · <a href="#rejected">rejected</a> · <a href="#events">events</a></span><br>
</div> </div>
<div class="righthand"> <div class="righthand">
@@ -410,6 +410,8 @@ from <select id="lookup-peer"><option value="">any peer</option></select>
$('s-asn').className = 'asn'; $('s-asn').dataset.asn = s.local_asn; $('s-asn').className = 'asn'; $('s-asn').dataset.asn = s.local_asn;
$('s-version').textContent = s.version; $('s-hostname').textContent = s.hostname; $('s-version').textContent = s.version; $('s-hostname').textContent = s.hostname;
$('s-started').textContent = new Date(s.started_at_unix * 1000).toString().replace(/ \(.*\)$/, ''); $('s-started').textContent = new Date(s.started_at_unix * 1000).toString().replace(/ \(.*\)$/, '');
const m = s.memory;
setHTML($('s-memory'), m ? ` · memory <span class="mono" title="jemalloc: allocated / active / resident / mapped / retained">${fmtOctets(m.allocated)} allocated, ${fmtOctets(m.resident)} resident</span>` : '');
tick(); tick();
} }
+2
View File
@@ -214,6 +214,8 @@ environment variables that control the run.
| `SCALE_DOWN` | Divides the size of the table | 1 | | `SCALE_DOWN` | Divides the size of the table | 1 |
| `HOLD_SECS` | How long the sessions stay open after the table is sent | 30 | | `HOLD_SECS` | How long the sessions stay open after the table is sent | 30 |
| `FREQ` | The sampling frequency in hertz | 1999 | | `FREQ` | The sampling frequency in hertz | 1999 |
| `COLLECTOR` | `1` adds a route collector session that is sent every path (`all_paths`) | 0 |
| `HEAP` | `1` profiles memory instead: the daemon is built with jemalloc's heap profiler, and `heap.svg` and `heap-top.txt` show where the allocations are | 0 |
| `OUT` | The directory for the results | `~/profile` | | `OUT` | The directory for the results | `~/profile` |
| `JEMALLOC_CONF` | jemalloc options for the daemon (its `MALLOC_CONF`), for example `thp:always` | none | | `JEMALLOC_CONF` | jemalloc options for the daemon (its `MALLOC_CONF`), for example `thp:always` | none |
@@ -252,6 +252,7 @@ Table 3 lists queries for a monitoring dashboard.
| Established sessions | `sum(bgp_peer_up)`, of `count(bgp_peer_up)` | | Established sessions | `sum(bgp_peer_up)`, of `count(bgp_peer_up)` |
| Routes by neighbor | `bgp_peer_routes` | | Routes by neighbor | `bgp_peer_routes` |
| Routes advertised to each neighbor | `bgp_peer_advertised_routes` | | Routes advertised to each neighbor | `bgp_peer_advertised_routes` |
| Memory in use and resident | `bgp_memory_bytes{kind="allocated"}`, `bgp_memory_bytes{kind="resident"}` |
| Share of time a session is held up by the neighbor's receive window | `rate(bgp_peer_tcp_receive_window_limited_seconds[5m]) / rate(bgp_peer_tcp_busy_seconds[5m])` | | Share of time a session is held up by the neighbor's receive window | `rate(bgp_peer_tcp_receive_window_limited_seconds[5m]) / rate(bgp_peer_tcp_busy_seconds[5m])` |
| Round-trip time to each neighbor | `bgp_peer_tcp_rtt_seconds` | | Round-trip time to each neighbor | `bgp_peer_tcp_rtt_seconds` |
| Octets queued towards a neighbor | `bgp_peer_tcp_send_queue_bytes` | | Octets queued towards a neighbor | `bgp_peer_tcp_send_queue_bytes` |
+1 -1
View File
@@ -29,7 +29,7 @@ server returns the following error statuses:
|---|---| |---|---|
| `/` | Redirects to `/statusz`. | | `/` | Redirects to `/statusz`. |
| `/statusz` | Status page. | | `/statusz` | Status page. |
| `/api/status` | Router ID, AS number, uptime, table sizes, and whether control communities are enabled (`control_communities`). | | `/api/status` | Router ID, AS number, uptime, table sizes, memory use (`memory`, as jemalloc accounts for it: `allocated`, `active`, `metadata`, `resident`, `mapped`, `retained` octets), and whether control communities are enabled (`control_communities`). |
| `/api/config` | The configuration that was read at startup. | | `/api/config` | The configuration that was read at startup. |
| `/api/peers` | Configured neighbors and their sessions. | | `/api/peers` | Configured neighbors and their sessions. |
| `/api/routes/local` | Originated routes. | | `/api/routes/local` | Originated routes. |
+1
View File
@@ -30,6 +30,7 @@ alerting rules based on these metrics, see
|---|---|---| |---|---|---|
| `bgp_peer_routes{peer}` | Gauge | Number of the neighbor's routes in the routing table. | | `bgp_peer_routes{peer}` | Gauge | Number of the neighbor's routes in the routing table. |
| `bgp_peer_advertised_routes{peer}` | Gauge | Number of routes advertised to the neighbor and not withdrawn since. | | `bgp_peer_advertised_routes{peer}` | Gauge | Number of routes advertised to the neighbor and not withdrawn since. |
| `bgp_memory_bytes{kind}` | Gauge | The daemon's memory as jemalloc accounts for it. `kind` is `allocated` (octets in live allocations), `active` (the pages they occupy), `metadata` (the allocator's own), `resident` (active, metadata and freed pages not yet returned to the kernel), `mapped`, or `retained` (address space kept back, which costs nothing physical). |
| `bgp_peer_tcp_rtt_seconds{peer}` | Gauge | Smoothed round-trip time of the session's TCP connection. Also `bgp_peer_tcp_min_rtt_seconds` and `bgp_peer_tcp_rtt_variance_seconds`. Linux only; sampled from the kernel at each scrape, as are all `bgp_peer_tcp_*` metrics. | | `bgp_peer_tcp_rtt_seconds{peer}` | Gauge | Smoothed round-trip time of the session's TCP connection. Also `bgp_peer_tcp_min_rtt_seconds` and `bgp_peer_tcp_rtt_variance_seconds`. Linux only; sampled from the kernel at each scrape, as are all `bgp_peer_tcp_*` metrics. |
| `bgp_peer_tcp_cwnd_segments{peer}` | Gauge | Congestion window, in segments. `bgp_peer_tcp_mss_bytes` is the segment size. | | `bgp_peer_tcp_cwnd_segments{peer}` | Gauge | Congestion window, in segments. `bgp_peer_tcp_mss_bytes` is the segment size. |
| `bgp_peer_tcp_send_window_bytes{peer}` | Gauge | The neighbor's receive window: what it lets Ferrix send. Zero while the neighbor is not reading. | | `bgp_peer_tcp_send_window_bytes{peer}` | Gauge | The neighbor's receive window: what it lets Ferrix send. Zero while the neighbor is not reading. |
+81 -9
View File
@@ -7,6 +7,8 @@
# PEERS=4 scripts/profile/full-table.sh four peers send it at once # PEERS=4 scripts/profile/full-table.sh four peers send it at once
# KERNEL=0 scripts/profile/full-table.sh the routes stay in the table # KERNEL=0 scripts/profile/full-table.sh the routes stay in the table
# SCALE_DOWN=10 scripts/profile/full-table.sh a tenth of the table # SCALE_DOWN=10 scripts/profile/full-table.sh a tenth of the table
# COLLECTOR=1 scripts/profile/full-table.sh a route collector is sent every path
# HEAP=1 scripts/profile/full-table.sh where the memory goes, not the time
# #
# From a machine that is not Linux: scripts/linux-vm.sh scripts/profile/full-table.sh # From a machine that is not Linux: scripts/linux-vm.sh scripts/profile/full-table.sh
# #
@@ -26,7 +28,10 @@ set -eu
peers=${PEERS:-1} peers=${PEERS:-1}
scale=${SCALE_DOWN:-1} scale=${SCALE_DOWN:-1}
kernel=${KERNEL:-1} kernel=${KERNEL:-1}
collector=${COLLECTOR:-0}
heap=${HEAP:-0}
hold=${HOLD_SECS:-30} hold=${HOLD_SECS:-30}
level=${LOG_LEVEL:-warn}
freq=${FREQ:-1999} freq=${FREQ:-1999}
out=${OUT:-$HOME/profile} out=${OUT:-$HOME/profile}
ns=ferrix-profile ns=ferrix-profile
@@ -47,7 +52,12 @@ done
# allocator is built with them too). # allocator is built with them too).
cd "$repository" cd "$repository"
export RUSTFLAGS="-C force-frame-pointers=yes" CFLAGS="-fno-omit-frame-pointer" export RUSTFLAGS="-C force-frame-pointers=yes" CFLAGS="-fno-omit-frame-pointer"
cargo build --profile profiling --locked -p bgp --bin bgp # With HEAP=1 the daemon is built with jemalloc's heap profiler and told
# to sample allocations and to write a profile at exit; jeprof turns the
# profile into collapsed stacks for a flame graph of where the memory is.
features=()
[ "$heap" = 1 ] && features=(--features heap-profiling)
cargo build --profile profiling --locked -p bgp --bin bgp "${features[@]}"
cargo build --profile profiling --locked -p bgp-bench --example feed cargo build --profile profiling --locked -p bgp-bench --example feed
mkdir -p "$out" mkdir -p "$out"
@@ -66,12 +76,12 @@ sudo ip netns add "$ns_peer"
sudo ip link add bench0 netns "$ns" type veth peer name peer0 netns "$ns_peer" sudo ip link add bench0 netns "$ns" type veth peer name peer0 netns "$ns_peer"
run ip link set lo up run ip link set lo up
run ip addr add 192.0.2.254/24 dev bench0 run ip addr add 192.0.2.254/24 dev bench0
run ip -6 addr add 2001:db8::ffff/64 dev bench0 run ip -6 addr add 2001:db8::ffff/64 dev bench0 nodad
run ip link set bench0 up run ip link set bench0 up
peer ip link set lo up peer ip link set lo up
for n in $(seq "$peers"); do for n in $(seq "$((peers + collector))"); do
peer ip addr add "192.0.2.$n/24" dev peer0 peer ip addr add "192.0.2.$n/24" dev peer0
peer ip -6 addr add "2001:db8::$n/64" dev peer0 peer ip -6 addr add "2001:db8::$n/64" dev peer0 nodad
done done
peer ip link set peer0 up peer ip link set peer0 up
peer ping -c 1 -W 2 192.0.2.254 > /dev/null || fail "the namespaces do not reach each other" peer ping -c 1 -W 2 192.0.2.254 > /dev/null || fail "the namespaces do not reach each other"
@@ -84,11 +94,13 @@ cat > "$out/daemon.textpb" <<CONFIG
router_id: "192.0.2.254" router_id: "192.0.2.254"
local_asn: 4200000000 local_asn: 4200000000
listen_endpoints { address: "192.0.2.254" port: 1790 } listen_endpoints { address: "192.0.2.254" port: 1790 }
listen_endpoints { address: "2001:db8::ffff" port: 1790 }
http_endpoint { address: "127.0.0.1" port: 8080 } http_endpoint { address: "127.0.0.1" port: 8080 }
CONFIG CONFIG
# The peers are configured as a router's would be, with how many routes # The peers are configured as a router's would be, with how many routes
# to expect of each. They have private AS numbers and the synthetic table # to expect of each. They have private AS numbers and the synthetic table
# has a few bogon prefixes, so bogons are taken. # has a few bogon prefixes, so bogons are taken. With a collector, every
# peer has a role, since the collector is told what they send.
for n in $(seq "$peers"); do for n in $(seq "$peers"); do
cat >> "$out/daemon.textpb" <<CONFIG cat >> "$out/daemon.textpb" <<CONFIG
peers { peers {
@@ -96,17 +108,38 @@ peers {
endpoint { address: "192.0.2.$n" port: 179 } endpoint { address: "192.0.2.$n" port: 179 }
remote_asn: $(( 64512 + n )) remote_asn: $(( 64512 + n ))
expected_routes: $prefixes expected_routes: $prefixes
role: PEER
import { accept_bogons: true } import { accept_bogons: true }
} }
CONFIG CONFIG
done done
if [ "$collector" = 1 ]; then
cat >> "$out/daemon.textpb" <<CONFIG
peers {
name: "collector"
endpoint { address: "2001:db8::$((peers + 1))" port: 179 }
remote_asn: 212232
role: CUSTOMER
add_path: ADD_PATH_SEND
import { max_prefixes: 1 }
export { learned_routes: true all_paths: true }
}
CONFIG
fi
if [ "$kernel" = 1 ]; then if [ "$kernel" = 1 ]; then
echo 'kernel { stale_seconds: 0 }' >> "$out/daemon.textpb" echo 'kernel { stale_seconds: 0 }' >> "$out/daemon.textpb"
fi fi
# `JEMALLOC_CONF`, if set, is jemalloc's MALLOC_CONF for the daemon: # `JEMALLOC_CONF`, if set, is jemalloc's MALLOC_CONF for the daemon:
# "thp:always", say. The symbols are prefixed, hence the variable's name. # "thp:always", say. The symbols are prefixed, hence the variable's name.
run env _RJEM_MALLOC_CONF="${JEMALLOC_CONF:-}" "$daemon_bin" "$out/daemon.textpb" --level warn > "$out/daemon.log" 2>&1 & malloc_conf=${JEMALLOC_CONF:-}
if [ "$heap" = 1 ]; then
# One sample every 512 KiB allocated, and a profile every 1 GiB
# allocated: the last of them is the nearest to the table being in.
rm -f "$out"/heap.*.heap
malloc_conf="prof:true,prof_active:true,lg_prof_sample:19,lg_prof_interval:30,prof_prefix:$out/heap${malloc_conf:+,$malloc_conf}"
fi
run env _RJEM_MALLOC_CONF="$malloc_conf" "$daemon_bin" "$out/daemon.textpb" --level "$level" > "$out/daemon.log" 2>&1 &
for _ in $(seq 100); do for _ in $(seq 100); do
run curl -sf http://127.0.0.1:8080/api/status > /dev/null 2>&1 && break run curl -sf http://127.0.0.1:8080/api/status > /dev/null 2>&1 && break
sleep 0.1 sleep 0.1
@@ -121,7 +154,8 @@ perf_pid=$!
sleep 1 sleep 1
peer env FEED_TO=192.0.2.254:1790 FEED_PEERS="$peers" FEED_SCALE_DOWN="$scale" \ peer env FEED_TO=192.0.2.254:1790 FEED_PEERS="$peers" FEED_SCALE_DOWN="$scale" \
FEED_FROM=192.0.2 FEED_HOLD_SECS="$hold" "$feed_bin" > "$out/feed.log" 2>&1 & FEED_FROM=192.0.2 FEED_HOLD_SECS="$hold" FEED_COLLECTOR="$collector" \
FEED_COLLECTOR_TO="[2001:db8::ffff]:1790" FEED_COLLECTOR_FROM="2001:db8::$((peers + 1))" "$feed_bin" > "$out/feed.log" 2>&1 &
feed_pid=$! feed_pid=$!
feeding() { kill -0 "$feed_pid" 2>/dev/null || fail "the load generator stopped; see $out/feed.log"; } feeding() { kill -0 "$feed_pid" 2>/dev/null || fail "the load generator stopped; see $out/feed.log"; }
@@ -147,8 +181,31 @@ if [ "$kernel" = 1 ]; then
installed=$(since "$start") installed=$(since "$start")
echo "$prefixes routes in the kernel after $installed s ($(echo "$prefixes / $installed / 1000" | bc -l | cut -c1-3) k routes/s)" echo "$prefixes routes in the kernel after $installed s ($(echo "$prefixes / $installed / 1000" | bc -l | cut -c1-3) k routes/s)"
fi fi
rss=$(awk '/VmRSS/ { printf "%.0f", $2 / 1024 }' "/proc/$daemon_pid/status") if [ "$collector" = 1 ]; then
echo "daemon: $rss MiB resident with the table in" # Nearly every taken route goes to the collector: not one with its AS
# on the path or a well-known community that says not to. The count
# has settled when it has not moved for a few seconds.
advertised() { status peers '[.[] | select(.name == "collector") | .advertised_routes] | add // 0'; }
last=-1
while :; do
feeding; sleep 3
now_held=$(advertised)
echo " collector holds $now_held of $received after $(since "$start") s"
[ "$now_held" -gt 0 ] && [ "$now_held" = "$last" ] && break
last=$now_held
done
echo "collector: $(advertised) routes advertised after $(since "$start") s"
fi
memory_report() {
rss=$(awk '/VmRSS/ { printf "%.0f", $2 / 1024 }' "/proc/$daemon_pid/status")
echo "daemon: $rss MiB resident $1"
status status '.memory | to_entries | map("\(.key) \(.value / 1048576 | floor) MiB") | join(", ")' | sed 's/^/jemalloc: /'
}
memory_report "with the table in"
# And once the allocator has had its decay time to give back what the
# load allocated and freed: the figure a router shows for the long run.
sleep 30
memory_report "30 s later"
run curl -sf http://127.0.0.1:8080/metricz | grep '^bgp_kernel_route_changes_total' | sed 's/^/kernel: /' run curl -sf http://127.0.0.1:8080/metricz | grep '^bgp_kernel_route_changes_total' | sed 's/^/kernel: /'
sleep 1 sleep 1
sudo kill -INT "$perf_pid" sudo kill -INT "$perf_pid"
@@ -156,6 +213,21 @@ wait "$perf_pid" || true
cat "$out/feed.log" cat "$out/feed.log"
sudo chown "$(id -u)" "$out/perf.data" sudo chown "$(id -u)" "$out/perf.data"
if [ "$heap" = 1 ]; then
sudo chown "$(id -u)" "$out"/heap.*.heap 2>/dev/null || true
# jeprof comes with jemalloc's sources as a template; the prefix the
# allocator's symbols carry is filled in.
template=$(find "$HOME/.cargo/registry/src" -path "*tikv-jemalloc-sys*" -name jeprof.in | head -1)
[ -n "$template" ] || fail "jeprof.in not found in the cargo registry"
sed -e 's/@JEMALLOC_PREFIX@/_rjem_/g' -e 's/@jemalloc_version@/5.3/' "$template" > "$out/jeprof"
# The latest profile is the one nearest to the table being in.
profile=$(ls -t "$out"/heap.*.heap | head -1)
perl "$out/jeprof" --collapsed "$daemon_bin" "$profile" > "$out/heap.folded" 2> "$out/jeprof.log" \
|| fail "jeprof failed; see $out/jeprof.log"
inferno-flamegraph --title "ferrix heap: $peers peer(s), $prefixes prefixes each$([ "$collector" = 1 ] && echo ", a collector")" --countname bytes --colors mem --width 1600 "$out/heap.folded" > "$out/heap.svg"
perl "$out/jeprof" --text "$daemon_bin" "$profile" 2>/dev/null | head -40 > "$out/heap-top.txt"
echo "heap profile $profile: heap.svg heap-top.txt"
fi
# Without the period field, so that each sample counts one whatever event # Without the period field, so that each sample counts one whatever event
# perf sampled by (cpu-clock on a machine without performance counters). # perf sampled by (cpu-clock on a machine without performance counters).
perf script -i "$out/perf.data" -F comm,pid,tid,time,event,ip,sym,dso 2> /dev/null \ perf script -i "$out/perf.data" -F comm,pid,tid,time,event,ip,sym,dso 2> /dev/null \