Replicas (segrep + remote store) and remote-store recoveries already
download files in parallel across files (RemoteStoreFileDownloader), but
each file is a single sequential stream, so a large segment file becomes
the long pole of every transfer. Add byte-range multi-part parallelism
within a file while leaving the existing per-file fan-out untouched.
The design deliberately differs from the multi-stream download removed
in opensearch-project#10519. That implementation wrote parts positionally into a temp
file, bypassing the Lucene Directory pipeline, which broke recovery
stats, Lucene checksum verification and encryption integration. Here
the parts are fetched in parallel but presented strictly in order:
- ParallelPartInputStream is a sequential InputStream over a blob that
streams the part being read directly (no buffering) and prefetches the
following parts on the REMOTE_RECOVERY pool into heap buffers via
BlobContainer#readBlob(name, position, length). Range reads work on
any blob store and return plaintext under server-side encryption.
- The stream is wrapped in RemoteIndexInput, so the unchanged
Directory#copyFrom path, ReplicationStatsDirectoryWrapper,
Store.StoreDirectory transfer stats, local directory-level encryption
and IndexOutput-side checksum verification all keep working.
- The reader never waits on work that has not started: a part whose
prefetch task is still queued is stolen and read by the reader, and
the task becomes a no-op. With no permits the stream degrades to a
plain sequential range-by-range download, so it is deadlock-free even
when driven from the same pool that runs the prefetches.
- ParallelDownloadPermits is a node-wide, dynamically resizable budget
of in-flight prefetched parts shared by all downloads on the node,
bounding heap at max_concurrent_parts * part_size.
New dynamic node settings:
- indices.recovery.remote_store.parallel_download.part_size
(default 16mb, 1mb..1gb); files no larger than one part keep the
single-stream path.
- indices.recovery.remote_store.parallel_download.max_concurrent_parts
(default max(1, allocated_processors / 2); 0 disables the feature).
RemoteStoreFileDownloader wraps a RemoteSegmentStoreDirectory source in
a FilterDirectory whose openInput returns the multi-part stream for
files larger than one part. DataFormatAwareRemoteDirectory routes the
range reads to the container of the blob's data format.
Signed-off-by: Bukhtawar Khan <bukhtawa@amazon.com>
Description
Adds multi-part parallel download of large segment files from the remote store, for segment-replication replicas and remote-store recoveries.
Today.
RemoteStoreFileDownloaderalready parallelises across files (bounded byindices.recovery.max_concurrent_remote_store_streams), but each file is one sequential stream viaDirectory.copyFrom. A large segment file (.fdt,.doc,.dvd,.cfs, ...) is therefore the long pole of every transfer: once the small files are done, the remaining streams sit idle while one thread drains one object.History. A multi-stream-within-a-file download existed once and was removed in #10519 because it wrote parts positionally into a temp file, bypassing the Lucene
Directorypipeline. That broke recovery stats, Lucene checksum verification and (client-side) encryption integration. With server-side encryption now the norm, the encryption concern no longer applies, and this PR is designed so the other two never arise.Why the #10519 problems do not come back. Both broke because the parts were written positionally via
FileChannel, so nothing hooked onDirectory#copyFrom/IndexOutputever saw the bytes. HereRemoteStoreFileDownloaderstill callsdestination.copyFrom(source, file, file, ctx)unchanged; only whatsource.openInput(file)returns differs. Consequently:ReplicationStatsDirectoryWrapper(countsreadByteson the source input to feed the progress tracker) andStore.StoreDirectory#copyFrom(theDirectoryFileTransferTrackerbehindremote_store.downloadnode stats) wrap the same call and see every byte, incrementally, including the failure path.IndexOutput, soStore#createVerifyingOutput/Store#verify(theMultiFileWriterpattern) work with no re-read; a byte flipped inside a prefetched part is caught by the running CRC, not just by the footer. The default remote-store download path does not verify at write time today and this PR keeps that posture unchanged — it makes write-time verification possible, not mandatory.Both properties are asserted end-to-end, each with a negative control, in
RemoteStoreMultiPartDownloadPipelineTests.Approach — parallel fetch, ordered write.
ParallelPartInputStreamis a sequentialInputStreamover a blob. The part currently being read is streamed directly (no buffering); the following parts are prefetched on theremote_recoverypool into heap buffers viaBlobContainer#readBlob(name, position, length). Plain byte-range reads work on every blob store and return plaintext under SSE — no dependence on how the object was multipart-uploaded.RemoteIndexInput, so the unchangedDirectory#copyFrompath consumes it.ReplicationStatsDirectoryWrapper,Store.StoreDirectorytransfer stats, local directory-level encryption andIndexOutput-side checksum verification all keep working because the write side is still a plain sequentialIndexOutput.remote_recoverypool that runs the prefetches.ParallelDownloadPermitsis a node-wide, dynamically resizable budget of in-flight prefetched parts shared by all downloads on the node. Heap held by parallel downloads is bounded bymax_concurrent_parts × part_size(128 MB on a 16-vCPU node with defaults).RemoteStoreFileDownloaderwraps aRemoteSegmentStoreDirectorysource in aFilterDirectorywhoseopenInputreturns the multi-part stream for files larger than one part.DataFormatAwareRemoteDirectoryroutes the range reads to the container of the blob's data format.New dynamic node settings
indices.recovery.remote_store.parallel_download.part_size16mb1mb..1gb; files ≤ one part keep the single-stream pathindices.recovery.remote_store.parallel_download.max_concurrent_partsmax(1, allocated_processors / 2)0disables the featureTests
ParallelPartInputStreamTests— byte-exact ordering across parts, single-byte reads, permit accounting under a gated executor, reader stealing queued prefetches (and stolen tasks being no-ops),RejectedExecutionExceptionfallback, prefetch-failure propagation with permit release, short-range-read detection,close()returning outstanding permits, rate-limiter wrapping every part.ParallelDownloadPermitsTests— acquire/release, zero budget, grow, shrink-takes-effect-as-released, negative rejected.RemoteStoreFileDownloaderTests— end-to-end over a realRemoteSegmentStoreDirectory+ FS blob container: multi-part files land byte-identical with valid Lucene footers (CodecUtil.checksumEntireFile), exactly one range request per part, small files still use the single-stream path, all permits returned; and themax_concurrent_parts = 0disabled path.RecoverySettingsDynamicUpdateTests— defaults, dynamic updates, bounds, and that the permit budget is resized in place (it is shared with in-flight downloads).RemoteStoreMultiPartDownloadPipelineTests— end-to-end over a realRemoteSegmentStoreDirectorywith a multi-part file and a single-stream file: (a)ReplicationStatsDirectoryWrapperprogress equals every file's length and is reported incrementally, andStore'sDirectoryFileTransferTrackerrecords started/succeeded bytes — plus a negative control where an injected range-read failure lands infailed_bytes; (b)Store#createVerifyingOutput+Store#verifypass on the multi-part stream with exactly one range request per part and no re-read — plus a negative control where one byte flipped inside a prefetched part (footer intact) is rejected withCorruptIndexException.:server:precommitpasses.Benchmark (EC2, S3 remote store)
Two 2-node clusters (r6g.2xlarge, 8 vCPU, ~2.5 Gbps baseline network; gp3 @ 1000 MB/s), 1 primary + 1 replica, segrep + remote store on S3 (us-east-1), same binary (this PR's head) on both. The only difference between arms is the dynamic setting
indices.recovery.remote_store.parallel_download.max_concurrent_parts—0is exactly today's code path.max_concurrent_remote_store_streams=4,part_size=16mb, rate limiters disabled. Replication lag sampled from_cat/segment_replicationevery 5 s (phase 1) / 2 s (phase 2). Zero replication failures in any run.Phase 1 — refresh-dominated (geonames, 11.4 M docs, default compound ratio,
refresh_interval=30s)parts=0parts=0(repeat)parts=4parts=8Neutral. Large merged segments were non-compound (~10 files each), so the existing cross-file fan-out already saturated the instance NIC; splitting files further cannot add throughput to a full pipe, and it did not regress anything.
Phase 2 — merge-heavy,
index.compound_format: true(2 × geonames = 22.8 M docs into one shard, then_forcemerge?max_num_segments=1→ a single 5.3 GB.cfs).cfsroundparts=0parts=4(default on 8 vCPU)parts=8When a replication round is dominated by one large file — a compound-format merged segment here — the baseline is bound to a single S3 connection (~90 MB/s). Multi-part download raises it to 136 MB/s at 4 parts and 186 MB/s at 8, i.e. ~2× faster catch-up on the 5.3 GB segment and the p90 round time halved (14.7 s → 7.0 s). The intermediate 1.1–1.5 GB
.cfsmerges show the same pattern (parts=0: 13.5 / 14.7 / 14.8 s;parts=4: 9.7 / 10.0 s;parts=8: 7.0 / 8.9 s). Replica-sideremote_store.downloadstats agree: ~21 GB downloaded per run in 222 s of download time atparts=0vs 81 s atparts=8. Atparts=8the replica was at ~255 MB/s, close to the r6g.2xlarge network allowance, so the gain is NIC-bounded on this instance size.Caveats: arms ran sequentially on one load generator (opensearch-benchmark allows one run per host); one run per arm in phase 2; round time includes local commit and reader reopen, not only the download. Heap on the replica was not sampled — the budget bound is
max_concurrent_parts × part_size(128 MB atparts=8).Summary: neutral when the replica's network is already saturated by cross-file parallelism; up to ~2× faster on rounds dominated by a single large file (compound-format segments, large
.fdt/.dvd-heavy merges), with no failures or regressions observed.Related Issues
no linked issue: follow-up to the design discussion in #10519, which removed the previous multi-stream download; no tracking issue exists yet.
Check List
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
For more information on following Developer Certificate of Origin and signing off your commits, please check here.