Skip to content

Phase C1: Remote fetch (forward)

Part of the hcloud-rebench-20261014 campaign.

BenchmarkIndexFetch in two trees and two fetch methods, counterbalanced. USE_REMOTE_FETCH asks the storage server to resolve each index entry to its record (a mapped range read); SCAN_AND_FETCH scans the index and loads each record from the client. The question: what does remote fetch buy over the ordinary path, both in the tree that introduced it and in the final tree?

Arms

ArmSourceFetch method
pr2-ordinary5052212f520d1f888429d0bbcc749b657e27f39e (first remote-fetch tree)SCAN_AND_FETCH
pr2-remote5052212f520d1f888429d0bbcc749b657e27f39eUSE_REMOTE_FETCH
final-ordinary267a41434a81723b07e7150869d702636a1b91a9SCAN_AND_FETCH
final-remote267a41434a81723b07e7150869d702636a1b91a9USE_REMOTE_FETCH

Both trees ran byte-identical benchmark source (pkg/recordlayer/benchfetch, harness d94b4c61377d173b7c6884fde426a227dab0f0bb; see campaign-harness.md).

Environment and design

  • Host, CPU groups and network path as in phase A and phase B: Hetzner ccx43, one fdbserver 7.3.77 process on CPUs 2–7 with the memory engine and single redundancy, the benchmark process on CPUs 8–15 in a network namespace reaching the server over a veth pair.
  • Fixture: 1,000 records behind a value index, seeded and read back before any timer. small records are one key-value each; split records carry a 100,000-byte payload and are stored as two chunks. Every measured operation’s result (IDs, order, fields, payload checksum, terminal reason) is checked with the timer stopped, and an instrumented run before and after the loop asserts the path: remote fetch with no index scan or record load, or an index scan with record loads for SCAN_AND_FETCH.
  • Cases (all forward scans):
    • limit-1-broad: a scan over the whole index that stops after one row;
    • exact-10: IDs 100–109;
    • all-1000: all 1,000 records.
  • Per case: 8 blocks over the four arms in a Williams-square order (seed 641201), so each arm precedes each other arm equally often. One fresh process per arm and block, three repeats each (-test.count=3, 3 s per repeat): 24 samples per arm and case, 192 processes and 576 samples in total.
  • Estimand and bootstrap as in phase B: geometric mean over blocks of each block’s ratio of process medians, nominal 95% percentile bootstrap over blocks (20,000 resamples, seed 391706). Absolute values are the median over blocks of the process medians. ns/op is testing.B’s mean, not a percentile.

Results

Figure C1.1: Forward scans, USE_REMOTE_FETCH / SCAN_AND_FETCHBelow 1.0× favors USE_REMOTE_FETCH. Single in-memory server.

USE_REMOTE_FETCH is faster in 5 of 6 cases, down to 0.341×, and slower on split-all-1000 (1.150×).

0.3×0.3×0.5×0.5×0.7×0.7×1.0×1.0×1.3×1.3×← USE_REMOTE_FETCH lowerSCAN_AND_FETCH lower →Mean time (ns/op): final-remote / final-ordinarysmall-limit-1-broadsmall-limit-1-broad: 0.838× [0.795, 0.900]0.838× [0.795, 0.900]split-limit-1-broadsplit-limit-1-broad: 0.707× [0.685, 0.735]0.707× [0.685, 0.735]small-exact-10small-exact-10: 0.733× [0.724, 0.743]0.733× [0.724, 0.743]split-exact-10split-exact-10: 0.872× [0.843, 0.904]0.872× [0.843, 0.904]small-all-1000small-all-1000: 0.341× [0.321, 0.356]0.341× [0.321, 0.356]split-all-1000split-all-1000: 1.150× [1.129, 1.181]1.150× [1.129, 1.181]Go bytes (B/op): final-remote / final-ordinarysmall-limit-1-broadsmall-limit-1-broad: 0.960× [0.960, 0.961]0.960× [0.960, 0.961]split-limit-1-broadsplit-limit-1-broad: 0.715× [0.715, 0.715]0.715× [0.715, 0.715]small-exact-10small-exact-10: 0.751× [0.750, 0.751]0.751× [0.750, 0.751]split-exact-10split-exact-10: 0.695× [0.695, 0.695]0.695× [0.695, 0.695]small-all-1000small-all-1000: 0.512× [0.512, 0.512]0.512× [0.512, 0.512]split-all-1000split-all-1000: 0.692× [0.692, 0.692]0.692× [0.692, 0.692]Go allocations (allocs/op): final-remote / final-ordinarysmall-limit-1-broadsmall-limit-1-broad: 1.046× [1.046, 1.046]1.046× [1.046, 1.046]split-limit-1-broadsplit-limit-1-broad: 0.942× [0.940, 0.944]0.942× [0.940, 0.944]small-exact-10small-exact-10: 0.600× [0.600, 0.600]0.600× [0.600, 0.600]split-exact-10split-exact-10: 0.515× [0.514, 0.516]0.515× [0.514, 0.516]small-all-1000small-all-1000: 0.436× [0.436, 0.436]0.436× [0.436, 0.436]split-all-1000split-all-1000: 0.404× [0.404, 0.404]0.404× [0.404, 0.404]
95% CI below 1.0× 95% CI includes 1.0× (inconclusive) 95% CI above 1.0×

Ratios with nominal 95% intervals. Below 1 means the numerator arm is faster or allocates less.

Remote fetch vs ordinary fetch, final tree

final-remote / final-ordinary.

Casens/opB/opallocs/op
small-limit-1-broad0.838 [0.795, 0.900]0.960 [0.960, 0.961]1.046 [1.046, 1.046]
split-limit-1-broad0.707 [0.685, 0.735]0.715 [0.715, 0.715]0.942 [0.940, 0.944]
small-exact-100.733 [0.724, 0.743]0.751 [0.750, 0.751]0.600 [0.600, 0.600]
split-exact-100.872 [0.843, 0.904]0.695 [0.695, 0.695]0.515 [0.514, 0.516]
small-all-10000.341 [0.321, 0.356]0.512 [0.512, 0.512]0.436 [0.436, 0.436]
split-all-10001.150 [1.129, 1.181]0.692 [0.692, 0.692]0.404 [0.404, 0.404]

Remote fetch vs ordinary fetch, first remote-fetch tree

pr2-remote / pr2-ordinary.

Casens/opB/opallocs/op
small-limit-1-broad0.839 [0.810, 0.873]0.992 [0.992, 0.993]1.096 [1.096, 1.096]
split-limit-1-broad0.749 [0.721, 0.792]0.716 [0.716, 0.717]0.995 [0.993, 0.997]
small-exact-100.757 [0.746, 0.771]0.830 [0.829, 0.831]0.808 [0.808, 0.809]
split-exact-100.900 [0.863, 0.941]0.697 [0.697, 0.697]0.682 [0.681, 0.683]
small-all-10000.436 [0.423, 0.447]0.665 [0.665, 0.666]0.707 [0.707, 0.707]
split-all-10001.213 [1.182, 1.243]0.693 [0.693, 0.693]0.628 [0.628, 0.628]

Final vs first remote-fetch tree, per method

final-remote / pr2-remote:

Casens/opB/opallocs/op
small-limit-1-broad1.006 [0.978, 1.038]0.967 [0.966, 0.968]0.954 [0.954, 0.954]
split-limit-1-broad0.944 [0.891, 0.996]0.998 [0.997, 0.998]0.945 [0.944, 0.947]
small-exact-100.975 [0.962, 0.987]0.905 [0.905, 0.906]0.743 [0.742, 0.743]
split-exact-101.005 [0.952, 1.058]0.998 [0.997, 0.998]0.755 [0.754, 0.756]
small-all-10000.799 [0.770, 0.824]0.769 [0.769, 0.769]0.617 [0.617, 0.617]
split-all-10000.953 [0.932, 0.973]0.998 [0.997, 0.998]0.643 [0.643, 0.644]

final-ordinary / pr2-ordinary (control: the ordinary path):

Casens/opB/opallocs/op
small-limit-1-broad1.006 [0.925, 1.091]0.999 [0.999, 1.000]1.000 [1.000, 1.000]
split-limit-1-broad1.002 [0.978, 1.025]1.000 [0.999, 1.000]0.999 [0.997, 1.000]
small-exact-101.007 [0.997, 1.019]1.001 [1.000, 1.002]1.001 [1.000, 1.001]
split-exact-101.037 [1.013, 1.060]1.000 [1.000, 1.000]1.000 [0.999, 1.000]
small-all-10001.024 [1.010, 1.040]1.000 [0.999, 1.000]1.000 [1.000, 1.000]
split-all-10001.005 [0.966, 1.049]1.000 [1.000, 1.000]1.000 [1.000, 1.000]

Absolute values

Median over blocks of each process’s median of its three repeats.

Casepr2-ordinarypr2-remotefinal-ordinaryfinal-remote
small-limit-1-broad992 µs816 µs1.01 ms824 µs
split-limit-1-broad1.71 ms1.26 ms1.71 ms1.19 ms
small-exact-101.37 ms1.04 ms1.38 ms1.01 ms
split-exact-104.05 ms3.59 ms4.26 ms3.69 ms
small-all-100043.1 ms18.9 ms44.0 ms15.3 ms
split-all-1000162 ms196 ms163 ms188 ms

Final tree, allocations per operation:

CaseB/op ordinaryB/op remoteallocs/op ordinaryallocs/op remote
small-limit-1-broad17,67016,972219229
split-limit-1-broad478,972342,347265249
small-exact-1093,68470,348690414
split-exact-104,652,1123,233,8061,032532
small-all-10004,562,6222,335,13851,96622,664
split-all-1000457,420,264316,337,93877,73631,401

Interpretation

Measured:

  • Remote fetch is faster in five of six cases, in both trees. In the final tree it takes 0.71–0.87 of the ordinary path’s time for the one-row and ten-row scans and 0.34 for the 1,000 small records (44.0 → 15.3 ms).
  • One retained negative result: 1,000 split records. Remote fetch is 15% slower in the final tree (1.150 [1.129, 1.181]; 163 → 188 ms) and 21% slower in the first tree (1.213), although it allocates 31% fewer bytes and 60% fewer objects. Every one of the 8 block ratios is above 1.11.
  • The final tree improved remote fetch itself. final-remote/pr2-remote has allocs/op 0.62–0.95 in every case and time 0.80 [0.77, 0.82] for 1,000 small records and 0.95 [0.93, 0.97] for 1,000 split records. In the one-row and ten-row cases time is 0.94–1.01; only split-limit-1-broad (0.944 [0.891, 0.996]) and small-exact-10 (0.975 [0.962, 0.987]) have intervals below 1.
  • The ordinary path did not change between the trees in bytes or allocations (all ratios within 0.1% of 1). Its time ratios are 1.002–1.037; split-exact-10 (1.037 [1.013, 1.060]) and small-all-1000 (1.024 [1.010, 1.040]) have intervals above 1. With identical allocations, this run does not show where those 2–4% come from.

Hypotheses, not tested here:

  • For 1,000 split records, the storage server reads and returns about 100 MB per operation itself; on one fdbserver process that serial server-side work may cost more than the client-side pipelined loads it replaces. The allocation ratios show the client does less work; server CPU was not profiled.
  • The benefit measured here excludes inter-host and physical-network latency: client and server share a host and talk over a veth pair. Remote fetch saves client round trips, so its effect over a real network is likely different. The multi-node campaign (tools/benchcampaign/multinode) is designed to measure that.

Limits

  • Single host, one fdbserver process, single redundancy, memory engine: no replication, no network hop, no disk. One operation at a time.
  • testing.B reports means; no latency percentiles.
  • Eight blocks give limited resolution; intervals are nominal, per case and metric, with no multiplicity correction and no aggregate across cases. Numerator-first and denominator-first blocks agree in direction for every remote/ordinary time ratio; the largest spread is pr2 split-exact-10 (0.947 vs 0.855).
  • Case labels in the aggregate CSV were reconstructed from the pinned plan and the unchanged per-run logs, for the same runner bug described in phase B; 576 labels were restored in a separate working copy and no measurement was repeated or changed.