Co-authored-by: heihutu <heihutu@gmail.com>
10 KiB
Internode msgpack/JSON Convergence Runbook
Operational runbook for retiring the redundant JSON compatibility fields on internode gRPC metadata RPCs (grpc-optimization P2-1). This is a cross-version change: it proceeds strictly by observation-gated stages, never in one step.
Background
Internode RPCs dual-encode each metadata value as both:
- a msgpack binary field (
*_bin, e.g.file_info_bin), and - a JSON compatibility string (e.g.
file_info).
Decoders prefer the _bin payload and fall back to the JSON string only when _bin is
empty (decode_msgpack_or_json). The dual-write costs bandwidth and CPU. Before the JSON
fields can be dropped, the fallback branch must be proven unused in production —
otherwise a rolling upgrade with mixed node versions could read an emptied field.
The observation metric (already shipped)
rustfs_system_network_internode_msgpack_json_fallback_total{direction, message}
rustfs_system_network_internode_msgpack_json_decode_total{direction, message, codec}
rustfs_system_network_internode_msgpack_json_decode_error_total{direction, message, codec}
The decode counter increments after a successful msgpack or JSON compatibility decode. The fallback counter increments whenever a decode falls back to the JSON field because the msgpack payload was absent. The decode-error counter increments whenever either codec fails to decode.
direction="request"— a server decoding a peer's request (node_service/disk.rs).direction="response"— a client decoding a peer's response (cluster/rpc/remote_disk.rs), including the list-levelReadMultiple/BatchReadVersionfallbacks.message— the value name, e.g.FileInfo,RawFileInfo,ReadMultipleResp.codec— the failed codec for decode errors:msgpackfor corrupt non-empty_bin, orjsonfor corrupt legacy fallback JSON.
Stage 0 — Observe (current stage)
Ship the current release (which contains these counters) and let it run for at least one full release window across the whole fleet. The fallback and decode-error counters must stay at zero.
First confirm each expected message/direction has real traffic in the observation window:
sum by (direction, message, codec) (
increase(rustfs_system_network_internode_msgpack_json_decode_total[30d])
)
For every convergence-ready message/direction below, the codec="msgpack" series must be non-zero before a zero fallback result is meaningful. Missing series, zero traffic, counter reset, or scrape gaps make the gate inconclusive rather than passed.
Confirm zero across the observation window (adjust [30d] to the window length):
sum by (direction, message) (
increase(rustfs_system_network_internode_msgpack_json_fallback_total[30d])
)
Every series must be 0. A non-zero value means some peer is still emitting an empty
_bin (an old node, or a message whose sender does not fill _bin) — investigate the
{direction, message} label before proceeding.
Decode errors must also stay at zero across the observation window:
sum by (direction, message, codec) (
increase(rustfs_system_network_internode_msgpack_json_decode_error_total[30d])
)
A non-zero codec="msgpack" series means a peer sent corrupt or incompatible _bin bytes; it must fail closed and block convergence. A non-zero codec="json" series means the legacy fallback field was corrupt or semantically incompatible; it also blocks convergence and rollback confidence.
Standing alert (keep enabled through all stages):
- alert: InternodeMsgpackJsonFallback
expr: sum by (direction, message) (increase(rustfs_system_network_internode_msgpack_json_fallback_total[15m])) > 0
for: 5m
labels: { severity: warning }
annotations:
summary: "Internode RPC fell back to JSON decode ({{ $labels.direction }}/{{ $labels.message }})"
description: "A peer sent an empty msgpack _bin payload. Do NOT advance msgpack-only convergence while this fires."
- alert: InternodeMsgpackJsonDecodeError
expr: sum by (direction, message, codec) (increase(rustfs_system_network_internode_msgpack_json_decode_error_total[15m])) > 0
for: 5m
labels: { severity: warning }
annotations:
summary: "Internode RPC msgpack/JSON decode failed ({{ $labels.direction }}/{{ $labels.message }}/{{ $labels.codec }})"
description: "A peer sent an undecodable msgpack or JSON compatibility payload. Do NOT advance msgpack-only convergence while this fires."
Field → peer-decoder audit
The send-side change (Stage 1) may only empty a JSON field whose peer decodes _bin
first. The following mapping is verified against the current code.
Convergence-ready (peer decodes _bin first)
| Direction | Message / field | Peer decoder |
|---|---|---|
| request | WriteMetadata.file_info |
FileInfo (node_service/disk.rs) |
| request | UpdateMetadata.file_info |
FileInfo |
| request | UpdateMetadata.opts |
UpdateMetadataOpts |
| request | RenameData.file_info |
FileInfo |
| request | ReadMultiple.read_multiple_req |
ReadMultipleReq |
| request | BatchReadVersion.batch_read_version_req |
BatchReadVersionReq |
| request | Read*.opts |
ReadOptions |
| response | ReadVersion.file_info |
FileInfo (cluster/rpc/remote_disk.rs) |
| response | ReadXL.raw_file_info |
RawFileInfo |
| response | RenameData.rename_data_resp |
RenameDataResp |
| response | ReadMultiple resp list |
per-item + list fallback |
| response | BatchReadVersion resp list |
per-item + list fallback |
_bin support added THIS release — converge after their own window
The DeleteVersion/DeleteVersions protos had no _bin fields. They gained additive
*_bin fields plus bin-first server decoders in this release, and the client now dual-writes
them. They are kept out of the msgpack-only set (always dual-write) until their own
fallback counter has read zero across a window with the new decoders fully deployed.
| Direction | Message / field | Status |
|---|---|---|
| request | DeleteVersion.file_info (FileInfo) |
_bin added; dual-write; converge after window |
| request | DeleteVersion.opts (DeleteOptions) |
_bin added; dual-write; converge after window |
| request | DeleteVersions.versions (FileInfoVersions) |
_bin added; dual-write; converge after window |
| request | DeleteVersions.opts (DeleteOptions) |
_bin added; dual-write; converge after window |
Any
*_binproto field not in the tables above must be mapped to a confirmed_bin-first peer decoder before it is added to the convergence set.
Still JSON-only (no _bin field)
| Direction | Message / field | Note |
|---|---|---|
| response | DeleteVersion.raw_file_info |
proto has no _bin; needs an additive proto field before it can converge. |
Stage 1 — Stop writing JSON (env-gated, after Stage 0 reads zero)
The send-side lever is implemented and requires two default-off env flags:
RUSTFS_INTERNODE_RPC_MSGPACK_ONLY=trueRUSTFS_INTERNODE_RPC_MSGPACK_ONLY_FLEET_CONFIRMED=true
The first flag only requests msgpack-only. The second flag is the explicit proof gate that the fleet has passed the release-window fallback, capability, and rollback checks. If only RUSTFS_INTERNODE_RPC_MSGPACK_ONLY=true is set, RustFS keeps dual-writing JSON compatibility fields so old JSON-only peers remain compatible. When both flags are enabled, the convergence-ready fields above send only _bin and leave the JSON string empty; the _bin payload is always sent and decoders keep the JSON read fallback unchanged. The delete fields are excluded (dual-write) per the section above.
Only enable it after Stage 0 has read zero for a full window across the fleet:
- Ship with the flag off (no behavior change).
- Rehearse the
request-onlygate withRUSTFS_INTERNODE_RPC_MSGPACK_ONLY=trueandRUSTFS_INTERNODE_RPC_MSGPACK_ONLY_FLEET_CONFIRMED=false. This must keep writing JSON compatibility fields and is safe with unsupported/old peers; any fallback or decode-error increment still blocks convergence. - Enable it on one canary node with both
RUSTFS_INTERNODE_RPC_MSGPACK_ONLY=trueandRUSTFS_INTERNODE_RPC_MSGPACK_ONLY_FLEET_CONFIRMED=true, then restart that node only and watch the fallback and decode-error counters for a soak period while real internode traffic is present. If they stay zero, enable fleet-wide. - Rollback: set either
RUSTFS_INTERNODE_RPC_MSGPACK_ONLY=falseorRUSTFS_INTERNODE_RPC_MSGPACK_ONLY_FLEET_CONFIRMED=false(or unset either flag) and restart. No wire-format was broken in this stage, so rollback is immediate and safe.
The benchmark helper pins these operator states as dry-run phases:
scripts/run_internode_grpc_ab_bench.sh --stage p2 --phase before --dry-run
scripts/run_internode_grpc_ab_bench.sh --stage p2 --phase request-only --dry-run
scripts/run_internode_grpc_ab_bench.sh --stage p2 --phase canary --dry-run
scripts/run_internode_grpc_ab_bench.sh --stage p2 --phase after --dry-run
scripts/run_internode_grpc_ab_bench.sh --stage p2 --phase rollback --dry-run
Stage 2 — Remove the proto JSON fields (next release, N+1)
Only after Stage 1 has been stable with the flag on for a full window and the counter is still zero.
- Mark the retired text fields
reservedincrates/protos/src/node.proto(never reuse the field numbers) and delete the JSON read-fallback branches; codec becomes msgpack-only. - This is a hard wire-format change — it requires the mixed-version upgrade rehearsal (four-node scripts) to pass, and it cannot be rolled back by env alone.
Rollback matrix
| Stage | Wire-format broken? | Rollback |
|---|---|---|
| 0 Observe | no | n/a (metric only) |
| 1 msgpack-only send | no | unset either msgpack-only env flag + restart |
| 2 remove fields | yes | redeploy prior release; field numbers stay reserved |
Related
- Codec + counter implementation: commit
feat(internode): P2 msgpack/JSON codec observability + encode buffer presizing. - Decoders:
decode_msgpack_or_jsonincrates/ecstore/src/cluster/rpc/remote_disk.rs(client) andrustfs/src/storage/rpc/node_service/disk.rs(server).