* fix(replication): close the GA blocker set from backlog#2366 (#7503) * fix(replication): close GA blockers from backlog#2366 Implements the P1 set from the pre-GA replication audit: - Replication rule tag filters now require every And.Tag to match, replacing the s3s OR semantics with a local AND matcher that fails closed on a malformed tag. - A replicated group membership change no longer writes the group status, so a membership update carrying the default Enabled status cannot silently re-enable a disabled group on the peer. - A successful IAM import schedules one collapsed full-IAM snapshot per remote peer instead of leaving the imported entities local-only. - A pending endpoint refresh is redriven by the heavyweight reconcile tick, carries its own ilm-expiry override, and no longer blocks a remove that drops every unacknowledged peer. - Site metrics expose local replication failure totals and rolling windows; node-level counters no longer report a constructed zero. - set/remove-remote-target notify peer metadata caches before returning, so a follow-up put-bucket-replication on another node sees the target. - Adds the site-replication operations runbook, a docs index, a replication support boundary section, and the Replication changelog section. * fix(site-replication): resume only a locally driven endpoint refresh The peer-side edit handler journals a pending endpoint refresh with an empty `remote_peers` map and commits it inside the same request through `apply_internal_peer_edit`. The reconcile tick could not tell that journal from the coordinator's own: with no required peers it reads as complete on sight, so the tick committed it with `edit_state` - losing the local-name sync - and cleared it under the request that owned it, whose commit then reported the refresh as changed and denied the coordinator the peer acknowledgement it was waiting for. Resume now runs only for a journal that carries the fan-out topology. A receiver's journal stays for the coordinator to redrive with the same refresh id, which is the path that already recovers it. * fix(site-replication): keep an explicit disabled group status on a snapshot Skipping the group-status write whenever an item carries members stopped a membership change from re-enabling a disabled group, but it also silenced the full-IAM snapshot, which always sends members together with the sender's real status. A peer that did not have the group yet created it through `GroupInfo::new` - enabled - so a bootstrap, a repair, or the snapshot an IAM import now schedules handed every member of a frozen group live access there. The madmin wire maps an unset `groupStatus` to Enabled, so only Enabled can be a default. Disabled is always explicit and is applied again. * fix(site-replication): schedule the import snapshot without recording a failure `import-iam` reused the failure-recording path to queue its full-IAM snapshot. That raises `retry_count` on every call, so three imports - the normal shape of a bulk migration done one archive at a time - escalated a healthy peer to `retryStats.failed` with the scheduling note shown as `lastError`, which is exactly the signal the runbook tells operators to repair. A full retry queue also turned a completed import into a 503. Scheduling now only ensures the collapsed entry exists, and a failure to schedule is logged instead of failing the request: the entities are already imported and the reconcile pass still closes the gap. * fix(admin): stop reporting replication failures as retries `retries` is the minio-go counter for redeliveries, and mc prints it as such. Filling it with the failure count claimed a redelivery that never happens: a failed object is not retried by an event today, it waits for the scanner heal pass. `errors` keeps the failure counters; `retries` stays zero until there is a real redelivery to count, and the runbook now says so. * perf(site-replication): aggregate failure windows without cloning bucket stats `site_metrics_snapshot` went through `get_all`, which clones every bucket's stats, and then scanned each target's sample deque twice. That deque is bounded only by the one-hour window, so an unreachable target under load - the case an operator polls this endpoint for - made every `mc admin replicate status` copy the whole backlog and hold the read lock against the failure path while doing it. It now folds under the read lock and takes both windows in one walk. The `max` against the serialized `last_minute` / `last_hour` snapshots is dropped: those are stamped onto per-bucket clones elsewhere and are always zero in this node-local cache. * fix(site-replication): reject a conflicting ilm-expiry override on a re-run The commit now reads the ilm-expiry override back out of the pending refresh journal, so a second edit that asks for a different value had it dropped while the request still reported success. Re-running without the flag keeps pinning the recorded value - that is the documented way to redrive a stuck refresh - but an explicit different value is now rejected instead of ignored. * fix(admin): do not fail a remote-target write on a peer reload error set/remove-remote-target propagated the peer metadata reload error, so a target that was already persisted and live on this node reported a 5xx to the client whenever one peer could not be reached. Every S3 bucket-config write path treats that reload as best effort and only warns; these two admin handlers now do the same, and the reason is logged with the bucket and action. * fix(site-replication): undo every bucket a cut-short refresh rewrote When a remove accepted on another node clears the refresh journal mid-pass, only the bucket holding the lock at that moment had its restored target undone. The buckets rewritten earlier in the same pass kept a target pointing at the removed peer whenever the remove's own cleanup had already walked past them. The undo now covers every bucket this pass rewrote, attempting all of them so one failure does not strand the rest. * fix(site-replication): keep replay running while an endpoint refresh is pending A pending endpoint refresh took the whole heavyweight pass with it, so a peer that never came back froze IAM and bucket replay to every healthy peer too - the stall this journal's resume path was meant to end. The refresh arm now drains the retry queue before returning; it replays per-peer deliveries against the endpoints currently committed in state, so it is unaffected by the edit in flight. Bucket wiring reconciliation still waits, because it rewrites the very targets the refresh is changing, and the runbook now says so. * test(e2e): cover the AND semantics of a two-tag replication filter The acceptance matrix only had a single-tag rule, which matches under both AND and OR semantics and therefore proved nothing about the filter this fix changed. It now also carries a two-tag `And` rule - the shape `mc replicate add --tags "k1=v1&k2=v2"` writes - and asserts that an object with one of the two tags is not admitted while an object with both is. No new test function, so the nightly selection digest is unchanged. * refactor(site-replication): fold the refresh state-change error into one constructor The endpoint-refresh work added three `s3_error!` invocation lines, which the s3s footprint ratchet is meant to prevent. Five copies of the same concurrent-change error now share one constructor, so the surface nets one line smaller than main; the baseline is retightened to match. * fix(site-replication): report a peer whose IAM snapshot waits for a repair An escalated snapshot entry records a deletion a snapshot cannot replay, so only a repair settles it and the marker must survive. Scheduling an import snapshot therefore leaves that peer's entry alone - and now says so, instead of returning success while nothing was scheduled for it. * docs(operations): state the group-status and escalation convergence limits Two boundaries the fixes in this branch make load-bearing: a membership change never carries an enable, so a group disabled on one site only has to be re-enabled there explicitly; and a peer holding an escalated IAM entry does not receive a scheduled snapshot, including the one a bulk import schedules, until a repair settles it. * fix(ci): bind performance runs to selected inputs (#7512) * test(scanner): add G09 upgrade evidence runner Add a reusable Linux x86_64 runner for the Scanner/Heal G09 mixed-version and rollback upgrade evidence lanes. The helper reads the pinned previous-release asset metadata from the upgrade workflow, verifies the downloaded binary, builds the current head, runs both ignored E2E tests, and fails unless the expected G09 JSON artifacts exist. Co-Authored-By: heihutu <heihutu@gmail.com> Co-Authored-By: zhi22915 <qiuzgang@gmail.com> --------- Co-authored-by: 唐小鸭 <tangtang1251@qq.com> Co-authored-by: Zhengchao An <anzhengchao@gmail.com> Co-authored-by: zhi22915 <qiuzgang@gmail.com>
14 KiB
Site Replication Operations
Use this when: operating a site-replication deployment, diagnosing a peer outage or incomplete topology change, pairing sites that already contain data, or planning an upgrade.
Source of truth: rustfs/src/admin/handlers/site_replication.rs,
rustfs/src/site_replication/, and the bucket-replication worker under
crates/ecstore/src/bucket/replication/.
Site replication combines two different convergence paths:
- the control plane replicates buckets, bucket metadata, IAM, and topology;
- ordinary bucket replication moves object versions and delete operations.
An enabled: true response only says that a site has more than one configured
peer. It does not prove that every peer is reachable or caught up. Always read
pendingOperation, retryStats, PeerErrors, and Metrics as well.
Routine checks
Run these commands from an admin workstation with one alias per site:
mc admin replicate info site-a
mc admin replicate status site-a
Check more than one site. A partition can leave each side with a different but locally valid view.
replicate info is the compact control-plane view:
| Field | Interpretation |
|---|---|
enabled |
More than one site is configured; this is not a health verdict. |
sites |
The locally persisted topology. Compare deployment IDs and endpoints on every site. |
retryStats.pending |
Collapsed peer deliveries waiting to be retried. |
retryStats.failed |
Deliveries that crossed the escalation threshold and require attention. |
retryStats.lastError |
A redacted summary of the most recent delivery failure. |
pendingOperation |
A durable multi-step topology operation described below. Absence is the healthy steady state. |
replicate status adds detailed convergence state:
| Field | Interpretation |
|---|---|
Sites / PeerStates |
Configured peers and derived reachability/configuration state. |
PeerErrors |
A peer could not be queried. Its detailed counters may be absent; do not read zeros as success. |
BucketStats |
Per-bucket presence and versioning, replication, lifecycle, Object Lock, and metadata mismatches. |
PolicyStats, UserStats, GroupStats |
IAM inventory mismatches. |
RetryStats |
Durable control-plane retry backlog and escalation count. |
Metrics.replMetrics |
Per-destination online state, downtime, replicated counts/bytes, and failed totals/windows. |
Metrics.queued / Metrics.inProgress |
Object work waiting or active on the responding node. |
Metrics.errors |
Node-level object-replication failures. When only queue statistics are available, RustFS synthesizes a node entry and preserves this counter rather than reporting zero. |
Metrics.retries |
Redeliveries. Always zero today: a failed object is not retried by an event, it waits for the scanner pass described below. Read errors instead. |
Healthy means: the same topology is visible on all sites, no pending operation, no peer error, no failed retry escalation, required bucket/IAM state is in sync, and queue/error counters are stable or falling. Counters are cumulative; alert on their rate and on a backlog that does not drain, not merely on a non-zero total.
Pending operations and recovery
pendingOperation contains operation, an opaque id, pendingPeers, and
ackedPeers. Do not edit the site-replication state object by hand. The marker
is the crash-recovery journal and removing it can make a partially applied
operation look complete.
The heavyweight reconciler runs once at startup and every 600 seconds. The lightweight retry drain runs every 30 seconds. A restart is therefore a valid way to cause an immediate heavyweight pass after the underlying fault has been fixed, but it is not a substitute for fixing connectivity, credentials, TLS, or the remote endpoint.
remove
The original topology and each peer acknowledgement are persisted before the
operation finalizes. While peers remain in pendingPeers, restore access to
them and wait for reconciliation. If a peer is permanently gone, a new remove
request may remove all currently active unacknowledged peers; RustFS permits
that request and then finalizes against the remaining topology. Removing the
local site or all sites is also an explicit completion path.
Do not re-add a site merely to hide this marker. First compare the topology on
all reachable peers. If the same operation ID makes no progress for more than
one heavyweight interval, collect PeerErrors, RetryStats, and the
site-replication logs before retrying the remove.
rotate-svc-acct
Service-account rotation keeps the candidate secrets and peer acknowledgements until every current remote peer accepts the rotation. Restore the failing peer and allow the reconciler to resume it. Do not manually delete either candidate credential during this window: doing so can remove the only credential that a not-yet-acknowledged peer accepts.
After the marker clears, verify replicate status from every site, then retire
any separately retained old credential material according to local policy.
endpoint-refresh
An endpoint, CA, or TLS-verification edit first refreshes the replication target on every active peer and records acknowledgements. On startup and every heavyweight pass, RustFS probes peer capability, uses the endpoint-refresh API when supported (or the legacy peer-edit fallback), refreshes local bucket targets, and commits the edit only after every still-active peer acknowledges.
If this marker is stuck:
- Confirm that the proposed endpoint and CA are correct and reachable from every site, not only from the admin workstation.
- Restore the site-replication service account and TLS trust path.
- Wait for one 600-second pass or restart one healthy node to trigger the startup pass.
- Re-run the identical edit only if the operation remains visible; a different
endpoint edit is rejected while the existing refresh is pending. The journal
pins the edit's payload, so a re-run without
--replicate-ilm-expirykeeps the value the first attempt recorded, and a re-run asking for a different value is rejected. Finish or remove the pending refresh before changing it.
A peer removed from the topology no longer blocks completion. A remove request is accepted when it removes every active unacknowledged peer.
While this marker is present, control-plane retry replay to the other peers keeps running, but bucket wiring reconciliation waits: it rewrites the same targets the refresh is changing. Expect bucket-level drift on this site to persist until the refresh settles.
Outage recovery and convergence time
Control-plane retry begins on the 30-second drain, while heavyweight snapshots, pending topology operations, and bucket wiring are revisited on the 600-second pass. Object MRF entries are persisted every 10 seconds by default and target health is probed every 5 seconds. These are scheduling bounds, not delivery SLAs: network timeouts and the amount of queued work add to them.
Objects that must be rediscovered by the scanner have this conservative upper bound before discovery:
RUSTFS_DATA_USAGE_UPDATE_DIR_CYCLES
× max(RUSTFS_SCANNER_CYCLE, actual duration of one scanner cycle)
The defaults re-descend a compacted directory every 16 cycles. A practical
production starting point for a tighter recovery objective is
RUSTFS_DATA_USAGE_UPDATE_DIR_CYCLES=4; 1 forces re-descent every cycle.
Measure the additional disk and metadata load before lowering it further or
tuning the scanner cadence. For an immediate operator-driven recovery, start a
site resync with mc admin replicate resync start and monitor its status.
Transfer time after discovery remains proportional to backlog size, bandwidth,
worker capacity, and target latency. Use queue depth and the rate of
Metrics.errors rather than the formula alone to decide whether convergence is
progressing.
Pairing sites that already contain data
When more than one requested site is non-empty, preflight considers each bucket name held by more than one site:
- versioning must be
Enabledon every site holding the shared bucket; - Object Lock enablement must be identical on every holder.
A bucket present on only one site is safe: post-add backfill creates it on the other peers. A shared unversioned bucket is rejected because merging can overwrite the only copy of an object. An Object Lock mismatch is rejected because lock enablement cannot be changed after bucket creation and convergence could otherwise strip a WORM guarantee.
If preflight rejects the pair, keep the authoritative copy, delete the
conflicting bucket (or its contents) from all other sites, run replicate add
again, and then start replicate resync from the surviving site. Back up and
validate the authoritative data before deleting anything.
IAM convergence and repair boundary
Ordinary IAM changes are delivered to each peer. A successful bulk IAM import also schedules one collapsed full-IAM snapshot per remote peer. A failed IAM deletion is replayed before that snapshot so the snapshot cannot re-create a principal or grant that was already revoked.
The safety state has two bounds:
- deletion high-water marks are retained for 30 days;
- deletion replay bodies are capped at 256 distinct entities per peer.
Repeated deletion of the same entity replaces its saved body. When the per-peer cap is exceeded or the body cannot be serialized, the retry entry remains escalated rather than pretending the deletion is replayable. An item from an older sender without a source timestamp cannot install the 30-day high-water mark, so verify it explicitly after a prolonged split. A successful drain clears replay bodies; removing the peer prunes its bodies. For an escalated IAM retry, use the site-replication repair workflow for the affected peer and IAM family, then verify users, service accounts, groups, policies, and mappings on both sides. Repair is the operator's explicit accountability transfer and clears the saved deletion bodies only after the IAM repair succeeds.
A group's status converges in one direction. An explicit disable is applied
everywhere, including through a snapshot, but a membership change never
carries an enable - it would otherwise re-enable a group frozen on the
receiving site. If a group ended up disabled on one site only, re-enable it
there explicitly with mc admin group enable; a snapshot or repair will not
do it.
Treat IAM divergence as a security incident: a user deleted on one site can remain usable on an unreachable peer until replay or repair completes. A peer whose IAM entry is escalated does not receive scheduled snapshots either - including the one a bulk import schedules - until the repair settles it.
Encrypted objects
| Source form | Replication behavior | Fail-closed condition |
|---|---|---|
| SSE-S3 | The source decrypts the object; the request sends only AES256 intent; the destination encrypts with its own KMS. Source envelope material never leaves the site. |
The destination cannot satisfy the encryption request, or the source metadata is incomplete/unsupported. The replica is FAILED; plaintext is not silently stored. |
| SSE-KMS | The source decrypts the object; the request sends aws:kms intent without the source-local key ID; the destination selects its own configured KMS key. |
Either side cannot decrypt/encrypt, or the metadata mixes incompatible encryption evidence. |
| SSE-C | Stored ciphertext and the required SSE-C replication transport metadata pass through. RustFS verifies target evidence before accepting the replica. | The target does not echo the customer-algorithm evidence, required material/layout is absent, or the metadata is ambiguous. |
Unknown MinIO/RustFS encryption markers are never forwarded as ordinary user metadata. They fail replication so an operator must migrate or repair the object with a supported format.
Rolling upgrades and rollback
Keep every node in one site on the same version whenever possible. Upgrade all nodes of one site consecutively, verify its startup reconciliation and status, then move to the next site. Do not intentionally leave a site mixed-version: admin requests can land on different nodes, and an older node may not resume a new pending-operation shape or expose its health fields.
Current state additions are optional and defaulted, so older readers ignore them. The target-version ledger is stored as dual-prefixed internal object metadata and is also ignored by older readers; rollback does not corrupt the object format, but older code loses the assigned-version routing improvement.
Before rolling back across the fix that retains the data directory of a version awaiting purge replication (rustfs/rustfs#7307), ensure no version purge is pending. Older code can free that retained version's data directory before the remote purge is acknowledged, leaving unreadable metadata and blocking bucket deletion. Drain or repair replication and take a metadata/data backup first.
Runtime knobs
These values are read when the owning background task starts. Restart the server after changing them. The millisecond intervals have a 10 ms floor; invalid values fall back to the default with a warning.
| Variable | Default | Effect |
|---|---|---|
RUSTFS_REPL_HEALTH_CHECK_INTERVAL_MS |
5000 |
Remote-target health probe interval. Lowering it increases outbound probes. |
RUSTFS_REPL_MRF_FLUSH_INTERVAL_MS |
10000 |
Maximum periodic interval between MRF persistence flushes; 1,000 new entries also trigger a flush. |
RUSTFS_REPL_RESYNC_POLL_MAX_MS |
60000 |
Upper bound for randomized resync retry-poll sleep. |
RUSTFS_REPL_RESYNC_MAX_JOBS |
2 |
Concurrent resync jobs; values are bounded to 1..=32. |
Transport-specific controls and target behavior are documented in Replication outbound transport. Validate a new destination with Replication target check, and read Replication object size limits before moving large objects.