Skip to content
Phase M2: Remote fetch (3-node cluster)

Phase M2: Remote fetch (3-node cluster)

USE_REMOTE_FETCH (getMappedRange) against SCAN_AND_FETCH on the final tree (267a41434a81723b07e7150869d702636a1b91a9) across a 4-host VPC topology: one dedicated client VM and three dedicated FoundationDB 7.3.77 server VMs (double redundancy, ssd-2 storage engine, 9 storage servers). This is phase M2 (M2rl and M2skip) of hcloud-multinode-vpc-20261014.

Shard placement and qualification (Gate P0)

  • Before any M2 timing, placementprobe_cgo (H5, reading \xff/dataDistributionMode, \xff/moveKeysLock/Owner, \xff/moveKeysLock/Write, \xff/keyServers/, and transactional storage addresses via libfdb_c at snapshot read version V1 and rechecking stability at V2) and the ballast tool (H6) were qualified on the live cluster:
    • Probing with data distribution on was refused.
    • The unmodified scratch control store passed integrity_ok = true and all 30 Python provenance/key mutation controls (ok = true).
    • All three physical on-cluster defects (delete-chunk, stray-suffix, version-key) failed integrity_ok with the expected error messages.
  • Three placement attempts (256 MiB, 512 MiB, 1024 MiB ballast) were run following the preregistered rule (protocol.json placement.mechanism): settle for 120 s with moving_data in-flight and queued bytes at 0, freeze data distribution (datadistribution off), clear only the ballast range and verify it empty, and take the canonical CGo placement probe and receipt.
  • Per protocol, attempt 3 was sealed before timing (plans/testingb/manifest.json SHA 4ada3920f92da4489fe4037ad5dd3440cec59957fc4ccdcba81b792b7a45f54a):
    • benchfetch/small: integrity_ok = true, passed = true, 100% cross-server (server-disjoint fraction 1.0000) on every cell.
    • benchfetch/planskip: integrity_ok = true, passed = true, 100% cross-server (server-disjoint fraction 1.0000) on every cell.
    • benchfetch/split: integrity_ok = true, 1 index shard and 7 record shards, shared_shards = 0: limit1-forward and limit1-reverse are cross-server (1/1), rows10 is partially colocated (f = 0.0000) (0/10), and rows1000 is partially colocated (f = 0.6960) (696/1000 rows on storage-server teams disjoint from the index shard’s team).
    • With double replication across three machines, any two 2-replica storage teams share at least one physical machine by pigeonhole; cross-server therefore means storage-process (address) disjoint, not machine disjoint.
  • Before the first process and after the last process of every M2 case, R2 (bench_runner_mn.py) took a fresh CGo placement receipt and verified via placement_receipt.compare that shard boundaries, storage teams, and disjointness fractions remained identical (equal = true).
  • R4 telemetry reconciliation (reconcile_telemetry.py) across node1..node3 and client reported 12/12 clean cases in M2rl (144 processes, 432 samples) and 4/4 clean cases in M2skip (48 processes, 144 samples), with 0 contaminated and 0 unverifiable cases.

Results

Figure M2.1: Time per operation, USE_REMOTE_FETCH / SCAN_AND_FETCHBelow 1.0× means USE_REMOTE_FETCH is faster. 3-node cluster, 9 storage servers.

SCAN_AND_FETCH is faster in 10 of 16 cases, including all six 1,000-row scans (up to 2.326×). USE_REMOTE_FETCH is faster in 4, most clearly skip3-rows10-reverse (0.158×). Two are inconclusive.

0.15×0.15×0.25×0.25×0.5×0.5×0.7×0.7×1.0×1.0×1.4×1.4×2.0×2.0×2.5×2.5×← USE_REMOTE_FETCH fasterSCAN_AND_FETCH faster →M2rl: BenchmarkIndexFetch (3-node double ssd-2)small-limit1-forward [cross-srv]small-limit1-forward [cross-srv]: 0.979× [0.964, 0.995]0.979× [0.964, 0.995]small-limit1-reverse [cross-srv]small-limit1-reverse [cross-srv]: 0.985× [0.969, 1.003]0.985× [0.969, 1.003]small-rows10-forward [cross-srv]small-rows10-forward [cross-srv]: 1.031× [1.018, 1.042]1.031× [1.018, 1.042]small-rows10-reverse [cross-srv]small-rows10-reverse [cross-srv]: 1.026× [1.013, 1.039]1.026× [1.013, 1.039]small-rows1000-forward [cross-srv]small-rows1000-forward [cross-srv]: 1.226× [1.175, 1.280]1.226× [1.175, 1.280]small-rows1000-reverse [cross-srv]small-rows1000-reverse [cross-srv]: 1.255× [1.217, 1.293]1.255× [1.217, 1.293]split-limit1-forward [cross-srv]split-limit1-forward [cross-srv]: 1.110× [1.087, 1.131]1.110× [1.087, 1.131]split-limit1-reverse [cross-srv]split-limit1-reverse [cross-srv]: 1.114× [1.095, 1.138]1.114× [1.095, 1.138]split-rows10-forward [f = 0.0000]split-rows10-forward [f = 0.0000]: 0.873× [0.829, 0.920]0.873× [0.829, 0.920]split-rows10-reverse [f = 0.0000]split-rows10-reverse [f = 0.0000]: 0.854× [0.823, 0.886]0.854× [0.823, 0.886]split-rows1000-forward [f = 0.6960]split-rows1000-forward [f = 0.6960]: 2.326× [2.254, 2.397]2.326× [2.254, 2.397]split-rows1000-reverse [f = 0.6960]split-rows1000-reverse [f = 0.6960]: 2.300× [2.206, 2.384]2.300× [2.206, 2.384]M2skip: BenchmarkPlanSkip (3-node double ssd-2)skip3-rows10-forward [cross-srv]skip3-rows10-forward [cross-srv]: 1.155× [0.991, 1.345]1.155× [0.991, 1.345]skip3-rows10-reverse [cross-srv]skip3-rows10-reverse [cross-srv]: 0.158× [0.155, 0.160]0.158× [0.155, 0.160]skip3-rows1000-forward [cross-srv]skip3-rows1000-forward [cross-srv]: 1.360× [1.295, 1.414]1.360× [1.295, 1.414]skip3-rows1000-reverse [cross-srv]skip3-rows1000-reverse [cross-srv]: 1.324× [1.250, 1.407]1.324× [1.250, 1.407]
95% CI below 1.0× 95% CI includes 1.0× (inconclusive) 95% CI above 1.0×
Figure M2.2: Client allocations per operation, USE_REMOTE_FETCH / SCAN_AND_FETCHBelow 1.0× means USE_REMOTE_FETCH allocates fewer Go objects.

USE_REMOTE_FETCH allocates less in 14 of 16 cases, down to 0.398×. That did not make the 1,000-row scans faster (Figure M2.1).

0.35×0.35×0.5×0.5×0.7×0.7×1.0×1.0×1.2×1.2×← USE_REMOTE_FETCH fewer allocsSCAN_AND_FETCH fewer allocs →M2rl: client Go allocs/opsmall-limit1-forwardsmall-limit1-forward: 1.040× [1.038, 1.041]1.040× [1.038, 1.041]small-limit1-reversesmall-limit1-reverse: 1.049× [1.047, 1.052]1.049× [1.047, 1.052]small-rows10-forwardsmall-rows10-forward: 0.596× [0.596, 0.597]0.596× [0.596, 0.597]small-rows10-reversesmall-rows10-reverse: 0.595× [0.595, 0.595]0.595× [0.595, 0.595]small-rows1000-forwardsmall-rows1000-forward: 0.431× [0.431, 0.431]0.431× [0.431, 0.431]small-rows1000-reversesmall-rows1000-reverse: 0.430× [0.430, 0.430]0.430× [0.430, 0.430]split-limit1-forwardsplit-limit1-forward: 0.942× [0.940, 0.943]0.942× [0.940, 0.943]split-limit1-reversesplit-limit1-reverse: 0.952× [0.951, 0.953]0.952× [0.951, 0.953]split-rows10-forwardsplit-rows10-forward: 0.505× [0.504, 0.505]0.505× [0.504, 0.505]split-rows10-reversesplit-rows10-reverse: 0.504× [0.504, 0.505]0.504× [0.504, 0.505]split-rows1000-forwardsplit-rows1000-forward: 0.399× [0.399, 0.399]0.399× [0.399, 0.399]split-rows1000-reversesplit-rows1000-reverse: 0.398× [0.398, 0.398]0.398× [0.398, 0.398]M2skip: client Go allocs/opskip3-rows10-forwardskip3-rows10-forward: 0.790× [0.790, 0.790]0.790× [0.790, 0.790]skip3-rows10-reverseskip3-rows10-reverse: 0.709× [0.709, 0.709]0.709× [0.709, 0.709]skip3-rows1000-forwardskip3-rows1000-forward: 0.529× [0.529, 0.529]0.529× [0.529, 0.529]skip3-rows1000-reverseskip3-rows1000-reverse: 0.527× [0.527, 0.527]0.527× [0.527, 0.527]
95% CI below 1.0× 95% CI includes 1.0× (inconclusive) 95% CI above 1.0×

M2rl: BenchmarkIndexFetch (final-remote / final-ordinary)

Geometric mean of six paired block ratios (median of 3 repeats per process, 3s benchtime), with nominal 95% block-bootstrap intervals (20,000 resamples, preregistered seed 492817; initial exploratory analysis with seed 391706 is retained in the campaign archive). Below 1 means USE_REMOTE_FETCH is faster or allocates less.

CaseSealed placement labelMean ns/opGo B/opGo allocs/op
small-limit1-forwardcross-server0.979 [0.964, 0.995]0.957 [0.956, 0.958]1.040 [1.038, 1.041]
small-limit1-reversecross-server0.985 [0.969, 1.003]0.958 [0.957, 0.959]1.049 [1.047, 1.052]
small-rows10-forwardcross-server1.031 [1.018, 1.042]0.759 [0.759, 0.760]0.596 [0.596, 0.597]
small-rows10-reversecross-server1.026 [1.013, 1.039]0.755 [0.755, 0.756]0.595 [0.595, 0.595]
small-rows1000-forwardcross-server1.226 [1.175, 1.280]0.532 [0.531, 0.534]0.431 [0.431, 0.431]
small-rows1000-reversecross-server1.255 [1.217, 1.293]0.527 [0.526, 0.528]0.430 [0.430, 0.430]
split-limit1-forwardcross-server1.110 [1.087, 1.131]0.715 [0.715, 0.716]0.942 [0.940, 0.943]
split-limit1-reversecross-server1.114 [1.095, 1.138]0.715 [0.715, 0.715]0.952 [0.951, 0.953]
split-rows10-forwardpartially colocated (f = 0.0000)0.873 [0.829, 0.920]0.695 [0.695, 0.695]0.505 [0.504, 0.505]
split-rows10-reversepartially colocated (f = 0.0000)0.854 [0.823, 0.886]0.695 [0.695, 0.695]0.504 [0.504, 0.505]
split-rows1000-forwardpartially colocated (f = 0.6960)2.326 [2.254, 2.397]0.692 [0.692, 0.692]0.399 [0.399, 0.399]
split-rows1000-reversepartially colocated (f = 0.6960)2.300 [2.206, 2.384]0.693 [0.692, 0.693]0.398 [0.398, 0.398]

M2skip: BenchmarkPlanSkip (final-remote / final-ordinary)

CaseSealed placement labelMean ns/opGo B/opGo allocs/op
skip3-rows10-forwardcross-server1.155 [0.991, 1.345]0.689 [0.688, 0.689]0.790 [0.790, 0.790]
skip3-rows10-reversecross-server0.158 [0.155, 0.160]0.590 [0.589, 0.590]0.709 [0.709, 0.709]
skip3-rows1000-forwardcross-server1.360 [1.295, 1.414]0.599 [0.598, 0.600]0.529 [0.529, 0.529]
skip3-rows1000-reversecross-server1.324 [1.250, 1.407]0.594 [0.593, 0.595]0.527 [0.527, 0.527]

All rows/op and prefix-B ratios in both M2rl and M2skip were 1.000 [1.000, 1.000] (prefix-B = 19 in M2rl, 22 in M2skip).

Absolute values

Medians of the six block medians per arm.

PhaseCasePlacement labelOrdinary ns/op (ms)Remote ns/op (ms)Ordinary B/opRemote B/opOrdinary allocs/opRemote allocs/op
M2rlsmall-limit1-forwardcross-server2.692.6217,62016,852222231
M2rlsmall-limit1-reversecross-server2.712.6617,66816,930223234
M2rlsmall-rows10-forwardcross-server3.223.3392,27470,033696415
M2rlsmall-rows10-reversecross-server3.223.3392,78470,044697415
M2rlsmall-rows1000-forwardcross-server95.10117.084,497,0802,393,97352,66922,677
M2rlsmall-rows1000-reversecross-server96.39118.954,544,5382,393,43852,66422,669
M2rlsplit-limit1-forwardcross-server3.764.20475,736340,354263248
M2rlsplit-limit1-reversecross-server3.964.41475,836340,297263250
M2rlsplit-rows10-forwardpartially colocated (f = 0.0000)6.836.004,651,9583,232,7461,050530
M2rlsplit-rows10-reversepartially colocated (f = 0.0000)6.815.774,652,3303,233,1121,051530
M2rlsplit-rows1000-forwardpartially colocated (f = 0.6960)316.38742.51457,265,229316,465,89679,12031,564
M2rlsplit-rows1000-reversepartially colocated (f = 0.6960)327.62768.95457,308,020316,621,14279,12531,476
M2skipskip3-rows10-forwardcross-server3.694.89186,950128,7331,175928
M2skipskip3-rows10-reversecross-server31.134.87218,190128,6481,307927
M2skipskip3-rows1000-forwardcross-server98.67138.195,380,2603,224,83667,03035,456
M2skipskip3-rows1000-reversecross-server101.06133.245,433,2623,226,72867,27835,446

Interpretation

  • 1,000-row scans on the 3-node double ssd-2 cluster:
    • On this 3-node ssd-2 cluster (which differs from the single-node campaign simultaneously in host topology, network, ssd-2 vs memory storage engine, double vs single redundancy, and storage-server count, so differences between the two campaigns are descriptive and cannot be attributed to one factor alone), every 1,000-row scan — both the cross-server cases (small-rows1000, skip3-rows1000) and the partially colocated (f = 0.6960) case (split-rows1000) — was 23% to 133% slower with USE_REMOTE_FETCH than with SCAN_AND_FETCH.
    • Untested hypothesis: in SCAN_AND_FETCH, the client pipelines GetRange point-range reads directly to the storage servers holding the primary records (with client-side load balancing across the two double replicas). In USE_REMOTE_FETCH, the storage server holding the index shard issues secondary-to-primary lookups to the storage processes holding the primary records (which for process-disjoint teams may be on another process on the same host or across the VPC, as per-lookup routing was not traced) and serializes the combined MappedKeyValue batches back through the index storage server; on 1,000-row and 100 MB split-record scans on ssd-2, that path may bottleneck compared with direct client-pipelined fetches.
  • Where USE_REMOTE_FETCH won:
    • skip3-rows10-reverse (0.158 [0.155, 0.160], 4.87 ms vs 31.13 ms): dramatically faster with remote fetch.
    • split-rows10 (0.873 forward, 0.854 reverse, where the 10 target records happened to be colocated on the same storage team as the index shard, f = 0.0000) and small-limit1 (0.979 forward).
    • Client-side Go heap allocations (B/op and allocs/op) were lower with USE_REMOTE_FETCH on every multi-row case (B/op 0.527–0.759, allocs/op 0.398–0.790).
  • Implication for defaults: these multi-node VPC measurements show that USE_REMOTE_FETCH is not a universal win and regressed 1,000-row scans in the measured configurations (1.226–2.326×), supporting retaining SCAN_AND_FETCH as the default for the measured workloads and conditions.