9276bc9f7a443e75e0f2ea9f55e162ed105c61f4
6
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
9276bc9f7a |
session: write messages in batches, with TCP_NODELAY
A session wrote every message to its socket as it came: one write and, with Nagle's algorithm, mostly one segment for each UPDATE, a hundred or two octets. On the IPv6 cross-connect between the two routers, whose round trip is 50 microseconds, a million routes took 42 seconds to go out: the status page's new TCP row showed the connection busy for that long with neither window nor buffer limiting it, a congestion window stuck at ten segments, and a smoothed round-trip time of 18 ms. That is the receiver's delayed acknowledgements, 40 ms at a time, on a stream of small segments, holding the sender to a few segments a round trip. Now a session gathers what is waiting for it, up to the outbound queue's length, in one wake, encodes the messages into one buffer, and writes the buffer when it reaches 64 KiB or when the actions of the wake are done; the socket has TCP_NODELAY, so a batch's last, partial segment is not held back for the acknowledgement of the one before. The NOTIFICATION that ends a session, including one sent after a panic, is flushed on its own. Measured in the Linux VM with four peers' tables going to a collector over a veth pair, 151 MiB: 121,850 segments instead of 313,773, the connection busy 0.8 s instead of 2.0 s, and the congestion window grown to 47 segments instead of 13. Over loopback the transit benchmark is unchanged at 2.5 s for the second neighbor to have the table: there the receiver's parsing sets the pace, not the segments. Testing: the session tests, including the one that sends a NOTIFICATION after a panic; the integration tests; the kernel tests and every lab phase. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
b0dd357986 |
export: a kilobyte less a route advertised with all_paths; memory accounted for
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>
|
||
|
|
03f4f58ecb |
rib: give a row room for every peer's route at its first growth
A row's routes are in a vector that grew by one route at a time, so with four peers sending full tables every row's vector was reallocated three times as their routes arrived, and the reallocations were 7% of the daemon's CPU time in a profile of a four-peer load. This change has the first growth of a row take it to as many routes as the table has peers, at most 8 (`FIRST_ROOM`), since a prefix that one full table has, the others nearly always have too; after that it grows by a quarter as before. A table with one peer never grows a row and is unchanged. A route server's clients do not all announce every prefix, hence the cap: a row of a prefix that fewer peers announce has at most seven spare slots of 16 bytes. Measured in the Linux VM with four peers sending the synthetic table of 1.23 million prefixes: the kernel is in step 1.9 s after the first route instead of 2.2 s, the daemon's CPU time over the load is 5% lower, and it is 604 MiB resident instead of about 630, since there are no reallocation leftovers. With one peer nothing changes. Memory per route in the benchmarks, allocations counted: 77 bytes before and after with the synthetic table from four peers; 64 before and 65 after with the RIPE RIS dump's eight largest peers, whose load went from 2.8 s to 1.8 s on one thread, the allocation counting making every reallocation dearer than it is in the daemon. Also tried and not adopted, with the new `JEMALLOC_CONF` setting of the profiling script: transparent huge pages for the allocator (`thp:always,metadata_thp:always`). Over three pairs of runs with one peer the table arrived in 0.66, 1.17 and 0.70 s with them against 0.87, 0.75 and 0.87 s without, the CPU time was within the noise, and the daemon was 60 to 100 MiB larger. An arena for the routes would not help either: jemalloc's size classes already give rows exact 16, 32, 48 and 64 byte allocations, and the cost was the number of reallocations, not the allocator. Testing: the table tests, including the one that grows a row past 65,535 routes, pass; the kernel tests and the interoperability lab pass. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
48da8cc7d2 |
rib: size the routing table for the routes the peers are expected to send
The routing table's shards started empty and grew as routes came in, each growth moving every row of the shard into a table twice the size. In a profile of the daemon taking in a full table, the growth showed as up to a seventh of the session's time, and the daemon's resident memory had the old and the new table of each shard side by side for a moment. This change adds `RibTable::expect_prefixes`, which reserves room for that many prefixes spread over the shards, and has the server call it at startup with the largest `expected_routes` among the configured peers: the prefixes of several full tables are mostly the same ones. Without `expected_routes` nothing changes. The `full_table` benchmark does the same unless `BENCH_PRESIZE=0`. Measured, the saving in time is within the run-to-run noise: in the Linux VM one peer's table is in the routing table in 0.81 s instead of 0.89 s, and the kernel is in step at about the same time as before; three pairs of runs of the benchmark on a Mac average 0.33 s against 0.35 s. The profile's attribution to the growth was mostly sampling skid. The memory saving is real: 442 MiB resident with one peer's table in instead of 483 MiB, and 626 MiB instead of 632 MiB with four. Also: the profiling script printed nothing for the kernel's change counts because it asked `/metrics` for them; the endpoint is `/metricz`. With that fixed, a four-peer load shows 1.25 million installs for 1.23 million prefixes: the journal already gives the kernel task only the latest change of a prefix when it lags, so no further coalescing of installs is called for. Documentation: `expected_routes` in the configuration reference, the neighbor guide and the proto comment say that the table is sized by it; the testing guide's VM results are brought up to date. Testing: a table test checks that the room is there and is kept; the workspace tests pass. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
5915c8e562 |
profile: configure the peers and report the kernel's change counts
The full-table profiling script took its peers as unconfigured ones under the permissive policy, which is not how a router is set up. Now it writes a peer block for each of them, with `expected_routes` set to the table's size, so that the default import policy and anything that sizes itself by the expected routes are in the measurement. The few bogon prefixes in the synthetic table are refused by that policy, as the output says. After the table is in, the script also prints `bgp_kernel_route_changes_total`, so that a run shows how many installs and removals the kernel was asked for and not only how many routes it ended up with. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
1bc71aaac5 |
fib, bench: profile the daemon taking in a full table; count blackholes instead of scanning
Before this change there was no way to see where the daemon's CPU time goes while it receives a full Internet routing table: the benchmarks in crates/bench time the routing table in-process, without the sessions, the policy, the kernel task or the production allocator. Profiling the daemon itself showed that a third of its CPU time during a full-table load went into `Plan::blackholes()`, which walked every installed route to count the blackhole ones each time the kernel task published its status, after every batch of changes; with 1.2 million routes installed that was 3.9 seconds from the first route to a synced kernel instead of 2.0. This change: * Keeps counts of blackhole routes and of routes kept unconfirmed from an earlier run in `Plan`, in step with the map through `Plan::put` and `Plan::take`, so that `Plan::blackholes()` and `Plan::kept()` cost nothing. `kept()` had the same scan; it ran only while routes of an earlier run were being kept, which is exactly during a full-table load after a restart. * Adds `scripts/profile/full-table.sh`, which builds the daemon with the new `profiling` Cargo profile (release with line tables) and frame pointers, runs it in a network namespace joined by a veth pair to a second namespace, feeds it the synthetic table from there as one or more neighbors, samples it with `perf`, and writes a flame graph (inferno), flat profiles by self and inclusive time, the time until the routes are in the table and in the kernel, and the daemon's resident memory. `PEERS`, `KERNEL`, `SCALE_DOWN`, `HOLD_SECS`, `FREQ` and `OUT` control it. From macOS it runs in the Linux VM through scripts/linux-vm.sh. * Adds the load generator `crates/bench/examples/feed.rs`: it connects from the peers' addresses, completes the handshakes, writes each neighbor's table, drains what the daemon sends back and holds the sessions open with KEEPALIVEs. `TestPeer::into_stream` gives it the connection after the handshake. * Updates the testing guide: the current `full_table` numbers, a "Profiling" section, and the daemon's numbers in the VM. Measured in the Linux VM (8 vCPUs) with the synthetic table of 1.23 million prefixes: one neighbor's table is in the routing table after 0.7 s and in the kernel after 2.0 s (before: 3.9 s); four neighbors' 4.9 million routes are in the table after 1.1 s and the kernel is synced after 3.0 s; 520 MB resident with one table, 680 MB with four. Without the kernel one session takes in 2.0 million routes per second on one core. A profile also attributed 14% of the session's time to `Journal::tidy`; inlining its early return changed nothing in an A/B run of the benchmark, so that was a sampling artefact and is left as it is. Testing: a unit test checks the counts against a scan of the entries through adopt, best, reset, synced, release, withdrawal, refusal and clear; the kernel test suite and the workspace tests pass; the script was run with one and four neighbors, with and without the kernel. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |