mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-25 05:26:50 +00:00
docs(internode): align transport adapter scope (#3064)
Co-authored-by: Henry Guo <marshawcoco@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,507 @@
|
||||
# RFC: Internode Transport Adapter Boundary
|
||||
|
||||
> Status: draft
|
||||
> Last updated: 2026-05-22
|
||||
> Scope: OSS internode data-plane adapter analysis, benchmark baseline, and
|
||||
> transport boundary
|
||||
|
||||
## Summary
|
||||
|
||||
The current distributed internode paths use TCP-based HTTP/gRPC transports:
|
||||
|
||||
- `tonic` gRPC `NodeService` for most control, metadata, lock, health, and
|
||||
peer operations.
|
||||
- HTTP streaming routes under `/rustfs/rpc/` for remote disk file streams.
|
||||
|
||||
This document frames the existing work as an OSS `InternodeDataTransport`
|
||||
adapter boundary. The adapter keeps RustFS data-plane logic separate from the
|
||||
concrete transport backend while preserving the current TCP/HTTP behavior as
|
||||
the default implementation.
|
||||
|
||||
Current implementation status:
|
||||
|
||||
- `InternodeDataTransport` exists in
|
||||
`crates/ecstore/src/rpc/internode_data_transport.rs`.
|
||||
- The default and only production backend is `tcp-http`; `tcp` is accepted as
|
||||
an alias.
|
||||
- `RUSTFS_INTERNODE_DATA_TRANSPORT` selects the backend. Blank or unset values
|
||||
use `tcp-http`; invalid values fail closed.
|
||||
- `RemoteDisk::read_file_stream`, `RemoteDisk::create_file`,
|
||||
`RemoteDisk::append_file`, and `RemoteDisk::walk_dir` delegate to the
|
||||
transport.
|
||||
- `NodeService` gRPC remains the internode control plane and continues to carry
|
||||
metadata/control operations.
|
||||
|
||||
Related design notes in this directory:
|
||||
|
||||
- `transport-capabilities.md`
|
||||
- `transport-buffer-lifecycle.md`
|
||||
- `transport-buffer-contract.md`
|
||||
- `transport-fallback-and-selection.md`
|
||||
|
||||
## Open-source Scope
|
||||
|
||||
The OSS scope is:
|
||||
|
||||
- define a clear `InternodeDataTransport` adapter boundary;
|
||||
- keep `tcp-http` as the default backend;
|
||||
- keep existing TCP/HTTP behavior unchanged;
|
||||
- keep internode data-plane behavior observable through metrics and baseline
|
||||
tooling;
|
||||
- document buffer ownership, fallback, and capability expectations for
|
||||
maintainable transport code;
|
||||
- avoid hardware-specific dependencies or backend implementations.
|
||||
|
||||
The OSS scope is not:
|
||||
|
||||
- RDMA support;
|
||||
- DPU support;
|
||||
- DOCA support;
|
||||
- BlueField support;
|
||||
- RoCE/InfiniBand support;
|
||||
- hardware benchmark planning;
|
||||
- hardware-specific backend implementation.
|
||||
|
||||
Hardware-specific transport experiments are outside the scope of this OSS
|
||||
document.
|
||||
|
||||
## Goals
|
||||
|
||||
- Document the current internode control plane and data plane.
|
||||
- Identify the existing transfer paths covered by the
|
||||
`InternodeDataTransport` adapter and the paths that remain on gRPC.
|
||||
- Define the minimum benchmark baseline required before transport changes.
|
||||
- Sketch a pluggable transport boundary that preserves the current TCP/HTTP
|
||||
behavior as the default backend.
|
||||
- Document backend-neutral capability, fallback, buffer ownership, and
|
||||
observability expectations.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Implement RDMA, RoCE, InfiniBand, DPU, DOCA, DPDK, SPDK, or SmartNIC support.
|
||||
- Replace `tonic` gRPC for control-plane RPCs.
|
||||
- Redesign erasure coding, quorum handling, disk health tracking, or object
|
||||
correctness semantics.
|
||||
- Require specialized hardware for default development, CI, or ordinary RustFS
|
||||
deployments.
|
||||
|
||||
## Current Internode Architecture
|
||||
|
||||
### Server-side entry points
|
||||
|
||||
The main HTTP server builds a hybrid service per connection:
|
||||
|
||||
- `rustfs/src/server/http.rs` wires a `NodeServiceServer` for gRPC.
|
||||
- `rustfs/src/storage/rpc/InternodeRpcService` intercepts HTTP paths under
|
||||
`/rustfs/rpc/`.
|
||||
- Other HTTP/S3 traffic continues through the normal S3 service.
|
||||
|
||||
Compression logic already treats `/rustfs/rpc/` and `/rustfs/peer/` as internode
|
||||
RPC paths and skips normal response compression for them.
|
||||
|
||||
### gRPC channel management
|
||||
|
||||
`crates/protos/src/lib.rs` creates internode gRPC channels with `tonic`
|
||||
`Endpoint`:
|
||||
|
||||
- connect timeout
|
||||
- TCP keepalive
|
||||
- HTTP/2 keepalive interval and timeout
|
||||
- request timeout
|
||||
- optional TLS configuration
|
||||
- global channel caching and failed-connection eviction
|
||||
|
||||
This confirms the current gRPC transport is TCP/HTTP2-based.
|
||||
|
||||
### NodeService layout
|
||||
|
||||
`crates/protos/src/node.proto` defines one `NodeService` that mixes several
|
||||
classes of RPCs:
|
||||
|
||||
- meta service: bucket and metadata operations
|
||||
- disk service: local/remote disk operations
|
||||
- lock service: distributed lock operations
|
||||
- peer rest service: node health, metrics, IAM/policy reload, rebalance,
|
||||
profiling, events, and admin-style operations
|
||||
|
||||
The service layout is practical today, but it is too broad to become the
|
||||
transport adapter surface. A pluggable data transport should target only disk
|
||||
data streams and keep this gRPC service as the control plane.
|
||||
|
||||
## Control Plane vs Data Plane
|
||||
|
||||
### Control plane
|
||||
|
||||
These paths carry coordination, metadata, health, and administrative state.
|
||||
They should remain on gRPC/TCP:
|
||||
|
||||
| Area | Client/server code | Examples | Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| Bucket peer ops | `crates/ecstore/src/rpc/peer_s3_client.rs`, `rustfs/src/storage/rpc/bucket.rs` | `MakeBucket`, `ListBucket`, `DeleteBucket`, `GetBucketInfo`, `HealBucket` | Small metadata/control payloads. |
|
||||
| Locking | `crates/ecstore/src/rpc/remote_locker.rs`, `rustfs/src/storage/rpc/lock.rs` | `Lock`, `UnLock`, `Refresh`, batch lock/unlock | Latency-sensitive but not bulk data; correctness and timeout semantics matter more than transport bandwidth. |
|
||||
| Peer/admin state | `crates/ecstore/src/rpc/peer_rest_client.rs`, `rustfs/src/storage/rpc/health.rs`, `metrics.rs`, `event.rs` | `LocalStorageInfo`, `ServerInfo`, `GetMetrics`, `GetLiveEvents`, reload APIs, rebalance APIs | Operational control plane. |
|
||||
| Disk metadata/control | `crates/ecstore/src/rpc/remote_disk.rs`, `rustfs/src/storage/rpc/disk.rs` | `DiskInfo`, `ReadXL`, `ReadVersion`, `ReadMetadata`, `WriteMetadata`, `RenameFile`, `RenamePart`, `Delete*`, `VerifyFile`, `CheckParts` | Usually metadata, integrity checks, or namespace mutations. |
|
||||
| Connection health | `RemoteDisk`, `RemotePeerS3Client`, `PeerRestClient` | TCP connectivity probes and fault/recovery state | Must remain available even if an optional data backend is unavailable. |
|
||||
|
||||
### Data plane candidates
|
||||
|
||||
These paths move object shard bytes or stream potentially large disk data and
|
||||
are the only reasonable first candidates for a pluggable transport.
|
||||
|
||||
| Priority | Path | Current client | Current server | Current transport | Why it matters |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| P0 | `read_file_stream` | `RemoteDisk::read_file_stream` | `handle_read_file` in `http_service.rs` | HTTP `GET /rustfs/rpc/read_file_stream` with a streaming response body | Main remote disk read stream used by bitrot readers and erasure reads. |
|
||||
| P0 | `put_file_stream` | `RemoteDisk::create_file` and `RemoteDisk::append_file` | `handle_put_file` in `http_service.rs` | HTTP `PUT /rustfs/rpc/put_file_stream` with a streaming request body | Main remote disk write stream used by bitrot writers and erasure writes. |
|
||||
| P1 | `walk_dir` | `RemoteDisk::walk_dir` | `handle_walk_dir` in `http_service.rs` | HTTP `GET /rustfs/rpc/walk_dir` with a streamed metadata listing | Can be high-volume during scans/healing, but it is metadata-oriented rather than object byte data. |
|
||||
| P1 | `ReadAll` / `WriteAll` | `RemoteDisk::read_all` / `write_all` | gRPC unary disk handlers | gRPC unary `bytes` payload | Moves bytes today, but should be measured before treating it as a high-throughput data path. |
|
||||
| P2 | proto `WriteStream` / `ReadAt` | currently not used | currently returns unimplemented | gRPC streaming definitions exist but are not implemented | Possible future API shape, not a current production path. |
|
||||
|
||||
## P1 Data Path Inventory
|
||||
|
||||
Classification:
|
||||
|
||||
- Covered by `InternodeDataTransport`: `RemoteDisk` opens the transfer through
|
||||
the transport abstraction.
|
||||
- Still direct TCP/HTTP/gRPC: bytes move over a fixed internode protocol
|
||||
outside the transport abstraction.
|
||||
- Metadata/control-plane only: payloads are expected to be small metadata,
|
||||
namespace, lock, health, or admin messages.
|
||||
- Not relevant: declared or test-only paths that are not current production
|
||||
data paths.
|
||||
|
||||
### Covered by `InternodeDataTransport`
|
||||
|
||||
| Path | Owner references | Server references | Classification | Notes |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Remote shard read stream | `crates/ecstore/src/rpc/remote_disk.rs::RemoteDisk::read_file_stream`; `crates/ecstore/src/rpc/internode_data_transport.rs::InternodeDataTransport::open_read`; `crates/ecstore/src/bitrot.rs::create_bitrot_reader` | `rustfs/src/storage/rpc/http_service.rs::handle_read_file` | Covered by `InternodeDataTransport` | Object GET, repair reads, and erasure decode use this path for remote shard bytes. |
|
||||
| Remote shard write stream | `RemoteDisk::create_file`; `RemoteDisk::append_file`; `InternodeDataTransport::open_write`; `crates/ecstore/src/bitrot.rs::create_bitrot_writer` | `rustfs/src/storage/rpc/http_service.rs::handle_put_file` | Covered by `InternodeDataTransport` | Object PUT and multipart part upload use this path for remote shard bytes. |
|
||||
| Remote namespace walk stream | `RemoteDisk::walk_dir`; `InternodeDataTransport::open_walk_dir`; `crates/ecstore/src/cache_value/metacache_set.rs` walk producers | `rustfs/src/storage/rpc/http_service.rs::handle_walk_dir` | Covered by `InternodeDataTransport` | High-volume listing/scanner/heal metadata stream. It is not object byte data, but it is a large internode stream. |
|
||||
| Remote zero-copy read fallback | `RemoteDisk::read_file_zero_copy` | same as remote shard read stream | Covered by `InternodeDataTransport` through `read_file_stream` | The remote path buffers the stream into `Bytes`; true zero-copy is not guaranteed for remote disks. |
|
||||
|
||||
### Still Direct TCP/HTTP/gRPC
|
||||
|
||||
| Path | Owner references | Server references | Classification | Notes |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `ReadAll` | `RemoteDisk::read_all`; `crates/ecstore/src/store_init.rs`; heal resume metadata readers | `rustfs/src/storage/rpc/disk.rs::handle_read_all` | Still direct gRPC | Unary `bytes` response. Currently used mostly for metadata/config files; measure before moving. |
|
||||
| `WriteAll` | `RemoteDisk::write_all`; `crates/ecstore/src/store_init.rs`; heal resume metadata writers | `rustfs/src/storage/rpc/disk.rs::handle_write_all` | Still direct gRPC | Unary `bytes` request. Currently used mostly for metadata/config/checkpoint writes. |
|
||||
| `ReadMultiple` | `RemoteDisk::read_multiple`; `crates/ecstore/src/set_disk/read.rs::read_multiple_files` | `rustfs/src/storage/rpc/disk.rs::handle_read_multiple` | Still direct gRPC | Returns multiple small file payloads, usually metadata/listing support. Could become large with many entries. |
|
||||
| `ReadParts` | `RemoteDisk::read_parts`; `crates/ecstore/src/set_disk/read.rs::read_parts`; multipart list/complete paths | `rustfs/src/storage/rpc/disk.rs::handle_read_parts` | Still direct gRPC | Encoded `ObjectPartInfo` metadata, not object data. |
|
||||
| `RenamePart` | `RemoteDisk::rename_part`; `crates/ecstore/src/set_disk/write.rs::rename_part` | `rustfs/src/storage/rpc/disk.rs::handle_rename_part` | Still direct gRPC | Carries part metadata while committing multipart data already written through stream writers. |
|
||||
| `ListDir` | `RemoteDisk::list_dir`; multipart/lifecycle metadata listing callers | `rustfs/src/storage/rpc/disk.rs::handle_list_dir` | Still direct gRPC | Directory name listing, metadata/control-plane unless measured otherwise. |
|
||||
| Legacy gRPC `WalkDir` | `rustfs/src/storage/rpc/node_service.rs::NodeService::walk_dir` | same file | Still direct gRPC | Server implementation remains, but current `RemoteDisk::walk_dir` uses HTTP through the transport. Keep until callers are audited or compatibility policy is set. |
|
||||
|
||||
### Metadata/control-plane only
|
||||
|
||||
| Area | Owner references | Classification | Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| Disk metadata and namespace mutations | `RemoteDisk::{read_metadata,write_metadata,update_metadata,read_version,read_xl,rename_data,rename_file,delete*,verify_file,check_parts,disk_info}` | Metadata/control-plane only | These remain on gRPC by design. |
|
||||
| Peer/bucket/admin operations | `crates/ecstore/src/rpc/{peer_s3_client.rs,peer_rest_client.rs,remote_locker.rs}` and matching `rustfs/src/storage/rpc/*` handlers | Metadata/control-plane only | Not candidates for a data-plane backend without separate measurements. |
|
||||
| Store init and format operations | `crates/ecstore/src/store_init.rs` | Metadata/control-plane only | Uses `ReadAll`/`WriteAll` for small format/config objects. |
|
||||
| Heal orchestration | `crates/heal/src/heal/storage.rs` and `crates/ecstore/src/set_disk.rs::heal_object` | Metadata/control-plane plus covered data reads | Heal object data reads go through `get_object_reader` and then covered shard streams; resume/checkpoint metadata uses direct gRPC disk metadata calls. |
|
||||
|
||||
### Not Relevant Current Paths
|
||||
|
||||
| Path | Owner references | Classification | Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| Proto `Write` | `crates/protos/src/node.proto`; `rustfs/src/storage/rpc/disk.rs::handle_write` | Not relevant | Handler is unimplemented. |
|
||||
| Proto `WriteStream` | `crates/protos/src/node.proto`; `rustfs/src/storage/rpc/node_service.rs::write_stream` | Not relevant | Returns `unimplemented`. |
|
||||
| Proto `ReadAt` | `crates/protos/src/node.proto`; `rustfs/src/storage/rpc/node_service.rs::read_at` | Not relevant | Returns `unimplemented`. |
|
||||
| E2E reliant gRPC helpers | `crates/e2e_test/src/reliant/*` | Not relevant | Test harnesses, not production internode data-path callers. |
|
||||
|
||||
### Current Limitations
|
||||
|
||||
| Risk | Limitation |
|
||||
| --- | --- |
|
||||
| Medium | `ReadAll` and `WriteAll` still carry unary `bytes` over gRPC. They appear metadata-oriented today, but there is no size threshold or routing policy. |
|
||||
| Medium | `ReadMultiple` can aggregate many metadata files into one gRPC response. |
|
||||
| Low | Legacy gRPC `WalkDir` remains implemented while `RemoteDisk::walk_dir` uses HTTP through the transport. |
|
||||
| Medium | Remote `read_file_zero_copy` is a buffered read over the transport, not a remote zero-copy contract. |
|
||||
| Medium | Server-side TCP HTTP route handling is outside the client-side trait. |
|
||||
|
||||
## Current Object Write Path
|
||||
|
||||
For object PUTs in distributed erasure mode, the relevant flow is:
|
||||
|
||||
1. Upper storage layers prepare object data and erasure metadata.
|
||||
2. `SetDisks` selects local and remote disks.
|
||||
3. `create_bitrot_writer` calls `disk.create_file(...)` for each shard writer.
|
||||
4. For a remote disk, `RemoteDisk::create_file` delegates to
|
||||
`InternodeDataTransport::open_write`.
|
||||
5. `HttpWriter` sends an HTTP `PUT` to `/rustfs/rpc/put_file_stream`.
|
||||
6. The remote node's `handle_put_file` opens the local file writer and copies
|
||||
incoming body chunks into it.
|
||||
7. `Erasure::encode` writes shards through `MultiWriter` to all selected
|
||||
writers while enforcing write quorum.
|
||||
|
||||
This is the primary write data-plane candidate.
|
||||
|
||||
## Current Object Read Path
|
||||
|
||||
For object GETs and repair reads in distributed erasure mode, the relevant flow is:
|
||||
|
||||
1. `SetDisks` prepares shard readers for the selected disks.
|
||||
2. `create_bitrot_reader` uses local zero-copy only when `disk.is_local()`.
|
||||
3. For a remote disk, it calls `disk.read_file_stream(...)`.
|
||||
4. `RemoteDisk::read_file_stream` delegates to
|
||||
`InternodeDataTransport::open_read`.
|
||||
5. `HttpReader` sends an HTTP `GET` to `/rustfs/rpc/read_file_stream`.
|
||||
6. The remote node's `handle_read_file` opens the local disk stream and returns
|
||||
it as an HTTP streaming body.
|
||||
7. The erasure decoder reads from the shard streams and reconstructs the object.
|
||||
|
||||
This is the primary read data-plane candidate.
|
||||
|
||||
## Existing Metrics and Benchmark Surface
|
||||
|
||||
RustFS already has coarse internode metrics in `crates/io-metrics/src/internode_metrics.rs`:
|
||||
|
||||
- sent bytes
|
||||
- received bytes
|
||||
- outgoing requests
|
||||
- incoming requests
|
||||
- errors
|
||||
- dial errors
|
||||
- average dial time
|
||||
|
||||
These metrics are useful as a starting point. For backend comparisons, the
|
||||
relevant route-level and operation-level dimensions are:
|
||||
|
||||
- `read_file_stream`
|
||||
- `put_file_stream`
|
||||
- `walk_dir`
|
||||
- gRPC `ReadAll` / `WriteAll`
|
||||
- gRPC control-plane request volume
|
||||
|
||||
Existing benchmark assets:
|
||||
|
||||
- `scripts/run_object_batch_bench.sh`
|
||||
- `scripts/run_object_batch_bench_enhanced.sh`
|
||||
- `scripts/run_object_batch_bench_abc.sh`
|
||||
- `scripts/run_four_node_cluster_failover_bench.sh`
|
||||
- `scripts/run_internode_transport_baseline.sh` (scenario matrix wrapper for local vs distributed TCP baseline artifacts)
|
||||
- Criterion benches under `crates/ecstore/benches/`
|
||||
|
||||
These mostly cover S3/object workload or erasure coding performance. They do
|
||||
not yet isolate internode transport cost.
|
||||
|
||||
## Required TCP Baseline
|
||||
|
||||
Before changing internode data transport behavior or comparing a non-default
|
||||
backend, collect a baseline for the current TCP/HTTP/gRPC implementation.
|
||||
|
||||
### Topology
|
||||
|
||||
Minimum:
|
||||
|
||||
- 1-node local erasure deployment, to measure local disk and erasure overhead.
|
||||
- 4-node distributed erasure deployment, to measure internode overhead.
|
||||
|
||||
Preferred:
|
||||
|
||||
- Same host count and disk layout for every run.
|
||||
- Dedicated network interface or isolated VLAN.
|
||||
- Fixed CPU governor and no unrelated background load.
|
||||
- Recorded kernel version, NIC model, MTU, RustFS commit, Rust toolchain, and
|
||||
benchmark tool versions.
|
||||
|
||||
### Workloads
|
||||
|
||||
| Workload | Sizes | Concurrency | Main signal |
|
||||
| --- | --- | --- | --- |
|
||||
| S3 PUT | 4 KiB, 1 MiB, 16 MiB, 128 MiB, 1 GiB | 1, 16, 64, 128 | End-to-end write throughput and tail latency. |
|
||||
| S3 GET | 4 KiB, 1 MiB, 16 MiB, 128 MiB, 1 GiB | 1, 16, 64, 128 | End-to-end read throughput and tail latency. |
|
||||
| Remote disk stream read | shard-sized ranges from `read_file_stream` | 1, 16, 64 | Isolated internode read path. |
|
||||
| Remote disk stream write | shard-sized writes through `put_file_stream` | 1, 16, 64 | Isolated internode write path. |
|
||||
| Healing / repair | missing disk or missing shard scenario | controlled | Rebuild throughput and read/write amplification. |
|
||||
| Scanner walk | large bucket/object namespace | controlled | Metadata streaming pressure, not the primary object-byte transport path. |
|
||||
|
||||
### Measurements
|
||||
|
||||
Collect:
|
||||
|
||||
- throughput in bytes/s and objects/s
|
||||
- p50, p95, p99, and max latency
|
||||
- CPU utilization per process and per core
|
||||
- memory RSS and allocation pressure where available
|
||||
- `rustfs_system_network_internode_*` metrics
|
||||
- TCP retransmits, socket errors, and NIC throughput
|
||||
- disk throughput and utilization
|
||||
- failure/retry/fallback counts
|
||||
|
||||
The baseline should produce a machine-readable artifact, for example
|
||||
`target/bench/internode-transport/<timestamp>/summary.csv`, plus the exact
|
||||
commands and configuration used.
|
||||
|
||||
### Baseline runner entry point
|
||||
|
||||
Use `scripts/run_internode_transport_baseline.sh` to execute a reproducible
|
||||
S3 PUT/GET matrix against `local` and `distributed` scenarios and export:
|
||||
|
||||
- `summary.csv` (throughput/latency summary per workload and object size)
|
||||
- `internode_metric_deltas.csv` (operation-level internode metric deltas when
|
||||
`--metrics-url` is provided)
|
||||
|
||||
## Transport Abstraction Proposal
|
||||
|
||||
### Design principle
|
||||
|
||||
Keep `NodeService` as the control plane. Introduce a separate data transport
|
||||
only below `RemoteDisk`, where remote disk byte streams are opened today.
|
||||
|
||||
The first implementation should be a no-behavior-change TCP/HTTP backend that
|
||||
wraps the current `HttpReader`, `HttpWriter`, and `/rustfs/rpc/*` handlers.
|
||||
Non-default backend work should not proceed until the default wrapper is
|
||||
measured and adapter gaps are documented.
|
||||
|
||||
### Candidate boundary
|
||||
|
||||
The current boundary is remote disk stream transfer:
|
||||
|
||||
```rust
|
||||
#[async_trait::async_trait]
|
||||
pub trait InternodeDataTransport: Send + Sync + std::fmt::Debug {
|
||||
async fn open_read(&self, request: ReadStreamRequest) -> Result<FileReader>;
|
||||
async fn open_write(&self, request: WriteStreamRequest) -> Result<FileWriter>;
|
||||
async fn open_walk_dir(&self, request: WalkDirStreamRequest) -> Result<FileReader>;
|
||||
fn name(&self) -> &'static str;
|
||||
fn capabilities(&self) -> InternodeDataTransportCapabilities;
|
||||
}
|
||||
```
|
||||
|
||||
Initial request fields should mirror the current HTTP query parameters:
|
||||
|
||||
- peer endpoint
|
||||
- disk reference
|
||||
- volume
|
||||
- path
|
||||
- offset
|
||||
- length
|
||||
- append/create mode
|
||||
- expected size
|
||||
- optional stall timeout for long-running listing streams
|
||||
|
||||
The initial TCP backend can keep the current signed HTTP URLs internally.
|
||||
|
||||
### Integration point
|
||||
|
||||
`RemoteDisk` delegates only these methods to the data transport:
|
||||
|
||||
- `read_file_stream`
|
||||
- `read_file_zero_copy` as a wrapper over `read_file_stream` unless the backend
|
||||
supports a stronger zero-copy API
|
||||
- `append_file`
|
||||
- `create_file`
|
||||
- `walk_dir`
|
||||
|
||||
All other `RemoteDisk` methods continue using the current gRPC client
|
||||
until measurements prove otherwise.
|
||||
|
||||
### Capability model
|
||||
|
||||
Avoid hard-coding transport-specific assumptions into the generic interface.
|
||||
Use capabilities:
|
||||
|
||||
- stream read
|
||||
- stream write
|
||||
- bounded range read
|
||||
- bidirectional streaming
|
||||
- backend-specific buffer registration or staging requirements
|
||||
- stable buffer ownership support
|
||||
- copy-reduced receive into caller-owned or backend-owned buffers
|
||||
- authenticated out-of-band transfer
|
||||
- transport fallback support
|
||||
|
||||
The first TCP backend should report only capabilities that it actually provides.
|
||||
|
||||
## TCP Fallback Requirements
|
||||
|
||||
TCP/HTTP/gRPC must remain the default and required backend.
|
||||
|
||||
Fallback rules:
|
||||
|
||||
- If no explicit data transport is configured, use the current TCP/HTTP
|
||||
implementation.
|
||||
- The current accepted values for `RUSTFS_INTERNODE_DATA_TRANSPORT` are
|
||||
`tcp-http` and the `tcp` alias. Empty and unset values use `tcp-http`.
|
||||
- Invalid configured values fail closed with an error that includes the env var
|
||||
name and invalid value.
|
||||
- If a future non-default backend fails initialization, either fail fast with a
|
||||
clear error or fall back to TCP only when the configured policy allows
|
||||
fallback.
|
||||
- Runtime fallback must preserve object correctness and quorum semantics.
|
||||
- Fallback events must be logged and counted in metrics.
|
||||
- CI and local development must not require specialized transport hardware.
|
||||
|
||||
Suggested future configuration shape:
|
||||
|
||||
```text
|
||||
RUSTFS_INTERNODE_DATA_TRANSPORT=tcp-http
|
||||
RUSTFS_INTERNODE_DATA_TRANSPORT_FALLBACK=tcp
|
||||
```
|
||||
|
||||
Do not add fallback settings until there is an implementation PR that uses them.
|
||||
|
||||
## Baseline Validation Commands
|
||||
|
||||
Dry-run command:
|
||||
|
||||
```bash
|
||||
scripts/run_internode_transport_baseline.sh \
|
||||
--access-key minioadmin \
|
||||
--secret-key minioadmin \
|
||||
--scenarios local=http://127.0.0.1:9000,distributed=http://127.0.0.1:9001 \
|
||||
--sizes 4KiB,1MiB \
|
||||
--concurrencies 1 \
|
||||
--duration 10s \
|
||||
--dry-run
|
||||
```
|
||||
|
||||
Real TCP baseline command with metrics:
|
||||
|
||||
```bash
|
||||
RUSTFS_INTERNODE_DATA_TRANSPORT=tcp-http \
|
||||
scripts/run_internode_transport_baseline.sh \
|
||||
--access-key "$RUSTFS_ACCESS_KEY" \
|
||||
--secret-key "$RUSTFS_SECRET_KEY" \
|
||||
--scenarios local=http://127.0.0.1:9000,distributed=http://127.0.0.1:9001 \
|
||||
--metrics-url http://127.0.0.1:9000/metrics \
|
||||
--out-dir target/bench/internode-transport/manual-run
|
||||
```
|
||||
|
||||
Expected artifacts:
|
||||
|
||||
- `run_manifest.txt`
|
||||
- `summary.csv`
|
||||
- `internode_metric_deltas.csv` when `--metrics-url` is provided
|
||||
|
||||
The baseline validates the default TCP/HTTP path only. It must not be used to
|
||||
claim support or performance for any non-default transport backend.
|
||||
|
||||
## Non-default Backend Boundary
|
||||
|
||||
A future non-default backend must be explicitly enabled and must not replace
|
||||
`tcp-http` silently. It should be designed as an optional data-plane backend,
|
||||
not as a replacement for the gRPC control plane.
|
||||
|
||||
A future non-default backend would need an explicit design for:
|
||||
|
||||
- peer capability discovery over the existing gRPC control plane;
|
||||
- connection management and health mapping into existing disk fault handling;
|
||||
- backend-specific buffer lifecycle and any staging or registration cache;
|
||||
- buffer ownership, alignment, and lifetime rules;
|
||||
- stable buffer behavior for erasure shards;
|
||||
- authentication and authorization for out-of-band data transfers;
|
||||
- encryption or an equivalent documented security boundary;
|
||||
- timeout, cancellation, retry, and fallback behavior;
|
||||
- metrics for transfer latency, bytes, queue depth, retries, fallback, and
|
||||
errors.
|
||||
|
||||
`walk_dir`, metadata RPCs, locks, admin RPCs, and bucket coordination remain
|
||||
outside the current data-plane boundary.
|
||||
|
||||
## Out of Scope
|
||||
|
||||
Hardware-specific transport experiments are outside the scope of this OSS
|
||||
document. This RFC does not add a plugin system, split the adapter into a
|
||||
separate crate, add accepted backend values, or implement a new transport
|
||||
backend.
|
||||
@@ -0,0 +1,142 @@
|
||||
# Internode Transport Buffer Contract
|
||||
|
||||
Status: design note only. This document defines a backend-neutral buffer
|
||||
ownership and lifecycle contract for the `InternodeDataTransport` adapter. It
|
||||
does not implement a new backend and does not change production behavior.
|
||||
|
||||
## Open-source Scope
|
||||
|
||||
The open-source RustFS path keeps `tcp-http` as the default internode data
|
||||
transport. This document defines adapter contracts only:
|
||||
|
||||
- no production RDMA, DPU, DOCA, BlueField, DPDK, SPDK, or hardware
|
||||
acceleration backend is introduced;
|
||||
- no hardware SDK, `libibverbs`, `rdma-core`, or vendor dependency is added;
|
||||
- no new accepted production backend value is added;
|
||||
- future external or separately maintained backends may implement the same
|
||||
adapter boundary without changing RustFS core data-plane logic.
|
||||
|
||||
Examples of possible future external backends include DOCA/BlueField,
|
||||
RDMA/RoCE, or other DPU/NIC implementations. These are examples only and are
|
||||
not implemented or scheduled by this design.
|
||||
|
||||
## Current Adapter Surface
|
||||
|
||||
The current data-plane surface is byte-stream based:
|
||||
|
||||
| Current path | Current API shape | Current ownership |
|
||||
| --- | --- | --- |
|
||||
| Remote read stream | `InternodeDataTransport::open_read(...) -> FileReader` | Backend returns boxed `AsyncRead`; callers provide temporary `ReadBuf` storage per poll. |
|
||||
| Remote write stream | `InternodeDataTransport::open_write(...) -> FileWriter` | Callers pass borrowed `&[u8]` slices into boxed `AsyncWrite`; the backend owns any async body staging. |
|
||||
| Walk-dir stream | `InternodeDataTransport::open_walk_dir(...) -> FileReader` | Same boxed stream model as read, with a small serialized request body. |
|
||||
|
||||
This API is correct for the current TCP/HTTP backend. A future non-default
|
||||
backend may have stricter memory ownership, buffer lifetime, or completion
|
||||
requirements, so the adapter contract needs to describe those boundaries
|
||||
without assuming a specific implementation.
|
||||
|
||||
## Buffer Ownership Model
|
||||
|
||||
| Buffer role | Allocator | Lifetime owner | Backend-specific state | TCP/HTTP behavior |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Send buffer | Caller or RustFS-owned pool | Caller until the backend accepts the buffer; backend until completion if an owned-buffer API is used | Optional backend-managed buffer, staging buffer, or registration handle | Copy into the existing `AsyncWrite` path when the backend cannot use the buffer directly. |
|
||||
| Receive buffer | Caller-provided storage or backend-owned receive pool | Backend while filling; caller after completion if ownership is returned | Optional backend-owned receive buffer or backend-specific registration | Copy from `AsyncRead` into caller storage as today. |
|
||||
| Control metadata | RustFS caller | Caller/request object | Not buffer-managed by the data-plane backend | Serialize into HTTP/gRPC/control-plane messages. |
|
||||
| Fallback staging | TCP/HTTP backend | TCP/HTTP backend | No backend-specific registration | Existing `HttpReader`/`HttpWriter` buffering semantics. |
|
||||
|
||||
Backends with stricter memory requirements must not let callers mutate or reuse
|
||||
a buffer while an async transfer is still in flight. A backend that cannot use a
|
||||
caller buffer directly must either reject the transfer before payload movement
|
||||
or copy through a clearly documented backend-owned staging buffer.
|
||||
|
||||
Zero-copy is not guaranteed by this contract. Backends must document whether
|
||||
their path is zero-copy, copy-reduced, or staging-buffer based, and must
|
||||
document where copies occur.
|
||||
|
||||
## Compatibility Contract
|
||||
|
||||
The current stream API remains the OSS compatibility contract:
|
||||
|
||||
```rust
|
||||
#[async_trait::async_trait]
|
||||
pub trait InternodeDataTransport {
|
||||
async fn open_read(&self, request: ReadStreamRequest) -> Result<FileReader>;
|
||||
async fn open_write(&self, request: WriteStreamRequest) -> Result<FileWriter>;
|
||||
async fn open_walk_dir(&self, request: WalkDirStreamRequest) -> Result<FileReader>;
|
||||
}
|
||||
```
|
||||
|
||||
Future extensions for backend-managed buffers should be additive and
|
||||
capability-gated. A possible shape is:
|
||||
|
||||
```rust
|
||||
pub struct TransferBuffer {
|
||||
pub bytes: bytes::Bytes,
|
||||
pub backend_state: Option<TransportBufferState>,
|
||||
}
|
||||
|
||||
pub struct CompletedTransfer {
|
||||
pub bytes: bytes::Bytes,
|
||||
pub transfer_len: usize,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait BackendBufferInternodeTransport: InternodeDataTransport {
|
||||
async fn write_backend_buffer(&self, request: WriteStreamRequest, buffer: TransferBuffer) -> Result<CompletedTransfer>;
|
||||
async fn read_backend_buffer(&self, request: ReadStreamRequest, buffer: TransferBuffer) -> Result<CompletedTransfer>;
|
||||
}
|
||||
```
|
||||
|
||||
The concrete type of `TransportBufferState` should stay backend-private. The
|
||||
generic contract only needs to state whether the buffer is usable by the
|
||||
backend, who owns it during transfer, and when ownership returns.
|
||||
|
||||
This PR does not add this extension. It documents the boundary that a future
|
||||
external backend may need.
|
||||
|
||||
## Required Contract for Stricter Backends
|
||||
|
||||
| Area | Required contract |
|
||||
| --- | --- |
|
||||
| Ownership | Define when caller-owned bytes become backend-owned and when ownership returns. |
|
||||
| Completion | Signal completion before RustFS can reuse or mutate backend-managed memory. |
|
||||
| Staging | Declare whether the backend copies through backend-owned staging buffers when direct use is unavailable. |
|
||||
| Size limits | Expose any RustFS-visible `max_transfer_size`. |
|
||||
| Ordering | Either provide ordered delivery or include a reassembly layer before exposing stream semantics. |
|
||||
| Copy accounting | Document every known copy boundary and avoid claiming zero-copy unless the path proves it. |
|
||||
|
||||
## Optional Optimizations
|
||||
|
||||
| Area | Optional behavior |
|
||||
| --- | --- |
|
||||
| Buffer pooling | RustFS may keep reusable pools for send and receive buffers. |
|
||||
| Backend state cache | A backend may cache backend-specific handles for long-lived buffers. |
|
||||
| Scatter/gather | A backend may accept multiple shard slices without repacking when its completion model can report per-slice ownership safely. |
|
||||
| Backend-owned receive | A backend may return owned receive chunks instead of filling caller-provided buffers. |
|
||||
|
||||
These are optional optimizations, not requirements for the OSS TCP/HTTP path.
|
||||
|
||||
## Current API Limitations
|
||||
|
||||
| Current API | Limitation for stricter backends |
|
||||
| --- | --- |
|
||||
| `FileReader = Box<dyn AsyncRead + Send + Sync + Unpin>` | `AsyncRead` exposes temporary caller `ReadBuf` storage, not a stable backend-managed receive buffer or explicit completion token. |
|
||||
| `FileWriter = Box<dyn AsyncWrite + Send + Sync + Unpin>` | `AsyncWrite::poll_write` receives borrowed `&[u8]` that cannot outlive the poll, so async direct transfer requires copying or a different ownership API. |
|
||||
| `HttpWriter` | The async HTTP body must own `Bytes`, so borrowed write buffers are copied into `BytesMut` or `Bytes`. |
|
||||
| `write_body_chunks_to_writer` | Server-side HTTP body chunks are copied into `BytesMut` before local disk write. |
|
||||
| Erasure encode output | Encoded shards are represented as `Vec<Bytes>` and written through `AsyncWrite`, not a completion-aware backend-buffer API. |
|
||||
| Erasure decode input | Shard reads allocate `Vec<u8>` buffers before decode; no backend-owned receive pool is visible at the transport boundary. |
|
||||
|
||||
These limitations do not block the current `tcp-http` backend. They describe
|
||||
where a future external backend would need staging or an additive API.
|
||||
|
||||
## External Backend Crate Compatibility
|
||||
|
||||
`InternodeDataTransport` should remain implementable by future backends without
|
||||
modifying RustFS core data-plane logic. In the short term, the trait and
|
||||
`tcp-http` backend may remain inside `ecstore`.
|
||||
|
||||
A future external or separately maintained backend could live in a separate
|
||||
crate if the trait, request/response types, capability report, and error model
|
||||
are public and stable enough. This PR does not perform a crate split, add
|
||||
runtime loading, or introduce a plugin system.
|
||||
@@ -0,0 +1,125 @@
|
||||
# Internode Buffer Lifecycle and Copy Count
|
||||
|
||||
Status: P1-D analysis only. This document records the current TCP/HTTP
|
||||
internode data path and the ownership boundaries that matter for the
|
||||
backend-neutral `InternodeDataTransport` adapter. It does not implement a new
|
||||
backend or change production behavior.
|
||||
|
||||
## Open-source Scope
|
||||
|
||||
The OSS scope is:
|
||||
|
||||
- define buffer ownership and copy-count behavior for the current
|
||||
`InternodeDataTransport` adapter;
|
||||
- keep `tcp-http` as the default backend;
|
||||
- keep existing TCP/HTTP behavior unchanged;
|
||||
- document copy hotspots and ownership gaps for maintainable transport code;
|
||||
- avoid hardware-specific dependencies or backend implementations.
|
||||
|
||||
The OSS scope is not:
|
||||
|
||||
- RDMA support;
|
||||
- DPU support;
|
||||
- DOCA support;
|
||||
- BlueField support;
|
||||
- RoCE/InfiniBand support;
|
||||
- hardware benchmark planning;
|
||||
- hardware-specific backend implementation.
|
||||
|
||||
## Scope
|
||||
|
||||
The covered paths are the large internode data-plane calls currently routed
|
||||
through `InternodeDataTransport`:
|
||||
|
||||
| Path | Entry | Transport owner | Server owner |
|
||||
| --- | --- | --- | --- |
|
||||
| Read stream | `RemoteDisk::read_file_stream` in `crates/ecstore/src/rpc/remote_disk.rs` | `TcpHttpInternodeDataTransport::open_read` in `crates/ecstore/src/rpc/internode_data_transport.rs`, `HttpReader` in `crates/rio/src/http_reader.rs` | `handle_read_file` in `rustfs/src/storage/rpc/http_service.rs` |
|
||||
| Write stream | `RemoteDisk::create_file`, `RemoteDisk::append_file` in `crates/ecstore/src/rpc/remote_disk.rs` | `TcpHttpInternodeDataTransport::open_write` in `crates/ecstore/src/rpc/internode_data_transport.rs`, `HttpWriter` in `crates/rio/src/http_reader.rs` | `handle_put_file` in `rustfs/src/storage/rpc/http_service.rs` |
|
||||
| Walk dir stream | `RemoteDisk::walk_dir` in `crates/ecstore/src/rpc/remote_disk.rs` | `TcpHttpInternodeDataTransport::open_walk_dir`, `HttpReader` | `handle_walk_dir` in `rustfs/src/storage/rpc/http_service.rs` |
|
||||
|
||||
Object read/write/heal callers enter these streams through
|
||||
`create_bitrot_reader` and `create_bitrot_writer` in
|
||||
`crates/ecstore/src/bitrot.rs`. Erasure decode and encode then move data
|
||||
through `ParallelReader` in `crates/ecstore/src/erasure_coding/decode.rs` and
|
||||
`MultiWriter` in `crates/ecstore/src/erasure_coding/encode.rs`.
|
||||
|
||||
## Read Stream
|
||||
|
||||
| Step | Owner | Buffer type | Copy? | Reason |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Build request | `RemoteDisk::read_file_stream` | `String` fields in `ReadStreamRequest` | Yes | Volume, path, endpoint, and disk references are copied into an owned request before async transport dispatch. This is metadata, not payload. |
|
||||
| Select transport | `TcpHttpInternodeDataTransport::open_read` | URL `String`, `HeaderMap` | Yes | URL and auth headers are HTTP control data. No object bytes are copied here. |
|
||||
| Open local file on server | `handle_read_file`, `LocalDisk::read_file_stream` | `FileCacheReclaimReader` boxed as `FileReader` | No payload copy | The server owns an async file reader positioned at the requested offset. |
|
||||
| File to HTTP body | `read_file_body_stream` | `ReaderStream<AsyncRead>` yielding `Bytes` | Yes | `ReaderStream::with_capacity` reads from the file into chunk buffers. This is the file-to-network buffer materialization point. |
|
||||
| Length limiting | `rustfs_utils::net::bytes_stream` | `Bytes` | Usually no | `Bytes::truncate` adjusts the chunk view when the last chunk exceeds the requested length. It does not copy the retained prefix. |
|
||||
| HTTP receive | `HttpReader::with_capacity_and_stall_timeout` | `reqwest::Response::bytes_stream()` yielding `Bytes` | Network stack dependent | The user-level object is `Bytes`; any kernel/TLS/hyper copy is below the current RustFS abstraction. |
|
||||
| Stream to caller buffer | `HttpReader::poll_read` | `StreamReader<Stream<Item = Bytes>>`, caller `ReadBuf` | Yes | `StreamReader` exposes `AsyncRead`, so it copies bytes from each `Bytes` chunk into the caller-provided `ReadBuf`. |
|
||||
| Bitrot verification | `BitrotReader::read` | caller `&mut [u8]`, `hash_buf: Vec<u8>` | No additional payload copy | The bitrot reader reads hash bytes into `hash_buf` and payload bytes directly into the supplied output slice. Hash calculation reads the slice. |
|
||||
| Erasure shard read | `ParallelReader::read` | `Vec<u8>` per shard | Yes | Each shard read allocates `vec![0u8; shard_size]`; data is filled there before decode/reconstruction. |
|
||||
| Object response write | `write_data_blocks` | slices of shard `Vec<u8>` | No extra staging copy | Decoded data block slices are written to the target writer with `write_all`; the target may copy internally. |
|
||||
| Remote zero-copy helper | `RemoteDisk::read_file_zero_copy` | `Vec<u8>` then `Bytes` | Yes | The remote implementation reads the full stream into a `Vec` and converts it into `Bytes`. It is a convenience fallback, not network zero-copy. |
|
||||
|
||||
## Write Stream
|
||||
|
||||
| Step | Owner | Buffer type | Copy? | Reason |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Build writer request | `RemoteDisk::create_file`, `RemoteDisk::append_file` | `String` fields in `WriteStreamRequest` | Yes | Volume, path, endpoint, and disk references are copied into an owned request. This is metadata. |
|
||||
| Select transport | `TcpHttpInternodeDataTransport::open_write` | URL `String`, `HeaderMap` | Yes | URL and auth headers are HTTP control data. No object bytes are copied here. |
|
||||
| Erasure encode input | `Erasure::encode` in `encode.rs` | reusable `Vec<u8>` sized to `block_size` | Yes | `rustfs_utils::read_full` fills a block buffer from the source reader before encoding. |
|
||||
| Erasure encode output | `Erasure::encode_data` caller in `encode.rs` | `Vec<Bytes>` per encoded block | Yes | Encoding creates shard `Bytes` for data and parity blocks before queueing them to writers. |
|
||||
| Multi-writer fanout | `MultiWriter::write` | borrowed `Bytes` shards | No additional fanout copy | The writer fanout passes borrowed `Bytes` references to each `BitrotWriterWrapper`. |
|
||||
| Bitrot write | `BitrotWriter::write` | shard `&[u8]`, checksum bytes | Yes for checksum bytes | The payload slice is passed to the inner writer, while checksum bytes are generated and written before payload when enabled. |
|
||||
| Client HTTP writer buffer | `HttpWriter::poll_write` and `poll_write_vectored` | `BytesMut` pending chunk or `Bytes::copy_from_slice` | Yes | Small writes are coalesced with `BytesMut::extend_from_slice`; large single writes still copy into an owned `Bytes` because the async body must outlive the caller's borrowed buffer. |
|
||||
| Client channel to reqwest | `HttpWriter::poll_send_pending_chunk`, `ReceiverStream` | `Bytes` | No | `BytesMut::split().freeze()` transfers owned chunk storage to `Bytes`; the mpsc channel and stream move the `Bytes` handle. |
|
||||
| HTTP receive body on server | `handle_put_file` | `Incoming::into_data_stream()` yielding `Bytes` | Network stack dependent | The server receives owned `Bytes` chunks from hyper. |
|
||||
| Server body coalescing | `write_body_chunks_to_writer` | `BytesMut` sized to `DEFAULT_READ_BUFFER_SIZE` | Yes | Each incoming `Bytes` chunk is copied into `pending` before writing to the local file writer. This normalizes chunk size but adds a full payload copy. |
|
||||
| Local file write | `LocalDisk::create_file`, `LocalDisk::append_file`, `FileCacheReclaimWriter` | `&[u8]` into `tokio::fs::File` | Kernel dependent | RustFS passes slices to Tokio file writes. Kernel page-cache copies are below the RustFS abstraction. |
|
||||
|
||||
## Request and Serialization Boundaries
|
||||
|
||||
| Boundary | Owner | Buffer type | Copy? | Notes |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Read/write query parameters | `build_read_file_stream_url`, `build_put_file_stream_url` | URL-encoded `String` | Yes | Metadata only. It includes disk, volume, path, offset, length, append, and size. |
|
||||
| Auth headers | `build_auth_headers` callers | `HeaderMap` | Yes | Metadata only. This is currently tied to HTTP request construction. |
|
||||
| Walk dir request | `RemoteDisk::walk_dir`, `open_walk_dir`, `handle_walk_dir` | JSON `Vec<u8>` body, collected `Bytes` on server | Yes | Walk dir is a streamed response but its request body is serialized JSON control data. |
|
||||
| gRPC read/write-all | `RemoteDisk::read_all`, `RemoteDisk::write_all`, `NodeService::{handle_read_all,handle_write_all}` | Prost `Bytes`/message bodies | Yes | These paths are still gRPC byte paths, not `InternodeDataTransport`; they matter for metrics and inventory but are outside this P1-D stream-copy count. |
|
||||
|
||||
## Hotspots
|
||||
|
||||
| Rank | Hotspot | Impact | Reason |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | `HttpWriter::poll_write` and `poll_write_vectored` | High on write path | Every borrowed caller buffer is copied into owned `BytesMut` or `Bytes` before it can be sent by an async HTTP body. |
|
||||
| 2 | `write_body_chunks_to_writer` | High on write path | The server copies every received `Bytes` chunk into a coalescing `BytesMut` before local disk write. |
|
||||
| 3 | `ParallelReader::read` shard buffers | High on read path | Each shard read allocates and fills a `Vec<u8>` before decode can proceed. This is also where degraded reads wait on quorum. |
|
||||
| 4 | `ReaderStream::with_capacity` plus `StreamReader` | Medium on read path | Server file reads create `Bytes` chunks, then client `AsyncRead` copies those chunks into the caller's `ReadBuf`. |
|
||||
| 5 | `Erasure::encode` block and shard materialization | Medium on write path | Source data is first read into a block `Vec<u8>`, then encoded into per-shard `Bytes`. This is necessary for the current erasure API. |
|
||||
| 6 | `RemoteDisk::read_file_zero_copy` | Medium when used | Remote zero-copy reads buffer the whole stream into memory. The name does not mean zero-copy over the network. |
|
||||
| 7 | URL/query/header/JSON serialization | Low | Metadata copies are small and not on the large payload hot path. |
|
||||
|
||||
## Adapter Ownership Gaps
|
||||
|
||||
1. `FileReader` and `FileWriter` are boxed `AsyncRead`/`AsyncWrite` trait
|
||||
objects. They expose borrowed buffers per poll, not stable backend-owned
|
||||
regions, transfer handles, or explicit completion ownership.
|
||||
2. `InternodeDataTransport` currently returns stream traits only. Its
|
||||
capabilities advertise that TCP/HTTP does not require backend-specific
|
||||
buffer registration and is not a zero-copy candidate, but there is no
|
||||
backend API to pass stable backend-managed buffers.
|
||||
3. `HttpWriter` must own outgoing chunks because the async request body outlives
|
||||
the caller's borrowed `&[u8]`. A lower-copy backend would need a different
|
||||
lifetime contract or an owned buffer pool.
|
||||
4. Server write handling normalizes all incoming body chunks into a new
|
||||
`BytesMut`. Avoiding that copy would require passing incoming `Bytes` or
|
||||
backend-owned receive buffers directly into the disk/bitrot writer contract.
|
||||
5. Erasure decode owns shard `Vec<u8>` buffers and write-back happens through
|
||||
`AsyncWrite`. A lower-copy backend would need explicit ownership of shard
|
||||
buffers across decode, reconstruction, and network completion.
|
||||
6. Erasure encode materializes `Vec<Bytes>` blocks before fanout. A
|
||||
backend that can send multiple stable slices would need an encode output
|
||||
representation that can be transferred without repacking.
|
||||
7. The HTTP auth and URL construction boundary is part of the current TCP/HTTP
|
||||
backend. A non-HTTP backend would need equivalent peer authentication and
|
||||
disk addressing without assuming URL query parameters.
|
||||
8. Local disk zero-copy exists only for local reads via `read_file_zero_copy`.
|
||||
Remote disks deliberately fall back to network streaming and full-buffer
|
||||
collection for the zero-copy helper.
|
||||
@@ -0,0 +1,78 @@
|
||||
# Internode Transport Capabilities
|
||||
|
||||
Status: design note for backend-neutral capability reporting. This document
|
||||
does not add a backend and does not require specialized hardware or transport
|
||||
crates.
|
||||
|
||||
## Open-source Scope
|
||||
|
||||
The OSS scope is:
|
||||
|
||||
- define honest capability reporting for the `InternodeDataTransport` adapter;
|
||||
- keep `tcp-http` as the default backend;
|
||||
- keep existing TCP/HTTP behavior unchanged;
|
||||
- document the capability fields needed for maintainable transport code;
|
||||
- avoid hardware-specific dependencies or backend implementations.
|
||||
|
||||
The OSS scope is not:
|
||||
|
||||
- RDMA support;
|
||||
- DPU support;
|
||||
- DOCA support;
|
||||
- BlueField support;
|
||||
- RoCE/InfiniBand support;
|
||||
- hardware benchmark planning;
|
||||
- hardware-specific backend implementation.
|
||||
|
||||
## Purpose
|
||||
|
||||
`InternodeDataTransportCapabilities` describes what a backend can honestly do
|
||||
for RustFS internode data-plane transfers. The fields are intentionally neutral:
|
||||
they can describe the current TCP/HTTP backend and a future non-default backend
|
||||
without naming a specific transport implementation.
|
||||
|
||||
The capability report is descriptive. It does not select a backend, negotiate
|
||||
with peers, or weaken object correctness semantics.
|
||||
|
||||
## Capability Fields
|
||||
|
||||
| Field | Meaning |
|
||||
| --- | --- |
|
||||
| `streaming_read` | The backend can open a remote disk reader for `read_file_stream`. |
|
||||
| `streaming_write` | The backend can open a remote disk writer for `create_file` or `append_file`. |
|
||||
| `streaming_walk_dir` | The backend can stream `walk_dir` responses. |
|
||||
| `zero_copy_candidate` | The backend has an API shape that could avoid an extra user-space payload copy. This is not a promise that every transfer is zero-copy. |
|
||||
| `registered_memory_required` | The backend requires pinned, registered, staged, or otherwise backend-managed buffers before payload transfer. |
|
||||
| `ordered_delivery` | Bytes for each opened transfer are delivered in order. |
|
||||
| `max_transfer_size` | Optional RustFS-level cap for a single transfer. `None` means no additional cap beyond the backend/protocol/runtime limits. |
|
||||
| `fallback_supported` | The backend can participate in the behavior-preserving TCP fallback path. |
|
||||
|
||||
## TCP/HTTP Backend
|
||||
|
||||
The default TCP/HTTP backend reports only capabilities it actually provides:
|
||||
|
||||
| Field | TCP/HTTP value | Reason |
|
||||
| --- | --- | --- |
|
||||
| `streaming_read` | `true` | `HttpReader` streams `/rustfs/rpc/read_file_stream` responses. |
|
||||
| `streaming_write` | `true` | `HttpWriter` streams `/rustfs/rpc/put_file_stream` request bodies. |
|
||||
| `streaming_walk_dir` | `true` | `HttpReader` streams `/rustfs/rpc/walk_dir` responses. |
|
||||
| `zero_copy_candidate` | `false` | The current path exposes `AsyncRead`/`AsyncWrite` and HTTP body chunks; it copies through normal user-space buffers. |
|
||||
| `registered_memory_required` | `false` | TCP/HTTP does not require RustFS-managed pinned, registered, or backend-owned buffers. |
|
||||
| `ordered_delivery` | `true` | Each HTTP request body or response body is consumed as an ordered byte stream. |
|
||||
| `max_transfer_size` | `None` | RustFS does not impose an extra per-transfer cap at the capability layer. |
|
||||
| `fallback_supported` | `true` | TCP/HTTP is the behavior-preserving default and fallback path. |
|
||||
|
||||
## Non-default Backend Fit
|
||||
|
||||
A future non-default backend can be described without changing the meaning of
|
||||
the existing TCP report:
|
||||
|
||||
| Capability shape | Interpretation |
|
||||
| --- | --- |
|
||||
| `zero_copy_candidate=true`, `registered_memory_required=true` | The backend can only use its lower-copy path with buffers that satisfy backend-specific ownership or registration rules. |
|
||||
| `zero_copy_candidate=true`, `registered_memory_required=false` | The backend may expose owned chunks or another lower-copy path without requiring caller-managed registration. |
|
||||
| `max_transfer_size=Some(n)` | The backend has a RustFS-visible transfer size ceiling and callers must split larger transfers or use fallback behavior. |
|
||||
| `ordered_delivery=false` | The backend cannot be used behind the current stream API without an ordering or reassembly layer. |
|
||||
|
||||
Unsupported or mismatched capabilities must not silently change quorum,
|
||||
integrity verification, retry, or timeout semantics.
|
||||
@@ -0,0 +1,144 @@
|
||||
# Internode Transport Fallback and Backend Selection Model
|
||||
|
||||
Status: design note only. This document defines backend-neutral selection,
|
||||
fallback, failure handling, negotiation, security, and observability rules for
|
||||
the `InternodeDataTransport` adapter. It does not implement a new backend and
|
||||
does not change production behavior.
|
||||
|
||||
## Open-source Scope
|
||||
|
||||
The open-source RustFS path keeps `tcp-http` as the default internode data
|
||||
transport. This document defines adapter contracts only:
|
||||
|
||||
- no production RDMA, DPU, DOCA, BlueField, DPDK, SPDK, or hardware
|
||||
acceleration backend is introduced;
|
||||
- no hardware SDK, `libibverbs`, `rdma-core`, or vendor dependency is added;
|
||||
- no new accepted production backend value is added;
|
||||
- future external or separately maintained backends may implement the same
|
||||
adapter boundary without changing RustFS core data-plane logic.
|
||||
|
||||
Examples of possible future external backends include DOCA/BlueField,
|
||||
RDMA/RoCE, or other DPU/NIC implementations. These are examples only and are
|
||||
not implemented or scheduled by this design. Hardware-specific backend plans
|
||||
are out of scope for this document.
|
||||
|
||||
## Static Backend Selection
|
||||
|
||||
Static config is the first selection model. Existing accepted values remain:
|
||||
|
||||
| Config value | Meaning |
|
||||
| --- | --- |
|
||||
| unset | Use default TCP/HTTP backend. |
|
||||
| `tcp-http` | Use default TCP/HTTP backend. |
|
||||
| `tcp` | Alias for `tcp-http`. |
|
||||
| any unsupported value | Fail closed with a diagnostic naming `RUSTFS_INTERNODE_DATA_TRANSPORT` and the invalid value. |
|
||||
|
||||
Unknown backend values must fail closed. Unsupported backend values must fail
|
||||
closed. A future external backend must be explicitly enabled and must not
|
||||
silently replace `tcp-http`.
|
||||
|
||||
Backend selection must expose an observable backend identity for metrics, logs,
|
||||
and benchmark interpretation. The default and fallback path remains `tcp-http`.
|
||||
|
||||
## Fallback Contract
|
||||
|
||||
Fallback must be explicit and observable. Silent fallback is not allowed for
|
||||
benchmark or production interpretation because it hides which backend moved the
|
||||
payload.
|
||||
|
||||
| Condition | Default behavior | Explicit fallback behavior | Observability |
|
||||
| --- | --- | --- | --- |
|
||||
| Unsupported configured backend | Fail closed during transport construction. | Fall back only when a separately configured policy explicitly allows unsupported-backend fallback. | Error includes config key and invalid value; fallback event is counted when fallback is enabled. |
|
||||
| Peer does not support selected backend | Fail before payload transfer. | Use TCP/HTTP only when both local policy and peer policy allow it. | Count peer mismatch and selected fallback backend. |
|
||||
| Capability mismatch | Fail before payload transfer. | Use TCP/HTTP only if TCP satisfies the operation and policy allows fallback. | Record missing capability names or a low-cardinality reason. |
|
||||
| Connection setup failure | Fail the operation. | Retry on TCP/HTTP only when fallback is allowed and no payload bytes have transferred. | Count setup failure, retry, fallback backend, and fallback result. |
|
||||
| Partial transfer failure | Fail the operation and let existing object/quorum logic decide retry behavior. | Do not silently resume on another backend unless the transfer protocol can prove byte range, checksum, and idempotency boundaries. | Count partial failure with bytes completed. |
|
||||
| Max transfer size exceeded | Fail before payload transfer or split at a higher layer. | Use TCP/HTTP if policy allows and TCP has no RustFS-level cap. | Record rejected size and selected backend. |
|
||||
| Auth or encryption mismatch | Fail closed. | No fallback unless the fallback path satisfies the same or stronger security requirements. | Security failure metric and audit log entry. |
|
||||
|
||||
Fallback settings should not be added until there is an implementation that
|
||||
uses them. A backend must define failure behavior before production use.
|
||||
|
||||
## Dynamic Negotiation Boundary
|
||||
|
||||
Dynamic negotiation, if added, belongs on the existing gRPC control plane. Data
|
||||
transfer must start only after both peers agree on:
|
||||
|
||||
| Negotiated item | Required property |
|
||||
| --- | --- |
|
||||
| Backend name | Both peers know the backend and have it enabled. |
|
||||
| Capability set | Required capabilities match the operation. |
|
||||
| Max transfer size | The selected operation fits or is split before transfer starts. |
|
||||
| Buffer rules | Both peers agree whether backend-managed or staged buffers are required. |
|
||||
| Completion semantics | Both peers agree when a transfer is considered complete and when buffers may be reused. |
|
||||
| Security mode | Authentication and encryption requirements are satisfied before any out-of-band transfer. |
|
||||
| Fallback policy | Both peers agree whether TCP/HTTP fallback is allowed for this operation. |
|
||||
|
||||
Negotiation must not silently downgrade security or bypass existing disk
|
||||
health, quorum, timeout, and integrity semantics.
|
||||
|
||||
## Failure Handling Requirements
|
||||
|
||||
| Failure mode | Requirement |
|
||||
| --- | --- |
|
||||
| Invalid config | Fail closed with `RUSTFS_INTERNODE_DATA_TRANSPORT` and the invalid value. |
|
||||
| Backend disabled | Fail closed with the selected backend name and the missing enablement condition. |
|
||||
| Backend unavailable | Fail closed with an actionable diagnostic; do not silently use TCP/HTTP. |
|
||||
| Peer mismatch | Fail before payload transfer unless explicit fallback is configured. |
|
||||
| Connection failure | Fail the operation and record setup failure; fallback only if policy allows and no payload bytes moved. |
|
||||
| Completion failure | Return an operation error and release backend-owned resources. |
|
||||
| Timeout | Return an operation error and preserve existing disk health and quorum semantics. |
|
||||
| Partial transfer | Do not silently resume on another backend without a safe byte-range/checksum/idempotency proof. |
|
||||
| Unsupported operation | Return a clear unsupported-operation error. |
|
||||
|
||||
## Security Requirements
|
||||
|
||||
- Backend selection must preserve peer authentication.
|
||||
- Fallback must not weaken encryption or authorization.
|
||||
- Out-of-band data-plane transfers must still bind to the intended disk,
|
||||
volume, path, request authority, and operation.
|
||||
- Partial transfers must not bypass bitrot verification or erasure quorum
|
||||
handling.
|
||||
- Any future external backend must document whether it relies on the same
|
||||
security boundary as TCP/HTTP or requires a separate deployment boundary.
|
||||
|
||||
## Metrics and Observability Requirements
|
||||
|
||||
Metrics and logs must use low-cardinality labels or metadata:
|
||||
|
||||
- selected backend;
|
||||
- requested backend;
|
||||
- fallback backend, when used;
|
||||
- operation name;
|
||||
- success/failure;
|
||||
- transferred bytes;
|
||||
- setup failure count;
|
||||
- partial transfer failure count;
|
||||
- capability mismatch count;
|
||||
- fallback decision count.
|
||||
|
||||
Backends must not add high-cardinality labels such as object names, full paths,
|
||||
full URLs, peer-specific dynamic strings, memory addresses, or backend-specific
|
||||
buffer keys.
|
||||
|
||||
## TCP/HTTP Compatibility
|
||||
|
||||
The `tcp-http` backend remains the default and behavior-preserving path. It
|
||||
uses ordinary byte streams, does not require backend-specific buffer
|
||||
registration, and remains suitable as the fallback path when an explicit
|
||||
fallback policy exists.
|
||||
|
||||
A future non-default backend must not change the correctness semantics of
|
||||
object writes, object reads, healing, bitrot verification, erasure quorum,
|
||||
timeouts, or disk health handling.
|
||||
|
||||
## External Backend Crate Compatibility
|
||||
|
||||
`InternodeDataTransport` should remain implementable by future backends without
|
||||
modifying RustFS core data-plane logic. In the short term, the trait and
|
||||
`tcp-http` backend may remain inside `ecstore`.
|
||||
|
||||
A future external or separately maintained backend could live in a separate
|
||||
crate if the trait, request/response types, capability report, and error model
|
||||
are public and stable enough. This PR does not perform a crate split, add
|
||||
runtime loading, or introduce a plugin system.
|
||||
@@ -107,6 +107,9 @@ pub struct WalkDirStreamRequest {
|
||||
/// This boundary is limited to remote disk streams that can move large payloads.
|
||||
/// Internode metadata, lock, health, and administrative calls remain on the
|
||||
/// existing gRPC control plane.
|
||||
///
|
||||
/// Buffer ownership, backend selection, and fallback expectations are documented
|
||||
/// in `crates/ecstore/docs/internode-transport/`.
|
||||
#[async_trait]
|
||||
pub trait InternodeDataTransport: Send + Sync + std::fmt::Debug {
|
||||
async fn open_read(&self, request: ReadStreamRequest) -> Result<FileReader>;
|
||||
@@ -248,7 +251,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tcp_http_capabilities_do_not_advertise_rdma_specific_features() {
|
||||
fn tcp_http_capabilities_do_not_advertise_stricter_backend_features() {
|
||||
let capabilities = TcpHttpInternodeDataTransport.capabilities();
|
||||
|
||||
assert!(!capabilities.zero_copy_candidate);
|
||||
@@ -337,18 +340,19 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn transport_config_rejects_unknown_backend() {
|
||||
let err = build_internode_data_transport(Some("rdma")).expect_err("unknown backend should fail closed");
|
||||
let err = build_internode_data_transport(Some("unsupported-backend")).expect_err("unknown backend should fail closed");
|
||||
|
||||
assert!(err.to_string().contains(ENV_RUSTFS_INTERNODE_DATA_TRANSPORT));
|
||||
assert!(err.to_string().contains("rdma"));
|
||||
assert!(err.to_string().contains("unsupported-backend"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cached_transport_config_error_uses_raw_message() {
|
||||
let err = build_internode_data_transport_result(Some("rdma")).expect_err("unknown backend should fail closed");
|
||||
let err =
|
||||
build_internode_data_transport_result(Some("unsupported-backend")).expect_err("unknown backend should fail closed");
|
||||
|
||||
assert!(!err.starts_with("io error "));
|
||||
assert!(err.contains(ENV_RUSTFS_INTERNODE_DATA_TRANSPORT));
|
||||
assert!(err.contains("rdma"));
|
||||
assert!(err.contains("unsupported-backend"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user