mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-07 22:03:14 +00:00
6f613317f6
* feat(internode): P0 gRPC transport tuning, message limits, payload metrics Land the P0 subtask from docs/grpc-optimization: close the client-vs-server transport gaps and add instrumentation to size which unary RPCs need channel isolation in P1. Transport tuning (G3): the client `Endpoint` now disables Nagle and raises the HTTP/2 stream/connection flow-control windows to mirror the server socket, so small lock/health RPCs are not batched and larger metadata responses are not throttled by the 64KiB default window. All env-overridable, 0 opts out. Message-size limits (G1): both `NodeServiceClient` and `NodeServiceServer` set max decode/encode size (default 100MiB) instead of tonic's silent 4MiB cap, so a large multi-version xl.meta or aggregated ReadMultiple no longer fails out_of_range. The server limit is set on `NodeServiceServer` before wrapping in the auth `InterceptedService` (the interceptor type does not expose it). Payload instrumentation (P1 prep): ReadAll/ReadMultiple record a payload-size histogram plus a large-payload counter when a response crosses the configured threshold (default 8MiB), feeding alerting on paths that contend with latency-sensitive control-plane traffic on the shared channel. Threshold-only counter, no per-call hot-path log. Verification: cargo check/test on config, io-metrics, ecstore, rustfs; clippy clean on touched files; make pre-commit green. Co-Authored-By: heihutu <heihutu@gmail.com> * feat(internode): P1 control/bulk gRPC channel isolation (opt-in) Land the P1 subtask from docs/grpc-optimization: physically separate large bytes-carrying unary RPCs from latency-sensitive control-plane RPCs so a big transfer can no longer head-of-line block a lock/health RPC on the shared HTTP/2 connection (G2/G5). Introduce ChannelClass { Control, Bulk } and get_channel_for_class in protos. Control RPCs keep the per-peer connection keyed by the bare address; Bulk RPCs (ReadAll/WriteAll/ReadMultiple/BatchReadVersion, via a new get_bulk_client) are round-robined across a small per-peer bulk pool. Rather than restructuring the global GLOBAL_CONN_MAP (and every consumer), bulk channels are cached under a composite key (addr\0bulk\0idx). The NUL separator cannot appear in a URL, so bulk keys never collide with the control key. This keeps the blast radius small on a consistency-sensitive path. create_new_channel is refactored into build_channel(dial_addr, cache_key) so several physically distinct channels to one peer cache independently while dialing/TLS still use the real address. Gated by RUSTFS_INTERNODE_CHANNEL_ISOLATION (default OFF) so the default build is byte-for-byte the pre-P1 behavior: bulk resolves to the control channel and the switch is a single-env rollback. RUSTFS_INTERNODE_BULK_CHANNELS (default 2, clamped >=1) sizes the pool. On failure, evict_failed_connection drops the whole bulk pool for the peer (round-robin hides which index was used), avoiding half-dead cached channels. Lock RPCs (remote_locker) already use the default Control path, so lock semantics and retry behavior are unchanged. Verification: cargo check/test on config, protos, ecstore, rustfs; new protos tests for bulk key routing and isolation-off passthrough; clippy clean on touched files; make pre-commit green. Co-Authored-By: heihutu <heihutu@gmail.com> * feat(internode): P2 msgpack/JSON codec observability + encode buffer presizing Land the safe, wire-compatible slice of P2 from docs/grpc-optimization: the observability prerequisite for retiring the redundant JSON fields, plus a codec micro-optimization. No proto/wire-format change; JSON is still dual-written. Internode RPCs today dual-encode each metadata value as both msgpack (`*_bin`) and a JSON compatibility string, and decoders prefer `_bin` with a JSON fallback. Before the JSON fields can ever be dropped (a cross-version change), that fallback must be proven unused in production. Add rustfs_system_network_internode_msgpack_json_fallback_total{direction, message}: incremented whenever a decode falls back to the JSON field because the msgpack payload was absent. Wired into both directions — the client decoding peer responses (remote_disk.rs, incl. the list-level read_multiple/batch fallbacks) and the server decoding peer requests (node_service/disk.rs). This counter must read zero across a release window before send paths stop writing JSON and the proto text fields are reserved/removed (the deferred P2-1 steps). Also pre-size the msgpack encode buffers (Vec::with_capacity(512)) on both sides, eliminating the repeated growth reallocations for typical FileInfo payloads with zero added copy. Full thread_local buffer pooling is deferred: it needs either an extra copy (unclear net win) or a send-path buffer-return lifecycle, to be justified by a codec microbenchmark first. Verification: cargo check/test on io-metrics, ecstore, rustfs; new fallback counter smoke test; existing codec decode tests green; clippy clean on touched files; make pre-commit green. Co-Authored-By: heihutu <heihutu@gmail.com> * docs(internode): add msgpack/JSON convergence observation runbook Runbook driving the observation-gated retirement of the redundant JSON compatibility fields on internode gRPC metadata RPCs (grpc-optimization P2-1). Documents the shipped fallback counter (rustfs_system_network_internode_msgpack_json_fallback_total{direction,message}), the PromQL to confirm it reads zero across a release window, a standing alert, and the staged flip/rollback procedure (env-gated msgpack-only send, then proto field removal in N+1). Includes the verified field -> peer-decoder audit: only fields whose peer decodes _bin first may be converged. Notes DeleteVersion.opts (DeleteOptions) is NOT convergence-ready — its server handler is not _bin-first and must gain a decode_msgpack_or_json path first. This gates the send-side change so it cannot empty a JSON field an old peer still needs. Co-Authored-By: heihutu <heihutu@gmail.com> * feat(internode): env-gated msgpack-only send + DeleteVersion _bin support (P2-1) Implements the send-side lever for retiring the redundant JSON compatibility fields on internode gRPC metadata RPCs, plus the missing `_bin` support on the delete path that it depends on (grpc-optimization P2-1). Default-off: the base build is byte-for-byte the prior dual-write behavior. Gated msgpack-only send (RUSTFS_INTERNODE_RPC_MSGPACK_ONLY, default false): - New rustfs_protos::internode_rpc_msgpack_only() reads the flag. - Client (remote_disk.rs) compat_json() and server (node_service/disk.rs) compat_response_json() emit an empty JSON string when the flag is on, so only the msgpack _bin payload is sent. The _bin field is always sent; decoders keep the JSON read fallback. Applied only to fields with a confirmed _bin-first peer decoder (WriteMetadata/UpdateMetadata/RenameData file_info, UpdateMetadata opts, ReadOptions, ReadMultipleReq, BatchReadVersionReq; ReadVersion/ReadXL/RenameData responses and the ReadMultiple/BatchReadVersion response lists). - Only enable after the P2 fallback counter has read zero across a release window (see docs/operations/internode-msgpack-json-convergence-runbook.md). Single-env rollback; no wire-format break. DeleteVersion(s) _bin support (prerequisite): - The DeleteVersion/DeleteVersions protos had NO _bin fields. Add additive (backward-compatible) bytes file_info_bin/opts_bin (DeleteVersion) and repeated bytes versions_bin + bytes opts_bin (DeleteVersions); regenerate the checked-in prost struct. - Client dual-writes them; server decodes them _bin-first with JSON fallback. - These delete fields are kept OUT of the msgpack-only set (always dual-write) until their own fallback counter reads zero across a window with the new decoders fully deployed. DeleteVersion.raw_file_info stays JSON-only (no _bin field yet). Verification: cargo check/test on protos, config, ecstore, rustfs (incl. the six delete request handler tests and a compat_json default-path test); clippy clean on touched files; make pre-commit green. Co-Authored-By: heihutu <heihutu@gmail.com> * feat(internode): P3 cluster peer online/offline health metric Land the safe observability core of P3 (grpc-optimization G6/G8): track each internode peer's reachability and expose the offline count, for parity with MinIO's minio_cluster_servers_offline_total. Pure instrumentation — peer selection and quorum are unchanged. - io-metrics: per-peer PeerHealthState { online, consecutive_failures } registry plus record_peer_reachable/record_peer_unreachable. A peer flips offline after N consecutive failures (dial failures or RPC-triggered evictions) and back online on the next successful dial; the count of offline peers is published to the rustfs_cluster_servers_offline_total gauge. - config: RUSTFS_INTERNODE_OFFLINE_FAILURE_THRESHOLD (default 3, clamped >= 1). - protos: build_channel marks the peer reachable on a successful dial and unreachable on a dial failure; evict_failed_connection feeds the failure signal too. Keyed by the real peer address, so control and bulk channels to one peer share health state. Deferred (documented in docs/grpc-optimization P3): startup prewarm (no clean topology-ready hook yet), the offline fast-bypass in peer routing (consistency- sensitive; must not change quorum), and idempotent-read-only retry. This commit is observability only. Verification: cargo check/test on io-metrics, config, protos (new peer-health state-machine and threshold-clamp tests); clippy clean on touched files; make pre-commit green. Co-Authored-By: heihutu <heihutu@gmail.com> * feat(internode): P3 control-channel prewarm + self-healing offline bypass Add the remaining P3 connection-lifecycle levers (grpc-optimization G6/G8), both env-gated and default-off so the base build is unchanged. Prewarm (RUSTFS_INTERNODE_PREWARM, default off): RemoteDisk::new spawns a best-effort background dial of the peer's control channel, deduped per peer address, moving the connect cost off the first RPC. Failures fall through to the existing lazy connect + recovery monitor. Offline bypass (RUSTFS_INTERNODE_OFFLINE_BYPASS, default off): remote_disk get_client/get_bulk_client fast-fail a peer already marked offline instead of paying the connect timeout, so the erasure layer proceeds on quorum sooner. This does NOT change quorum. It is self-healing: cluster_peer_should_bypass lets one request per RUSTFS_INTERNODE_OFFLINE_REPROBE_SECS (default 5s) through to recover the peer even with no background monitor, and the recovery monitor's own probe path calls the client directly so it is never bypassed. io-metrics gains cluster_peer_is_offline / cluster_peer_should_bypass (with a per-peer re-probe timestamp). Scope: data path only — remote_locker (lock RPCs, most consistency-sensitive) is left dual-writing/unbypassed as a follow-up. Verification: cargo check/test on io-metrics, config, ecstore (new self-healing bypass tests; all 105 rpc tests green); clippy clean on touched files; make pre-commit green. Co-Authored-By: heihutu <heihutu@gmail.com> * docs(internode): add A/B benchmark runbook for gRPC optimization stages Reproducible before/after collection procedure for grpc-optimization P0–P3. Since every stage is env-gated, before/after is the same binary with different env — no rebuild. Documents, per stage: the exact env toggles (baseline vs enabled column), which existing bench script to run (run_internode_transport_baseline.sh / run_four_node_cluster_failover_bench.sh), the Prometheus metrics to capture, and the acceptance gates from the design docs (e.g. lock p99 down >= 20% for P1, msgpack fallback counter = 0 before enabling P2, correct rustfs_cluster_servers_offline_total for P3). Live runs require a multi-node cluster + load tool + Prometheus scrape and cannot be produced in a single-process sandbox; artifacts land under target/bench (gitignored) and attach to the PR. Co-Authored-By: heihutu <heihutu@gmail.com> * feat(internode): P3-2 lock-path offline bypass + P3-3 idempotent read retry Extend the offline bypass to the lock path and add opt-in retries for idempotent reads (grpc-optimization P3-2/P3-3). Both env-gated and default-off/zero. Offline bypass (lock path): factor the bypass decision into a shared pub(crate) internode_offline_bypass_reason(addr) and call it from remote_locker::get_client too, so lock RPCs to an offline peer fast-fail (letting dsync reach quorum sooner) instead of paying the connect timeout. Does not change quorum; the self-healing re-probe keeps peers recoverable. Gated by RUSTFS_INTERNODE_OFFLINE_BYPASS (default off). Idempotent read retry (P3-3): add execute_read_with_retry — a bounded, exponential-backoff retry for read-only/reentrant RPCs on transient network errors — and route disk_info through it. RUSTFS_INTERNODE_IDEMPOTENT_READ_RETRIES defaults to 0 (disabled). Write/lock RPCs are never retried (quorum/idempotency safety, per CLAUDE.md); the wrapper requires an Fn closure so only reads that rebuild their request from borrowed inputs qualify. Deferred: grpc.health.v1 (optional ecosystem-compat only; needs a new tonic-health dep and 3-way hybrid-service wiring — internal needs are met by the existing Ping RPC). Verification: cargo check/test on config, ecstore (105 rpc tests green incl. disk_info now via the retry wrapper); clippy clean on touched files; make pre-commit green. Co-Authored-By: heihutu <heihutu@gmail.com> * feat(scripts): one-click internode gRPC A/B benchmark driver Wrap the per-stage env matrix from the benchmark runbook into scripts/run_internode_grpc_ab_bench.sh: given --stage <p0|p1|p2|p3> and --phase <before|after>, it emits the stage/phase RUSTFS_INTERNODE_* server env to <out-dir>/server-env.sh and runs the right underlying bench (run_internode_transport_baseline.sh for p0/p1/p2, run_four_node_cluster_failover_bench.sh for p3) into a labeled target/bench/internode-transport/<stage>-<phase>/. Passthrough args after `--` reach the underlying bench; --dry-run previews the env + command. The script is explicit that RUSTFS_INTERNODE_* are server env, so for the load-driven stages the operator must restart rustfs with the emitted env before the run; the docker four-node (p3) path exports them for a forwarding compose. shellcheck-clean. Runbook updated with a "One-click driver" section. Co-Authored-By: heihutu <heihutu@gmail.com> * chore(compose): forward RUSTFS_INTERNODE_* into the four-node cluster The four-node local-build compose only forwarded a fixed whitelist of env, so the internode gRPC knobs (grpc-optimization P0-P3) never reached the containers and the A/B bench driver's "after" phase was a no-op. Forward the full RUSTFS_INTERNODE_* set with defaults matching the binary defaults, so leaving them unset is a no-op and the A/B driver can toggle a stage per phase. Co-Authored-By: heihutu <heihutu@gmail.com> * fix(internode): address Copilot review — retry health action + poison-safe peer health Two review nits on #4337: - P3-3 idempotent read retry (remote_disk.rs): execute_read_with_retry ran every attempt through execute_with_timeout_for_op, which hardcodes FailureHealthAction::MarkFailure. So the first transient error could flip the disk faulty and short-circuit the remaining retries, and each attempt over-counted the failure. Route all but the final attempt through execute_with_timeout_for_op_and_health_action with IgnoreFailure; only the last attempt marks faulty/evicts. No default impact (retries default 0). - Peer-health helpers (io-metrics): record_peer_reachable/record_peer_unreachable, cluster_peer_is_offline and cluster_peer_should_bypass early-returned on a poisoned mutex, permanently stalling the offline gauge and bypass state after a single panic. Recover via PoisonError::into_inner(). Verification: cargo check/test on io-metrics + ecstore (105 rpc tests green); clippy clean on touched files; make pre-commit green. Co-Authored-By: heihutu <heihutu@gmail.com> --------- Co-authored-by: heihutu <heihutu@gmail.com>
319 lines
18 KiB
Rust
319 lines
18 KiB
Rust
// Copyright 2024 RustFS Team
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
/// Timeout for establishing a new internode gRPC connection.
|
|
pub const ENV_INTERNODE_CONNECT_TIMEOUT_SECS: &str = "RUSTFS_INTERNODE_CONNECT_TIMEOUT_SECS";
|
|
pub const DEFAULT_INTERNODE_CONNECT_TIMEOUT_SECS: u64 = 3;
|
|
|
|
/// TCP keepalive interval for internode gRPC channels.
|
|
pub const ENV_INTERNODE_TCP_KEEPALIVE_SECS: &str = "RUSTFS_INTERNODE_TCP_KEEPALIVE_SECS";
|
|
pub const DEFAULT_INTERNODE_TCP_KEEPALIVE_SECS: u64 = 10;
|
|
|
|
/// HTTP/2 keepalive interval for internode gRPC channels.
|
|
pub const ENV_INTERNODE_HTTP2_KEEPALIVE_INTERVAL_SECS: &str = "RUSTFS_INTERNODE_HTTP2_KEEPALIVE_INTERVAL_SECS";
|
|
pub const DEFAULT_INTERNODE_HTTP2_KEEPALIVE_INTERVAL_SECS: u64 = 5;
|
|
|
|
/// HTTP/2 keepalive timeout for internode gRPC channels.
|
|
///
|
|
/// This is the time a peer has to ACK a keepalive PING before the whole channel
|
|
/// (and every RPC/stream multiplexed on it) is torn down. A very aggressive value
|
|
/// misfires under load: on a saturated node the PING ACK is legitimately delayed,
|
|
/// the channel is wrongly declared dead, in-flight peer reads fail, and large-object
|
|
/// GETs truncate mid-stream (client "unexpected EOF"). See backlog#832. Keep this
|
|
/// generous enough to tolerate transient load while still detecting truly dead peers.
|
|
pub const ENV_INTERNODE_HTTP2_KEEPALIVE_TIMEOUT_SECS: &str = "RUSTFS_INTERNODE_HTTP2_KEEPALIVE_TIMEOUT_SECS";
|
|
pub const DEFAULT_INTERNODE_HTTP2_KEEPALIVE_TIMEOUT_SECS: u64 = 20;
|
|
|
|
/// Overall timeout for a single internode gRPC request.
|
|
pub const ENV_INTERNODE_RPC_TIMEOUT_SECS: &str = "RUSTFS_INTERNODE_RPC_TIMEOUT_SECS";
|
|
pub const DEFAULT_INTERNODE_RPC_TIMEOUT_SECS: u64 = 30;
|
|
|
|
// ── Client-side internode gRPC channel tuning (P0) ──
|
|
// These mirror the server-side HTTP/2 transport tuning in `rustfs/src/server/http.rs`
|
|
// on the *client* `tonic` `Endpoint` used for internode control-plane RPCs. Prior to
|
|
// this, the client channel only set timeouts/keepalive, leaving Nagle enabled and the
|
|
// default 64KiB HTTP/2 window in place — hurting small lock-RPC latency and large
|
|
// metadata-response throughput respectively.
|
|
|
|
/// Disable Nagle's algorithm on internode gRPC client sockets.
|
|
///
|
|
/// Latency-sensitive control-plane RPCs (locks, health, small metadata) send tiny
|
|
/// frames; Nagle batching adds avoidable delay. Defaults to `true` (nodelay on),
|
|
/// matching the server socket configuration.
|
|
pub const ENV_INTERNODE_RPC_TCP_NODELAY: &str = "RUSTFS_INTERNODE_RPC_TCP_NODELAY";
|
|
pub const DEFAULT_INTERNODE_RPC_TCP_NODELAY: bool = true;
|
|
|
|
// Compile-time invariant: nodelay defaults on so latency-sensitive control-plane RPCs are not
|
|
// batched by Nagle, matching the server socket configuration.
|
|
const _: () = assert!(DEFAULT_INTERNODE_RPC_TCP_NODELAY);
|
|
|
|
/// HTTP/2 initial stream window size (bytes) for internode gRPC client channels.
|
|
///
|
|
/// The library default (64KiB) throttles larger unary responses (e.g. `ReadMultiple`,
|
|
/// `BatchReadVersion`) by the bandwidth-delay product. Set to 0 to fall back to the
|
|
/// `tonic`/`hyper` default. Defaults to 1MiB.
|
|
pub const ENV_INTERNODE_RPC_HTTP2_STREAM_WINDOW_SIZE: &str = "RUSTFS_INTERNODE_RPC_HTTP2_STREAM_WINDOW_SIZE";
|
|
pub const DEFAULT_INTERNODE_RPC_HTTP2_STREAM_WINDOW_SIZE: u32 = 1024 * 1024;
|
|
|
|
/// HTTP/2 initial connection window size (bytes) for internode gRPC client channels.
|
|
///
|
|
/// Should be >= the stream window so multiple concurrent streams are not starved by the
|
|
/// connection-level flow-control window. Set to 0 to fall back to the library default.
|
|
/// Defaults to 2MiB.
|
|
pub const ENV_INTERNODE_RPC_HTTP2_CONN_WINDOW_SIZE: &str = "RUSTFS_INTERNODE_RPC_HTTP2_CONN_WINDOW_SIZE";
|
|
pub const DEFAULT_INTERNODE_RPC_HTTP2_CONN_WINDOW_SIZE: u32 = 2 * 1024 * 1024;
|
|
|
|
// Compile-time invariant: the connection-level window must be >= the per-stream window, or
|
|
// concurrent streams would be starved by the connection-level flow-control budget.
|
|
const _: () = assert!(DEFAULT_INTERNODE_RPC_HTTP2_CONN_WINDOW_SIZE >= DEFAULT_INTERNODE_RPC_HTTP2_STREAM_WINDOW_SIZE);
|
|
|
|
/// Maximum encoded message size (bytes) for internode gRPC, applied to both the client
|
|
/// `NodeServiceClient` and the server `NodeServiceServer`.
|
|
///
|
|
/// Without this, `tonic`'s default 4MiB decode limit silently caps `bytes`-carrying
|
|
/// unary RPCs (`ReadAll`/`WriteAll`/`ReadMultiple`/`BatchReadVersion`); a large multi-version
|
|
/// `xl.meta` or an aggregated response then fails with `out_of_range`. The default comes
|
|
/// from `rustfs_protos::DEFAULT_GRPC_SERVER_MESSAGE_LEN` (100MiB) at the call sites.
|
|
pub const ENV_INTERNODE_RPC_MAX_MESSAGE_SIZE: &str = "RUSTFS_INTERNODE_RPC_MAX_MESSAGE_SIZE";
|
|
|
|
/// Payload-size threshold (bytes) above which a unary internode gRPC response counts as a
|
|
/// "large payload" for alerting.
|
|
///
|
|
/// Large `ReadAll`/`ReadMultiple` responses share the control-plane channel with
|
|
/// latency-sensitive lock/health RPCs and can head-of-line block them (see grpc-optimization
|
|
/// G2). This threshold drives the `rustfs_system_network_internode_operation_large_payloads_total`
|
|
/// counter so operators can size which paths need channel isolation in P1. Defaults to 8MiB.
|
|
pub const ENV_INTERNODE_RPC_LARGE_PAYLOAD_WARN_BYTES: &str = "RUSTFS_INTERNODE_RPC_LARGE_PAYLOAD_WARN_BYTES";
|
|
pub const DEFAULT_INTERNODE_RPC_LARGE_PAYLOAD_WARN_BYTES: usize = 8 * 1024 * 1024;
|
|
|
|
/// Stop dual-writing the JSON compatibility strings on internode metadata RPCs and send only the
|
|
/// msgpack `_bin` payloads (grpc-optimization P2-1).
|
|
///
|
|
/// Defaults to `false` (dual-write, byte-for-byte legacy behavior). This is a rollout lever, not a
|
|
/// wire-format change: it may only be enabled **after** the JSON-fallback counter
|
|
/// (`rustfs_system_network_internode_msgpack_json_fallback_total`) has read zero across a release
|
|
/// window fleet-wide, confirming every peer decodes `_bin` first. Single-env rollback. See
|
|
/// `docs/operations/internode-msgpack-json-convergence-runbook.md`.
|
|
pub const ENV_INTERNODE_RPC_MSGPACK_ONLY: &str = "RUSTFS_INTERNODE_RPC_MSGPACK_ONLY";
|
|
pub const DEFAULT_INTERNODE_RPC_MSGPACK_ONLY: bool = false;
|
|
|
|
// Compile-time invariant: dual-write by default so the base build is byte-for-byte legacy behavior.
|
|
const _: () = assert!(!DEFAULT_INTERNODE_RPC_MSGPACK_ONLY);
|
|
|
|
/// Consecutive-failure threshold after which an internode peer is marked offline (grpc-optimization
|
|
/// P3 observability).
|
|
///
|
|
/// A peer accrues a failure on each dial failure or RPC-triggered connection eviction, and flips
|
|
/// back online on the next successful dial. Drives the `rustfs_cluster_servers_offline_total` gauge
|
|
/// (parity with MinIO's `minio_cluster_servers_offline_total`). Clamped to at least 1. Defaults to 3.
|
|
pub const ENV_INTERNODE_OFFLINE_FAILURE_THRESHOLD: &str = "RUSTFS_INTERNODE_OFFLINE_FAILURE_THRESHOLD";
|
|
pub const DEFAULT_INTERNODE_OFFLINE_FAILURE_THRESHOLD: u32 = 3;
|
|
|
|
/// Prewarm internode control channels in the background at remote-disk construction, moving the
|
|
/// connect cost off the first RPC (grpc-optimization P3-1).
|
|
///
|
|
/// Best-effort and deduped per peer; failures fall through to the existing lazy connect + recovery
|
|
/// monitor. Defaults to `false` (opt-in): enabling it dials every peer at startup, and it is not yet
|
|
/// validated against a cold-start baseline.
|
|
pub const ENV_INTERNODE_PREWARM: &str = "RUSTFS_INTERNODE_PREWARM";
|
|
pub const DEFAULT_INTERNODE_PREWARM: bool = false;
|
|
|
|
/// Fast-fail (bypass) internode RPCs to a peer already marked offline, instead of paying the connect
|
|
/// timeout, letting quorum proceed sooner (grpc-optimization P3-2).
|
|
///
|
|
/// Defaults to `false` (opt-in): this touches peer routing (consistency-sensitive) and must be
|
|
/// validated with the failover bench before rollout. It does NOT change quorum. The bypass is
|
|
/// self-healing — one request per [`ENV_INTERNODE_OFFLINE_REPROBE_SECS`] is let through to recover a
|
|
/// peer even without a background monitor. Single-env rollback.
|
|
pub const ENV_INTERNODE_OFFLINE_BYPASS: &str = "RUSTFS_INTERNODE_OFFLINE_BYPASS";
|
|
pub const DEFAULT_INTERNODE_OFFLINE_BYPASS: bool = false;
|
|
|
|
/// Re-probe interval (seconds) for the offline bypass: while a peer is offline, one request is let
|
|
/// through this often to attempt recovery. Clamped to a small minimum by callers. Defaults to 5s.
|
|
pub const ENV_INTERNODE_OFFLINE_REPROBE_SECS: &str = "RUSTFS_INTERNODE_OFFLINE_REPROBE_SECS";
|
|
pub const DEFAULT_INTERNODE_OFFLINE_REPROBE_SECS: u64 = 5;
|
|
|
|
// Compile-time invariant: prewarm and offline-bypass are opt-in so the base build is unchanged.
|
|
const _: () = assert!(!DEFAULT_INTERNODE_PREWARM);
|
|
const _: () = assert!(!DEFAULT_INTERNODE_OFFLINE_BYPASS);
|
|
|
|
/// Extra attempts for idempotent, read-only/reentrant control-plane RPCs (e.g. `DiskInfo`) on
|
|
/// transient network failures, with exponential backoff (grpc-optimization P3-3).
|
|
///
|
|
/// Defaults to `0` (disabled) — retries change latency-on-failure behavior and are opt-in. **Only**
|
|
/// idempotent reads use this; write/lock RPCs (`WriteAll`/`RenameData`/`Delete*`/`Lock`/`UnLock`)
|
|
/// must never auto-retry, to preserve quorum and idempotency semantics (see `CLAUDE.md`).
|
|
pub const ENV_INTERNODE_IDEMPOTENT_READ_RETRIES: &str = "RUSTFS_INTERNODE_IDEMPOTENT_READ_RETRIES";
|
|
pub const DEFAULT_INTERNODE_IDEMPOTENT_READ_RETRIES: usize = 0;
|
|
|
|
// ── Control/bulk channel isolation (P1) ──
|
|
// Large `bytes`-carrying unary RPCs (ReadAll/WriteAll/ReadMultiple/BatchReadVersion) otherwise
|
|
// share the control-plane HTTP/2 connection with latency-sensitive lock/health RPCs; a large
|
|
// transfer can head-of-line block a lock RPC (G2/G5). When isolation is enabled these bulk RPCs
|
|
// are routed onto a separate per-peer channel pool. See grpc-optimization P1.
|
|
|
|
/// Enable control/bulk internode gRPC channel isolation.
|
|
///
|
|
/// Defaults to `false`: this touches lock-RPC transport routing (consistency-sensitive), so it
|
|
/// is opt-in and validated against a baseline before rollout. When `false`, bulk RPCs reuse the
|
|
/// control channel exactly as before, so the switch is a single-env rollback.
|
|
pub const ENV_INTERNODE_CHANNEL_ISOLATION: &str = "RUSTFS_INTERNODE_CHANNEL_ISOLATION";
|
|
pub const DEFAULT_INTERNODE_CHANNEL_ISOLATION: bool = false;
|
|
|
|
// Compile-time invariant: isolation is opt-in so the default build behaves exactly as before P1.
|
|
const _: () = assert!(!DEFAULT_INTERNODE_CHANNEL_ISOLATION);
|
|
|
|
/// Number of bulk channels maintained per peer when channel isolation is enabled.
|
|
///
|
|
/// A tonic `Channel` is a single TCP/HTTP2 connection; multiple bulk channels are round-robined
|
|
/// to relieve the single-connection throughput ceiling for large transfers. Kept small (default
|
|
/// 2) to avoid a connection storm; clamped to at least 1. Set to 1 to isolate bulk onto a single
|
|
/// dedicated connection (still separate from control).
|
|
pub const ENV_INTERNODE_BULK_CHANNELS: &str = "RUSTFS_INTERNODE_BULK_CHANNELS";
|
|
pub const DEFAULT_INTERNODE_BULK_CHANNELS: usize = 2;
|
|
|
|
/// Profile selector for conservative internode HTTP data-plane client tuning.
|
|
pub const ENV_INTERNODE_HTTP_TUNING_PROFILE: &str = "RUSTFS_INTERNODE_HTTP_TUNING_PROFILE";
|
|
pub const DEFAULT_INTERNODE_HTTP_TUNING_PROFILE: &str = "legacy";
|
|
|
|
/// Internode HTTP connection pool maximum idle connections per host.
|
|
pub const ENV_INTERNODE_HTTP_POOL_MAX_IDLE_PER_HOST: &str = "RUSTFS_INTERNODE_HTTP_POOL_MAX_IDLE_PER_HOST";
|
|
|
|
/// Internode HTTP connection pool idle timeout in seconds.
|
|
pub const ENV_INTERNODE_HTTP_POOL_IDLE_TIMEOUT_SECS: &str = "RUSTFS_INTERNODE_HTTP_POOL_IDLE_TIMEOUT_SECS";
|
|
|
|
/// Internode HTTP/2 initial stream window size in bytes.
|
|
pub const ENV_INTERNODE_HTTP2_INITIAL_STREAM_WINDOW_SIZE: &str = "RUSTFS_INTERNODE_HTTP2_INITIAL_STREAM_WINDOW_SIZE";
|
|
|
|
/// Internode HTTP/2 initial connection window size in bytes.
|
|
pub const ENV_INTERNODE_HTTP2_INITIAL_CONNECTION_WINDOW_SIZE: &str = "RUSTFS_INTERNODE_HTTP2_INITIAL_CONNECTION_WINDOW_SIZE";
|
|
|
|
/// Whether internode HTTP/2 adaptive window sizing is enabled.
|
|
pub const ENV_INTERNODE_HTTP2_ADAPTIVE_WINDOW: &str = "RUSTFS_INTERNODE_HTTP2_ADAPTIVE_WINDOW";
|
|
|
|
/// Internode HTTP proxy mode: legacy, off, or system.
|
|
pub const ENV_INTERNODE_HTTP_PROXY: &str = "RUSTFS_INTERNODE_HTTP_PROXY";
|
|
|
|
/// Environment variable for selecting the internode data-plane transport backend.
|
|
pub const ENV_RUSTFS_INTERNODE_DATA_TRANSPORT: &str = "RUSTFS_INTERNODE_DATA_TRANSPORT";
|
|
pub const DEFAULT_INTERNODE_DATA_TRANSPORT: &str = "tcp-http";
|
|
|
|
/// Legacy alias for "tcp-http". Both values select the TCP/HTTP transport backend.
|
|
pub const INTERNODE_DATA_TRANSPORT_TCP: &str = "tcp";
|
|
|
|
/// Known internode transport backend names accepted by the config parser.
|
|
pub const KNOWN_INTERNODE_DATA_TRANSPORT_BACKENDS: &[&str] = &[DEFAULT_INTERNODE_DATA_TRANSPORT, INTERNODE_DATA_TRANSPORT_TCP];
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn internode_timeout_defaults_stay_in_expected_bounds() {
|
|
assert_eq!(DEFAULT_INTERNODE_CONNECT_TIMEOUT_SECS, 3);
|
|
assert_eq!(DEFAULT_INTERNODE_TCP_KEEPALIVE_SECS, 10);
|
|
assert_eq!(DEFAULT_INTERNODE_HTTP2_KEEPALIVE_INTERVAL_SECS, 5);
|
|
assert_eq!(DEFAULT_INTERNODE_HTTP2_KEEPALIVE_TIMEOUT_SECS, 20);
|
|
assert_eq!(DEFAULT_INTERNODE_RPC_TIMEOUT_SECS, 30);
|
|
assert_eq!(DEFAULT_INTERNODE_HTTP_TUNING_PROFILE, "legacy");
|
|
}
|
|
|
|
#[test]
|
|
fn internode_rpc_channel_tuning_defaults() {
|
|
assert_eq!(DEFAULT_INTERNODE_RPC_HTTP2_STREAM_WINDOW_SIZE, 1024 * 1024);
|
|
assert_eq!(DEFAULT_INTERNODE_RPC_HTTP2_CONN_WINDOW_SIZE, 2 * 1024 * 1024);
|
|
// The nodelay-on and connection-window >= stream-window invariants are enforced at
|
|
// compile time next to the constant definitions.
|
|
}
|
|
|
|
#[test]
|
|
fn internode_rpc_channel_tuning_env_names_are_stable() {
|
|
assert_eq!(ENV_INTERNODE_RPC_TCP_NODELAY, "RUSTFS_INTERNODE_RPC_TCP_NODELAY");
|
|
assert_eq!(
|
|
ENV_INTERNODE_RPC_HTTP2_STREAM_WINDOW_SIZE,
|
|
"RUSTFS_INTERNODE_RPC_HTTP2_STREAM_WINDOW_SIZE"
|
|
);
|
|
assert_eq!(ENV_INTERNODE_RPC_HTTP2_CONN_WINDOW_SIZE, "RUSTFS_INTERNODE_RPC_HTTP2_CONN_WINDOW_SIZE");
|
|
assert_eq!(ENV_INTERNODE_RPC_MAX_MESSAGE_SIZE, "RUSTFS_INTERNODE_RPC_MAX_MESSAGE_SIZE");
|
|
assert_eq!(
|
|
ENV_INTERNODE_RPC_LARGE_PAYLOAD_WARN_BYTES,
|
|
"RUSTFS_INTERNODE_RPC_LARGE_PAYLOAD_WARN_BYTES"
|
|
);
|
|
assert_eq!(DEFAULT_INTERNODE_RPC_LARGE_PAYLOAD_WARN_BYTES, 8 * 1024 * 1024);
|
|
}
|
|
|
|
#[test]
|
|
fn internode_channel_isolation_defaults_and_env_names() {
|
|
// The isolation-off default is asserted at compile time next to the constant definition.
|
|
assert_eq!(DEFAULT_INTERNODE_BULK_CHANNELS, 2);
|
|
assert_eq!(ENV_INTERNODE_CHANNEL_ISOLATION, "RUSTFS_INTERNODE_CHANNEL_ISOLATION");
|
|
assert_eq!(ENV_INTERNODE_BULK_CHANNELS, "RUSTFS_INTERNODE_BULK_CHANNELS");
|
|
}
|
|
|
|
#[test]
|
|
fn internode_msgpack_only_env_name_is_stable() {
|
|
// The dual-write-by-default invariant is asserted at compile time next to the definition.
|
|
assert_eq!(ENV_INTERNODE_RPC_MSGPACK_ONLY, "RUSTFS_INTERNODE_RPC_MSGPACK_ONLY");
|
|
}
|
|
|
|
#[test]
|
|
fn internode_offline_failure_threshold_defaults_and_env_name() {
|
|
assert_eq!(DEFAULT_INTERNODE_OFFLINE_FAILURE_THRESHOLD, 3);
|
|
assert_eq!(ENV_INTERNODE_OFFLINE_FAILURE_THRESHOLD, "RUSTFS_INTERNODE_OFFLINE_FAILURE_THRESHOLD");
|
|
}
|
|
|
|
#[test]
|
|
fn internode_p3_lifecycle_defaults_are_opt_in() {
|
|
// The opt-in (default-off) invariants are asserted at compile time next to the definitions.
|
|
assert_eq!(DEFAULT_INTERNODE_OFFLINE_REPROBE_SECS, 5);
|
|
assert_eq!(DEFAULT_INTERNODE_IDEMPOTENT_READ_RETRIES, 0);
|
|
assert_eq!(ENV_INTERNODE_PREWARM, "RUSTFS_INTERNODE_PREWARM");
|
|
assert_eq!(ENV_INTERNODE_OFFLINE_BYPASS, "RUSTFS_INTERNODE_OFFLINE_BYPASS");
|
|
assert_eq!(ENV_INTERNODE_OFFLINE_REPROBE_SECS, "RUSTFS_INTERNODE_OFFLINE_REPROBE_SECS");
|
|
assert_eq!(ENV_INTERNODE_IDEMPOTENT_READ_RETRIES, "RUSTFS_INTERNODE_IDEMPOTENT_READ_RETRIES");
|
|
}
|
|
|
|
#[test]
|
|
fn internode_timeout_env_names_are_stable() {
|
|
assert_eq!(ENV_INTERNODE_CONNECT_TIMEOUT_SECS, "RUSTFS_INTERNODE_CONNECT_TIMEOUT_SECS");
|
|
assert_eq!(ENV_INTERNODE_TCP_KEEPALIVE_SECS, "RUSTFS_INTERNODE_TCP_KEEPALIVE_SECS");
|
|
assert_eq!(
|
|
ENV_INTERNODE_HTTP2_KEEPALIVE_INTERVAL_SECS,
|
|
"RUSTFS_INTERNODE_HTTP2_KEEPALIVE_INTERVAL_SECS"
|
|
);
|
|
assert_eq!(
|
|
ENV_INTERNODE_HTTP2_KEEPALIVE_TIMEOUT_SECS,
|
|
"RUSTFS_INTERNODE_HTTP2_KEEPALIVE_TIMEOUT_SECS"
|
|
);
|
|
assert_eq!(ENV_INTERNODE_RPC_TIMEOUT_SECS, "RUSTFS_INTERNODE_RPC_TIMEOUT_SECS");
|
|
assert_eq!(ENV_INTERNODE_HTTP_TUNING_PROFILE, "RUSTFS_INTERNODE_HTTP_TUNING_PROFILE");
|
|
assert_eq!(ENV_INTERNODE_HTTP_POOL_MAX_IDLE_PER_HOST, "RUSTFS_INTERNODE_HTTP_POOL_MAX_IDLE_PER_HOST");
|
|
assert_eq!(ENV_INTERNODE_HTTP_POOL_IDLE_TIMEOUT_SECS, "RUSTFS_INTERNODE_HTTP_POOL_IDLE_TIMEOUT_SECS");
|
|
assert_eq!(
|
|
ENV_INTERNODE_HTTP2_INITIAL_STREAM_WINDOW_SIZE,
|
|
"RUSTFS_INTERNODE_HTTP2_INITIAL_STREAM_WINDOW_SIZE"
|
|
);
|
|
assert_eq!(
|
|
ENV_INTERNODE_HTTP2_INITIAL_CONNECTION_WINDOW_SIZE,
|
|
"RUSTFS_INTERNODE_HTTP2_INITIAL_CONNECTION_WINDOW_SIZE"
|
|
);
|
|
assert_eq!(ENV_INTERNODE_HTTP2_ADAPTIVE_WINDOW, "RUSTFS_INTERNODE_HTTP2_ADAPTIVE_WINDOW");
|
|
assert_eq!(ENV_INTERNODE_HTTP_PROXY, "RUSTFS_INTERNODE_HTTP_PROXY");
|
|
assert_eq!(ENV_RUSTFS_INTERNODE_DATA_TRANSPORT, "RUSTFS_INTERNODE_DATA_TRANSPORT");
|
|
assert_eq!(DEFAULT_INTERNODE_DATA_TRANSPORT, "tcp-http");
|
|
assert_eq!(INTERNODE_DATA_TRANSPORT_TCP, "tcp");
|
|
assert_eq!(KNOWN_INTERNODE_DATA_TRANSPORT_BACKENDS, &["tcp-http", "tcp"]);
|
|
}
|
|
}
|