Research scaling design - kmosoti/FabricO11y GitHub Wiki
Historical report imported 2026-10-10 from
docs/research/scaling-design.md. Original SHA-256:75640df440c175084c11b9698e5c96df7e2fbf89bc1ce51f2e7814f15ede4ad4. Measurements and outcomes retain their stated revisions. Links and local environment labels are normalized;$FABRIC_DATAis the mounted experiment drive. See evidence policy.
Scaling design: from this machine to more resources
Status: design, derived from measurements; nothing here is implemented unless it says so. It uses the constants measured on one 4-CPU container in ingest run 01, spindle run 01, query walk run 01, text filter run 01, block tail run 01 and storage layout run 01, and asks what each component needs for a target such as 500 hosts sending 500 MB/s in all.
The measured constants
| Component | Constant on this machine | Bound by |
|---|---|---|
| Spindle collection | about 31 MB/s of log text on a fraction of a core | file reads and encoding; not limiting |
| Spindle delivery | about 9.7 MB/s of text, 35 Batches/s, 27 ms per Batch (3.6 MB/s before the quiet rule) | one Batch in flight per Strand: 13 ms at the server and 12 ms collecting the next Batch, in series |
| Spindle encoding | 2.7 bytes of Batch per byte of text on 130-byte lines | per-line attributes (path, device, inode, offsets) |
| Server ingest with sealing | 48, 75, 81 MB/s at 1, 2, 3 sealing workers on 3 CPUs | sealing CPU; about 27 MB/s per core |
| Sealer memory | about 5.5 times a 64 MiB journal file per concurrent build | today's in-memory build (ADR-0022 flattens it to about 45 MiB) |
| Segment bytes | about 0.41 of the journal on real text | the custody copy and both projections (storage direction) |
| Selective queries | 10 to 50 ms whatever the retained volume (walk plan) | one source's overshoot |
| Text search with no match | under 5 ms on 64 Segments with filters; 28 ms on a 64 MiB tail with blocks | filter reads, one per row group or block |
One host
A host sending 1 MB/s of text is inside today's per-node delivery ceiling (9.7 MB/s) on well under a core. The levers for a busier host, in order of cost:
- Fewer encoded bytes per line (no protocol change): carry the file path and device once per Batch as a scope attribute and only the offsets per line. On 130-byte lines that cuts the Batch about in half, so the same Batch rate carries about twice the text. It changes the attributes a log row carries, so it is a product contract fidelity question first.
- A shorter commit wait (implemented 2026-10-03 as the quiet rule, ADR-0013 amended): a group also closes when nothing has arrived for 2 ms, so a lone Strand no longer waits out the 50 ms window while the sharing under load is kept. It changes when groups close, not their sync order. Measured in sealer profile run 01: one node's delivery went from 3.63 to 9.7 MB/s of log text (78 to 27 ms per Batch, of which the server's answer fell from 63 to 13 ms).
- More than one Batch in flight per Strand: a window of N Batches multiplies the per-node ceiling by about N. It changes the delivery rule (ADR-0013), its oracle and its model, and is the most expensive lever.
Collection itself needs nothing until well past 30 MB/s per host.
One server
At 27 MB/s per sealing core, 500 MB/s needs about 19 cores of sealing, plus commit, TLS and queries: about 24 to 32 cores on one server. Before that is reasonable:
- Flat-memory sealing (ADR-0022): with 45 MiB per build, 16 workers need under 1 GiB where today they would need 5.6 GiB.
- Cheaper sealing per byte: the build decodes every OTLP Batch and writes three tables and the custody copy. Re-blocking a canonical record stream (storage direction, hypothesis D7) instead of re-encoding, and keeping one canonical copy (D3), remove most of that work and about 60% of the Segment bytes.
- The disk: 500 MB/s of journal writes and about 200 MB/s of Segment writes sustained, 17 TiB of Segments a day, call for NVMe and a retention budget sized in days, not the 20 GiB default.
Where the sealing CPU goes
Sealer profile run 01 looked inside the 27 MB/s per core. On one 64 MiB real-text file, a quarter of the sealer's instructions went to the trigram filter's hash set and a fifth to Arrow and Parquet; decoding, sorting and (natively small) hashing shared the rest, and replaying the journal at open cost another fifth on top. Replacing the filter's hash set by a bitmap took sealing from 24.4 to 28.1 MB/s of journal on one core, replay included (13% less CPU where the instruction profile had promised a quarter), filter bytes unchanged. The next cheapest removals, in order: a header-only identify at replay, zero-copy payload fields in the envelope, and sorting indices instead of rows, the last already part of ADR-0022.
Several servers
When one server's cores or disk are not enough, shard by Strand: each Spindle's credential maps to one server (by enrollment), every Strand's custody, deduplication and ordering stay on that server, and nothing in delivery changes. A query fans out to every server and merges:
limit kqueries take the k smallest keys of the per-server answers; each server stops early with the walk, and the merge is the same threshold rule one level up (a server whose smallest key is above the merged heap's threshold contributes nothing more);- page tokens carry one snapshot per server;
- completeness is the conjunction of the servers' answers, and freshness the union of their maps.
The query specification in the core (fabric_core::query::spec) already states the merge's correctness: a threshold walk over sources with sound bounds equals the definition (theorem T7), and a server's answer is such a source.
The 500-host target, line by line
| Need | Today on this machine | What reaches it |
|---|---|---|
| 1 MB/s from each of 500 hosts | 9.7 MB/s per host ceiling (3.6 before the quiet rule) | nothing; inside the ceiling |
| 500 MB/s of server ingest | 81 MB/s on 3 cores | about 24 to 32 cores with flat-memory sealing, or 4 to 6 servers of 8 cores sharded by Strand; less with cheaper sealing |
| 24 h of retention | 20 GiB default | about 17 TiB of NVMe (about 7 TiB with one canonical copy) |
| Interactive queries | 10 to 50 ms for selective shapes; seconds for an hour-wide no-match search at this volume | the walk, the filters and the block tail as built; per-server fan-out keeps each server's share fixed |
What to measure next
- The quiet rule's fault runs and the history protocol under it (the rule itself is implemented and measured).
- Ingest with the flat-memory sealer at 1 to 8 workers on a larger host.
- Two servers sharded by Strand with a fan-out query, against one server, on the registered workload.