Scale write throughput across data-shard groups while keeping a single unified read surface: - scatter_gather.rs: pooled fan-out across shard groups (replaces per-request client construction); cross-shard query results merged on one node - cluster/node.rs: cross-shard read routing — a read on any node gathers from every shard group's leader and unions results - cluster/forward.rs: fix h2 204 forward-relay bug (relay_forwarded skips body for 1xx/204/304 — synthesized JSON body on a 204 triggered HTTP/2 RST_STREAM on the real mTLS plane) - dto.rs: cross-shard query/result DTOs - k8s/cluster/: enable 3-group `shards:` topology (statefulset, service-peers, topology-configmap) - k8s/cluster-local-kind/: local-kind overlay to run the T5 gate without Ref-A - tidal-stress/k8s/stress-job-t5.yaml: 2-generator sharded throughput job - tests: cluster_cross_shard_reads.rs + multiproc support; ran real on kind - docs/profiling/m12p4-t5-sharded-throughput.md: T5 throughput findings
6.6 KiB
m12p4 / T5 — Sharded ingestion at scale (real-cluster run, 2026-06-14)
What ran. A real 3-shard-group × RF=3 tidalDB cluster on a local kind
cluster (single node, 24 vCPU / 50 GB), full mTLS (cert-manager-issued certs),
the m12p4 server image, driven by two in-cluster tidal-stress generator
pods (the "second stress generator" m12p4 calls for). Not a simulation — real
binaries, real WAL + quorum, real gRPC replication, real inter-node mTLS HTTP.
This is the honest record of what the run proved, what it found, and why
the headline gate (≥2.5× scaling AND ≥5000 quorum writes/s) remains
Ref-A/k3s-pending — now with data and a sharper reason, not just a hardware
caveat.
What the run proved (real, reproducible)
- The 3-group topology deploys and serves on real k8s.
shards:enabled (k8s/cluster/topology-configmap.yaml), every pod binds a derived gRPC port per group (9601/9602/9603),/cluster/statusshows balanced leaders — shard 0→tidaldb-0, 1→tidaldb-1, 2→tidaldb-2, all term 0. Fresh parallel boot converges in ~30–48 s. (Reproduce:k8s/cluster-local-kind/overlay.) - Cross-group quorum writes work end-to-end — a write for an entity owned by
another group's leader is hash-routed + forwarded over the inter-node mTLS
plane and quorum-acked,
x-tidal-seqverdict header riding back. - Graceful behavior under overload — at offered loads past the engine knee, the cluster sheds with 503 backpressure / request timeouts; 0 pod crashes, 0 acked loss (a timed-out write is never acked, so it is not acked loss).
The blocker this run uncovered and fixed (HTTP/2 forward relay)
The FIRST cross-group write returned curl exit 92 (HTTP/2 stream error) /
HTTP 000; the identical write with --http1.1 returned 204. Root cause: the
inter-node/client plane serves h2 (ALPN h2+http/1.1), /signals///embeddings
return 204 No Content, and the forward relay (forward::relay_forwarded)
attached a synthesized JSON body to a 204 (forward_json_with_headers fills
an empty peer body). HTTP/2 RST_STREAMs a 204 that carries a DATA frame; HTTP/1.1
tolerated it, which masked the bug until the sharded forward ran over the real
h2 plane (never exercised before — was itself Ref-A/k3s-pending). Fixed in
relay_forwarded (skip the body for 1xx/204/304 via status_forbids_body) +
3 unit tests; the one direct relay site now routes through the helper too. After
the fix, all cross-group writes return 204 over h2.
This is a genuine correctness fix that only a real-h2-plane run surfaces — in-process / plaintext multiproc e2e (reqwest→plaintext = h1.1) cannot catch it.
Throughput numbers (measured)
Open-loop, coordinated-omission-corrected, --ack quorum, --mix writes,
--embedding-dim 1536 (schema width). "Sustained" = highest ramp stage at
≤1% error; aggregate = sum of the two generator pods.
| Config (per-pod CPU) | Sustained quorum writes/s (≤1% err) | Behavior past the knee |
|---|---|---|
| 1 group, RF=3 (2) | ~5,500 (2×~2,775) | latency rises to p99 ~280 ms, 503s — single leader saturates |
| 3 groups, RF=3 (2) | ~4,000–5,000 | collapses earlier (stage 2: 78% shed, 40 s timeouts) |
Both generators are identical and cap at ~2,300–3,900 rps each — the knee
is the generator's in-flight×latency limit (client-shed), not engine errors, at
the lower stages. Two generators offer at most ~7,800 rps aggregate.
The finding: full-placement sharding does not scale writes at fixed per-pod CPU
The roadmap premise — "3 groups → ~3× writes" — assumes the bottleneck is the single-leader funnel (one leader serializing WAL append + quorum), which sharding parallelizes across S leaders. That holds only when per-pod CPU is not the binding constraint.
With full placement (every pod replicates EVERY group — the shape chosen for failover simplicity), each pod runs all S groups: leader for 1 + follower for S−1. So a pod's total replication work scales with S, while its CPU is fixed. At 2 vCPU/pod the 3-group pods are doing 3 groups' WAL/fsync/ship/apply in the same budget as the 1-group pods do 1 group's — so the 3-group cluster saturates at a comparable or lower aggregate than single-group, not 3× higher. Measured: 3-group ≈ single-group (~4–5k), then 3-group collapses first under overload.
Implication. Full-placement sharding buys failover (any pod loss keeps every group's quorum) and leader-funnel relief, but it does not raise aggregate write throughput at a fixed cluster CPU budget — the per-pod replication overhead grows with S. Real write scaling needs partitioned placement (each pod hosts a SUBSET of groups, so adding pods adds both groups and CPU) and/or per-pod CPU headroom so the leader funnel — not per-pod CPU — is the bottleneck.
Why the gate stays Ref-A/k3s-pending (now with data)
To demonstrate ≥2.5× scaling AND ≥5,000/s you need ALL of:
- Per-pod CPU headroom so the single-leader funnel (not per-pod CPU) binds — i.e. enough cores that a pod running S groups isn't CPU-saturated.
- Generators that can offer ≥14k rps — 2.5× over the ~5.5k single-group ceiling. Two TLS generators cap at ~7.8k aggregate; that's ~1.4× at most.
- Or partitioned placement across more nodes (the architecturally correct write-scaling shape), which a single kind node cannot host meaningfully.
A single shared 24-vCPU laptop cannot satisfy (1)+(2) simultaneously — the engine and the TLS generators contend for the same cores. This is precisely the hardware dependency the roadmap names; the run confirms it with numbers and adds the full-placement insight above.
Artifacts (committed)
k8s/cluster/topology-configmap.yaml— 3-groupshards:block enabled.k8s/cluster/statefulset.yaml,service-peers.yaml— derived per-shard ports.k8s/cluster-local-kind/— local-kind overlay (image + storageclass override).tidal-stress/k8s/stress-job-t5.yaml— the 2-generator T5 Job (production shape).tidal-server/src/cluster/forward.rs— the h2 204-relay fix + tests.
Recommended follow-up (for the visionary)
The T5 gate as written assumes full-placement scales writes; the measured reality says it scales failover, not write throughput, at fixed CPU. Re-scope T5 to either (a) partitioned placement on multi-node k3s/Ref-A (the real write-scaling proof), or (b) restate the gate as "leader-funnel relief at CPU headroom" and size the pods accordingly. Either way the generator fleet must be ≥4 pods to offer the load a scaled cluster can absorb.