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 master (fa8725f1dcf6ff3ac66825ab6e3e819efbc0abcc), from one dedicated client host to three FoundationDB 7.3.77 server hosts (double redundancy, ssd-2, 9 storage servers). Both methods run in one master binary per benchmark. This is phase M2 (M2rl and M2skip) of fdbmn-20261020.

Cases and shard placement

  • M2rl (BenchmarkIndexFetch): small (unsplit) and split (100,000-byte padding, two chunks) records × limit1, rows10, rows1000 × forward/reverse: 12 cases.
  • M2skip (BenchmarkPlanSkip): Skip 3 × rows10, rows1000 × forward/reverse: 4 cases.
  • Fixtures: 1,000 Orders records, VALUE index price_idx, no record versions. Store prefixes are 19 bytes (M2rl) and 22 bytes (M2skip).
  • Placement: f is the fraction of fetched records whose storage-server team is disjoint from the index shard’s team. f = 1 means disjoint storage processes, not disjoint machines: with two replicas on three machines, any two teams share a machine.
    • Placement attempt 3 was selected by the preregistered rule (the first attempt where every cell passes, otherwise attempt 3).
    • Small and planskip stores: f = 1 on every cell.
    • Split store: f = 1 on every cell except rows1000, which is partially colocated at f = 0.820.
  • Data distribution stayed off for all of M2. Placement receipts taken before and after each eligible case matched.

Exclusions

Four of 16 cases are excluded whole, each by the pre-arm client CPU gate (15% on any single benchmark CPU, 5% aggregate):

PhaseCaseObserved
M2rlsplit-limit1-reverse15.2% max per CPU
M2rlsplit-rows10-forward15.8% max per CPU
M2rlsplit-rows1000-forward15.2% max per CPU
M2skipskip3-rows10-forward23.2% max per CPU, 6.78% aggregate

All twelve other cases have their complete six-block population and a clean R4 telemetry reconciliation. No case was rerun.

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, master.

USE_REMOTE_FETCH is slower on every 1,000-row scan (1.152–1.220 cross-server, 2.304 on the partially colocated f=0.820 split case), on split-rows10-reverse (1.326) and on skip3-rows10-reverse (1.169). It is faster on both one-row small fetches (0.976, 0.974). Four cases are excluded by the pre-arm client CPU gate.

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: BenchmarkIndexFetchsmall-limit1-forward [f=1.000]small-limit1-forward [f=1.000]: 0.976× [0.967, 0.986]0.976× [0.967, 0.986]small-limit1-reverse [f=1.000]small-limit1-reverse [f=1.000]: 0.974× [0.970, 0.978]0.974× [0.970, 0.978]small-rows10-forward [f=1.000]small-rows10-forward [f=1.000]: 1.018× [1.007, 1.029]1.018× [1.007, 1.029]small-rows10-reverse [f=1.000]small-rows10-reverse [f=1.000]: 0.995× [0.984, 1.007]0.995× [0.984, 1.007]small-rows1000-forward [f=1.000]small-rows1000-forward [f=1.000]: 1.213× [1.171, 1.260]1.213× [1.171, 1.260]small-rows1000-reverse [f=1.000]small-rows1000-reverse [f=1.000]: 1.213× [1.184, 1.242]1.213× [1.184, 1.242]split-limit1-forward [f=1.000]split-limit1-forward [f=1.000]: 1.009× [0.992, 1.030]1.009× [0.992, 1.030]split-limit1-reverse [f=1.000]N/A (excluded)split-rows10-forward [f=1.000]N/A (excluded)split-rows10-reverse [f=1.000]split-rows10-reverse [f=1.000]: 1.326× [1.292, 1.365]1.326× [1.292, 1.365]split-rows1000-forward [f=0.820]N/A (excluded)split-rows1000-reverse [f=0.820]split-rows1000-reverse [f=0.820]: 2.304× [2.247, 2.360]2.304× [2.247, 2.360]M2skip: BenchmarkPlanSkipskip3-rows10-forward [f=1.000]N/A (excluded)skip3-rows10-reverse [f=1.000]skip3-rows10-reverse [f=1.000]: 1.169× [1.151, 1.185]1.169× [1.151, 1.185]skip3-rows1000-forward [f=1.000]skip3-rows1000-forward [f=1.000]: 1.220× [1.198, 1.243]1.220× [1.198, 1.243]skip3-rows1000-reverse [f=1.000]skip3-rows1000-reverse [f=1.000]: 1.152× [1.110, 1.195]1.152× [1.110, 1.195]
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 fewer Go objects in 10 of 12 eligible cases, down to 0.454. The one-row small fetches allocate more (1.080, 1.090).

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.080× [1.080, 1.080]1.080× [1.080, 1.080]small-limit1-reversesmall-limit1-reverse: 1.090× [1.090, 1.090]1.090× [1.090, 1.090]small-rows10-forwardsmall-rows10-forward: 0.637× [0.636, 0.637]0.637× [0.636, 0.637]small-rows10-reversesmall-rows10-reverse: 0.636× [0.636, 0.637]0.636× [0.636, 0.637]small-rows1000-forwardsmall-rows1000-forward: 0.481× [0.481, 0.481]0.481× [0.481, 0.481]small-rows1000-reversesmall-rows1000-reverse: 0.481× [0.481, 0.481]0.481× [0.481, 0.481]split-limit1-forwardsplit-limit1-forward: 0.986× [0.983, 0.988]0.986× [0.983, 0.988]split-limit1-reverseN/A (excluded)split-rows10-forwardN/A (excluded)split-rows10-reversesplit-rows10-reverse: 0.547× [0.547, 0.548]0.547× [0.547, 0.548]split-rows1000-forwardN/A (excluded)split-rows1000-reversesplit-rows1000-reverse: 0.454× [0.454, 0.454]0.454× [0.454, 0.454]M2skip: client Go allocs/opskip3-rows10-forwardN/A (excluded)skip3-rows10-reverseskip3-rows10-reverse: 0.841× [0.841, 0.842]0.841× [0.841, 0.842]skip3-rows1000-forwardskip3-rows1000-forward: 0.583× [0.583, 0.584]0.583× [0.583, 0.584]skip3-rows1000-reverseskip3-rows1000-reverse: 0.583× [0.583, 0.583]0.583× [0.583, 0.583]
95% CI below 1.0× 95% CI includes 1.0× (inconclusive) 95% CI above 1.0×

Geometric mean of six paired block ratios (median of three testing.B repeats per process, 3 s benchtime), with nominal 95% block-bootstrap intervals (20,000 resamples, preregistered seed 492817). Below 1 means USE_REMOTE_FETCH is faster or allocates less. The metric is mean ns/op, not a latency percentile.

M2rl: BenchmarkIndexFetch (USE_REMOTE_FETCH / SCAN_AND_FETCH)

CasePlacementMean ns/opGo B/opGo allocs/op
small-limit1-forwardcross-server (f = 1.000)0.976 [0.967, 0.986]0.977 [0.976, 0.978]1.080 [1.080, 1.080]
small-limit1-reversecross-server (f = 1.000)0.974 [0.970, 0.978]0.976 [0.975, 0.977]1.090 [1.090, 1.090]
small-rows10-forwardcross-server (f = 1.000)1.018 [1.007, 1.029]0.788 [0.787, 0.788]0.637 [0.636, 0.637]
small-rows10-reversecross-server (f = 1.000)0.995 [0.984, 1.007]0.783 [0.782, 0.784]0.636 [0.636, 0.637]
small-rows1000-forwardcross-server (f = 1.000)1.213 [1.171, 1.260]0.565 [0.564, 0.566]0.481 [0.481, 0.481]
small-rows1000-reversecross-server (f = 1.000)1.213 [1.184, 1.242]0.559 [0.559, 0.560]0.481 [0.481, 0.481]
split-limit1-forwardcross-server (f = 1.000)1.009 [0.992, 1.030]0.716 [0.715, 0.716]0.986 [0.983, 0.988]
split-limit1-reversecross-server (f = 1.000)N/A (pre-arm client CPU gate)——
split-rows10-forwardcross-server (f = 1.000)N/A (pre-arm client CPU gate)——
split-rows10-reversecross-server (f = 1.000)1.326 [1.292, 1.365]0.698 [0.698, 0.699]0.547 [0.547, 0.548]
split-rows1000-forwardpartially colocated (f = 0.820)N/A (pre-arm client CPU gate)——
split-rows1000-reversepartially colocated (f = 0.820)2.304 [2.247, 2.360]0.693 [0.693, 0.693]0.454 [0.454, 0.454]

M2skip: BenchmarkPlanSkip (USE_REMOTE_FETCH / SCAN_AND_FETCH)

CasePlacementMean ns/opGo B/opGo allocs/op
skip3-rows10-forwardcross-server (f = 1.000)N/A (pre-arm client CPU gate)——
skip3-rows10-reversecross-server (f = 1.000)1.169 [1.151, 1.185]0.899 [0.899, 0.899]0.841 [0.841, 0.842]
skip3-rows1000-forwardcross-server (f = 1.000)1.220 [1.198, 1.243]0.634 [0.634, 0.635]0.583 [0.583, 0.584]
skip3-rows1000-reversecross-server (f = 1.000)1.152 [1.110, 1.195]0.629 [0.628, 0.630]0.583 [0.583, 0.583]

All rows/op and prefix-B ratios in both phases are 1.000 [1.000, 1.000]; they are audit quantities, not results.

  • Every 1,000-row scan is slower with USE_REMOTE_FETCH: small rows1000 by about 21% in both directions, Skip rows1000 by 15–22%, and the partially colocated split rows1000 reverse at 2.304.
  • On small rows1000, USE_REMOTE_FETCH allocates about 44% fewer Go bytes and 52% fewer Go objects per operation.
  • One-row small fetches are 2–3% faster with USE_REMOTE_FETCH (0.976 and 0.974).

Absolute values

Medians of each process’s three testing.B means, per arm.

PhaseCaseOrdinary msRemote msOrdinary B/opRemote B/opOrdinary allocs/opRemote allocs/op
M2rlsmall-limit1-forward3.1653.08715,98015,618187202
M2rlsmall-limit1-reverse3.1963.10616,05415,655188205
M2rlsmall-rows10-forward3.6543.71887,28668,748606386
M2rlsmall-rows10-reverse3.7373.70287,75868,706607386
M2rlsmall-rows1000-forward102.749123.6464,207,9362,377,13746,97222,590
M2rlsmall-rows1000-reverse101.429122.5514,257,7162,380,67246,97022,582
M2rlsplit-limit1-forward4.4284.448472,976338,430221218
M2rlsplit-rows10-reverse7.98310.6254,645,2083,244,086912500
M2rlsplit-rows1000-reverse359.495831.216456,772,766316,346,12567,95230,850
M2skipskip3-rows10-reverse4.4985.259140,950126,7021,054887
M2skipskip3-rows1000-forward103.323126.7125,033,9413,192,44660,60835,362
M2skipskip3-rows1000-reverse109.655126.7075,082,6493,196,06160,60835,353

Comparison with the 2026-10-14 campaign

The 2026-10-14 M2 ran both methods on old Go (267a41434); this campaign runs both on master. No old-versus-master M2 arm was measured in either campaign, so the table compares two cohorts and is descriptive only: no blocks are pooled and no cross-campaign interval is computed.

Casef, 2026-10-14 → 2026-10-20Remote/ordinary ns/op, 2026-10-14Remote/ordinary ns/op, 2026-10-20
M2rl/small-limit1-forward1.000 → 1.0000.979 [0.964, 0.995]0.976 [0.967, 0.986]
M2rl/small-limit1-reverse1.000 → 1.0000.985 [0.969, 1.003]0.974 [0.970, 0.978]
M2rl/small-rows10-forward1.000 → 1.0001.031 [1.018, 1.042]1.018 [1.007, 1.029]
M2rl/small-rows10-reverse1.000 → 1.0001.026 [1.013, 1.039]0.995 [0.984, 1.007]
M2rl/small-rows1000-forward1.000 → 1.0001.226 [1.175, 1.280]1.213 [1.171, 1.260]
M2rl/small-rows1000-reverse1.000 → 1.0001.255 [1.217, 1.293]1.213 [1.184, 1.242]
M2rl/split-limit1-forward1.000 → 1.0001.110 [1.087, 1.131]1.009 [0.992, 1.030]
M2rl/split-rows10-reverse0.000 → 1.0000.854 [0.823, 0.886]1.326 [1.292, 1.365]
M2rl/split-rows1000-reverse0.696 → 0.8202.300 [2.206, 2.384]2.304 [2.247, 2.360]
M2skip/skip3-rows10-reverse1.000 → 1.0000.158 [0.155, 0.160]1.169 [1.151, 1.185]
M2skip/skip3-rows1000-forward1.000 → 1.0001.360 [1.295, 1.414]1.220 [1.198, 1.243]
M2skip/skip3-rows1000-reverse1.000 → 1.0001.324 [1.250, 1.407]1.152 [1.110, 1.195]
  • Placement changed for two cases. split-rows10-reverse moved from f = 0 to f = 1 and split-rows1000-reverse from f = 0.696 to f = 0.820 (seven record shards then, six now). Their ratios are not a comparison of code.
  • Reverse Skip 10 rows is labelled cross-server in both cohorts, but the physical teams differ: index and record teams both occupied the node1/node2 pair on 2026-10-14, and span node1/node3 and node2/node3 now. Absolute ordinary/remote times were 31.127 ms / 4.873 ms then and 4.498 ms / 5.259 ms now.
  • Matched properties: the same frozen fixture schema, key schema and store prefixes; the same 12-case and 4-case geometry, method orders, repeats and benchtime; GOMAXPROCS=8 on CPUs 8–15. Key counts match (2,000 small, 3,000 split and 2,000 planskip keys); serialized value bytes were not compared.
  • Different: fresh hosts, freshly seeded data, different shard and replica layouts in all three stores, and the production revision (old Go then, master now).