lvol/blob: hold client IO across leadership promotion; demote candidates on writer conflict - #57
Open
schmidt-scaled wants to merge 2 commits into
Open
lvol/blob: hold client IO across leadership promotion; demote candidates on writer conflict#57schmidt-scaled wants to merge 2 commits into
schmidt-scaled wants to merge 2 commits into
Conversation
…ished leaders spdk_lvs_change_leader_state() guarded its entire freeze -> block_port -> demote sequence on lvs->leader, so a writer conflict arriving while a node was mid-promotion (failover update running, lvs->leader still false) was silently ignored: no port block, no response queueing, no demotion of the promotion state. The candidate's NVMf ports stayed open and the subsequent failed-update path flushed queued client IO as EIO straight to connected initiators (k8s_native_scale_break 2026-07-16 23:44:2x: both survivors of a primary shutdown were candidates, 'total blocked ports: 0' throughout, 5978 INTERNAL DEVICE ERRORs, client ext4 journal aborts). Match promotion candidates (update_in_progress) in addition to established leaders, and reset retry_on_update in both conflict paths so an aborted promotion starts from a clean retry budget. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ng/failing it A leadership failover on a non-leader lvolstore (accept-and-promote) had two windows where client IO escaped the freeze machinery and reached initiators as non-retryable errors (k8s_native_scale_break 2026-07-16 23:44, ext4 journal aborts on 10 PVCs): 1. Pre-confirmation pass-through: the promotion trigger flips the blobstore leader flag immediately but starts the confirming lvolstore update only after a 500 ms poller delay, and blob IO is queued solely on frozen_refcnt — so IO accepted in that window executed straight into the journal from a node whose leadership was never confirmed. Those premature appends are what the JC flags as a writer conflict, which then blocks the distribs the confirmation update itself needs. 2. Flush-on-failure: every failed confirmation attempt flushed the frozen queues with EIO (spdk_blob_update_failed_cleanup per pending lvol), surfacing thousands of INTERNAL DEVICE ERRORs during a 2.5 s contended failover instead of holding the IO for the retry. Introduce a blobstore-level promotion_hold: engaged at the promotion trigger, it queues all user blob IO on the existing per-channel queued_io list. The hold is released and flushed on exactly three events: leadership confirmed (ops execute in order), writer-conflict demotion (ops abort behind the ports the conflict path blocks), or retry-budget exhaustion (new spdk_lvs_promotion_give_up blocks the LVS data/hublvol ports FIRST, then fails the queues behind them — the initiator sees a path drop and retries elsewhere, never a device error). Failed confirmation attempts now retry in place (500 ms, existing 4-attempt budget) with all IO still held. Also hardened for the conflict-during-promotion case fix (previous commit): late completions of a cancelled update no longer re-claim leadership or trip the timeout_trigger abort, and a cancelled candidate's pending_update_lvols are drained (releasing their failover-freeze refs) so a later promotion cannot double-insert them. UNTESTED: needs CI + failover soak (graceful shutdown of an LVS primary under saturated redirect load, 64+ clients). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
RCA of
k8s_native_scale_break2026-07-16 23:44 (graceful shutdown of an LVS primary under 64-pod load): a normal leadership failover turned into ~6,000 client-visibleINTERNAL DEVICE ERROR (00/06)completions and ext4 journal aborts (filesystems remounted read-only) on 10 PVCs, within 3 seconds.Root cause chain (validated against pod logs + this source):
vbdev_hublvol_submit_request->spdk_lvs_check_active_process) flips the blobstore leader flag immediately, but the confirming lvolstore update starts only after a 500 ms poller delay. Blob IO is queued solely onfrozen_refcnt, so IO accepted in that window executes straight into the journal from a node whose leadership was never confirmed. The JC flags those premature appends as a writer conflict (observed at 23:44:22.960, before either survivor's update had started), which demotes the distribs (b_block_new_io=1) — blocking the very superblock write the confirmation update needs.spdk_lvs_change_leader_state()gated its whole freeze ->block_port-> demote sequence onlvs->leader, so mid-promotion candidates were ignored: no port block (total blocked ports: 0throughout the incident on both survivors).spdk_blob_update_failed_cleanupper pending lvol) — 4 attempts over 2.5 s, thousands of errors through open ports. As a non-path NVMe status, initiator multipath does not retry these; ext4 aborts its journal on the failed writes.Fix
Commit 1 — demote promotion candidates on writer conflict.
spdk_lvs_change_leader_state()now matchesupdate_in_progressstores in addition to established leaders, so a candidate gets the same freeze -> port-block -> demote treatment that makes established-leader conflicts invisible to clients. Retry state is reset in both conflict paths.Commit 2 —
promotion_hold: hold client IO across the promotion.promotion_hold, engaged at the promotion trigger: user blob IO submitted while set is queued on the existing per-channelqueued_iolist (same mechanism as the per-blob freeze). No premature journal appends -> the conflict source of this incident is gone.spdk_lvs_set_leader(true)): held ops execute in order;spdk_lvs_promotion_give_up()blocks the LVS data port and rejects the hublvol port first (mirroring the conflict demotion sequence, incl. the 10 s rule-removal poller), then fails the queues behind the block — initiators see a path drop and retry elsewhere, never a device error.timeout_triggerabort; a cancelled candidate'spending_update_lvolsare drained viaspdk_blob_update_failed_cleanup(releasing theirblob_freeze_on_failoverrefs, preventing double TAILQ insert on the next promotion).Expected behavior change
Failover of an LVS primary under load: clients see a ~1-2 s latency blip (IO held) instead of EIO. If promotion cannot complete (conflict, wedged JC), IO fails behind blocked ports -> multipath path-down semantics instead of non-retryable device errors.
Status / review notes
spdk_lvs_promotion_give_up(called from the failover cpl on the md thread;block_portusessystem()— same as the existing conflict path); (b)bs_flush_held_io_channelop-skip condition for still-frozen blobs; (c) interaction withspdk_lvs_nonleader_timeout's residualspdk_bs_set_leader(true)side effect (left in place; harmless under the hold, but could be removed for clarity).R26.2-PRE(traced branch). Ifmain-latestbuilds from a different branch, cherry-pick both commits there — the touched functions are identical modulo small line offsets.Full incident analysis:
sbcli/k8s_scale_break_rca_validation.md.🤖 Generated with Claude Code