Skip to content

linstor: restoring a PVC from a VolumeSnapshot leaves two of three replicas empty (silent data loss) #4862

Description

Describe the bug

Restoring a PVC from a LINSTOR VolumeSnapshot gives one replica with the data and two empty replicas, and all three are marked UpToDate. A pod that lands on an empty replica mounts a filesystem without the snapshot's files. This is silent data loss on restore: nothing fails up front, and a reader can see an empty volume before anything looks wrong.

Environment

  • Cozystack main at 1d813c944, plus the same main with the piraeus-server image rebuilt on Debian trixie (an open dependency roll-up). Both behave the same, so the base image is not the cause.
  • Test cluster: three Talos nodes, kernel 6.18, DRBD 9.3.2, thick ZFS storage pool data.
  • linstor-csi v1.11.2, LINSTOR 1.33.3, drbd-utils 9.34.3 (bookworm image) and 9.35.0 (trixie image).
  • StorageClass replicated with placementCount: "3", storagePool: data, allowRemoteVolumeAccess: "true".

To Reproduce

  1. Create a 1Gi RWO PVC on replicated, write a file from a pod, record its sha256, delete the pod.
  2. Take a VolumeSnapshot with class linstor-snapshots and wait for readyToUse.
  3. Create a PVC with dataSource set to the snapshot and a pod that reads the file.
  4. Run zfs get origin data/<pv>_00000 on each node and linstor resource list --resources <pv>.

Expected behaviour

All three replicas hold the snapshot's data, as clones or after a full resync from the restored replica, and the pod reads the same sha256.

Actual behaviour

Five restores out of five ended the same way, on both satellite images.

Only one node's zvol is a clone of the snapshot. On the other two nodes origin is -, so these are new empty zvols. The satellite on those nodes logs DRBD meta data created for <pv>/0 and then DRBD skipping initial sync for <pv>/0. They come up UpToDate with a fresh UUID and nothing is resynced.

When the peers connect, the kernel logs uuid_compare()=unrelated-data by rule=history-both and Unrelated data, aborting!, and the connections go StandAlone. After that piraeus-ha-controller logs resource '<pv>' on node '<node>' has failed, evicting every minute and keeps deleting the VolumeAttachment. The node also gets the drbd.linbit.com/lost-quorum:NoSchedule taint, so nothing without a toleration is scheduled there until the volume is deleted.

In one run the reader pod got an empty replica:

sha256sum: can't open '/d/blob': No such file or directory

The snapshot itself is fine. I cloned the ZFS snapshot by hand on every node and read it with drbdmeta ... internal dump-md: the metadata is valid and current-uuid is the same on all three.

Additional context

linstor-csi restores on one node on purpose and then autoplaces the rest. See reconcileSnapshotResources in pkg/client/linstor.go at v1.11.2, which calls RestoreSnapshot with Nodes: []string{selectedNode}, followed by reconcileResourcePlacement.

LINSTOR creates the autoplaced replicas as brand new volumes. In the satellite's DrbdLayer.createMetaData (v1.33.3) a new replica either wins the initial UpToDate race and gets a random current UUID, or asks DrbdLayerUtils.skipInitSync. That one returns true for any ZFS or ZFS_THIN volume unless DrbdOptions/ForceInitialSyncPermanent is set. For a resource definition that already holds data this shortcut is wrong, the new replica has to sync from the restored one.

A possible mitigation, not tested yet: set DrbdOptions/ForceInitialSync=true on the resource group. The controller copies it into ForceInitialSyncPermanent when it creates a resource (CtrlRscCrtApiHelper.copyForceInitialSyncProp), and then skipInitSync returns false. The price is a full initial sync for every new ZFS volume, so it needs measuring before it goes into platform defaults. Restoring on every node that has the snapshot would avoid the empty replicas too.

I'm preparing an upstream report for LINSTOR and will link it here.

Activity

  1. added
    kind/bugCategorizes issue or PR as related to a bug
    priority/critical-urgentHighest priority. Must be actively worked on as someone's top priority right now
    area/storageIssues or PRs related to storage (linstor, seaweedfs, bucket, velero, harbor)
    on Oct 11, 2026
  2. lexfrei commented on Oct 11, 2026

    @lexfrei
    ContributorAuthor

    I went through the LINSTOR v1.33.3 and linstor-csi v1.11.2 sources. The empty replicas come from two separate problems, and the resource-group workaround from the description would not fix either of them for snapshots that already exist.

    The first problem is a stale winner of the initial UpToDate race. linstor-csi creates the volume definition with RestoreVolumeDefinitionSnapshot, which copies only DrbdCurrentGi, so the new volume definition has no DRBD_INITIALIZED flag. The restore then copies the snapshot's volume definition properties into it, and Linstor/Drbd/InitialUpToDateOn still names the node that won the race for the source volume (see restoreOnNode). The restored replica already has metadata, so it never takes part in the race. When autoplace puts a replica on that old winner node, DrbdLayer.createMetaData gives the empty zvol a random current UUID and marks it UpToDate. This matches the run where InitialUpToDateOn was NODE0 and the restore ran on node1. Since the empty volume has no filesystem, the winner path also runs mkfs on it.

    The second problem is the skipInitSync shortcut for the other autoplaced replicas, as described in the report.

    DrbdOptions/ForceInitialSync on the resource group or the controller does not help here. copyForceInitialSyncProp runs in createResource only, not in createResourceFromSnapshot. So the restored resource definition gets ForceInitialSyncPermanent when autoplace runs, after checkDrbdInitializedState has already found nothing to do, and the stale winner stays. It would only cover snapshots of volumes created after the property was set, and every new thick ZFS volume would pay a full initial sync for that.

    LINSTOR already does the right thing for backup shipping. CtrlBackupCreateApiCallHandler sets ForceInitialSyncPermanent on the resource definition props of the snapshot. On restore this recreates metadata on the restored replica, makes it the race winner and clears DRBD_INITIALIZED, so every other replica does a full sync from it. The existing mkfs step checks for a filesystem first, so restored data is not touched. My plan is to set the same property on the target resource definition at the start of a restore that comes through the API. Rollback goes through the same function but restores every node from its own snapshot, so it stays as is. The cost lands only on restored volumes: one full sync per extra replica, which a one-node restore can't avoid anyway. New volumes keep skipping the initial sync.

    The stale winner path does not depend on the storage provider, so LVM thick is affected too, and thin pools hit skipInitSync the same way as ZFS. DrbdOptions/auto-diskful is enabled in Cozystack and adds diskful replicas later through createResource, so a restored volume can get an empty replica long after the restore.

    In Cozystack this hits every PVC created from a VolumeSnapshot. That includes Velero backups with snapshotMoveData: the data mover reads a PVC restored from the snapshot, so the backup itself can capture an empty volume. Tenant Kubernetes PVCs restored through kubevirt-csi go the same way, via a CDI DataVolume with a snapshot source. Golden image clones use csi-clone, and LINSTOR clones on every source node and copies the volume definition flags, so that path looks fine. I'll check it on a test cluster together with the fix.

    Next: a piraeus-server patch for the controller, a check on a three-node cluster (checksum from every replica, drbdadm verify with zero out-of-sync, a snapshot taken before the change), resync time, and the same change sent upstream.

  3. lexfrei commented on Oct 11, 2026

    @lexfrei
    ContributorAuthor

    A correction to my previous comment first. Only the stale-winner problem needs a fix, and LINBIT already fixed it upstream in LINBIT/linstor-server@9d8cdf5, released in v1.34.0. I backported that commit to our v1.33.3 build and ran it on a three-node cluster. The replicas that skip the initial sync are fine in practice. DRBD resyncs them from the restored replica, and the backing zvols end up byte-identical. A carried patch that forces a full initial sync is not needed, so I dropped that plan.

    Results with the backport: 5 restores out of 5 came up with every replica connected and UpToDate, the same checksum from a pod on each node, and identical zvols on all three nodes. In 4 of the 5, the source volume's winner was on a different node than the restore, which is the case that broke before. A snapshot taken before the change restores fine too. The resync after a restore costs about the written data: around 40 s for 1Gi and 134 s for 7.5 GiB written on a 10Gi volume.

    One measurement trap, in case someone repeats this: compare replicas through their backing devices only with dd iflag=direct. A buffered read of /dev/zvol/... can return stale page cache. On the same replica I got zeros from a buffered read and the real data with O_DIRECT.

    Known limitation, which upstream v1.34 shares since that code did not change: the autoplaced replicas rely on the source volume's day0 UUID being in the restored replica's history. If it ever isn't, DRBD goes StandAlone with "unrelated data" instead of syncing. That failure is visible, not silent, and I could not reproduce it.

    How to check and repair volumes restored before the fix

    The fix does not touch volumes that were already restored. In every broken restore I saw, exactly one replica was wrong: the stale winner. It holds an empty filesystem created by LINSTOR, while the restored replica and the others are intact. Every replica still shows UpToDate. The only sign in linstor resource list is StandAlone in the connections column, and nothing marks the volume as degraded.

    An affected volume has all of these:

    • Linstor/Drbd/InitialUptoDateOn in linstor volume-definition list-properties <pv> 0 names a node whose backing zvol has no origin (zfs get -H -o value origin <pool>/<pv>_00000 prints -), while another node's zvol has a snapshot as its origin.
    • That replica is StandAlone towards every peer in linstor resource list --resources <pv>.
    • The kernel log on that node has uuid_compare()=unrelated-data and Unrelated data, aborting! for the resource.

    On a healthy restored volume, InitialUptoDateOn names the node whose zvol is a clone of the snapshot, and every connection is Ok.

    To repair it, recreate the bad replica so it resyncs from the others:

    linstor resource delete <stale-node> <pv>
    linstor resource create <stale-node> <pv> --storage-pool <pool>

    drbdadm connect --discard-my-data does not help here, because DRBD treats unrelated data differently from a split brain. I tested the delete and recreate on a broken volume: the replica resynced and its zvol matched the other two byte for byte.

    Before deleting, check where the volume was attached. If an application already ran on the stale replica, it wrote to a different filesystem than the one on the other replicas. Deleting the replica throws those writes away, so decide which copy to keep first.

  4. added 4 commits that reference this issue on Oct 11, 2026
    c9479a4
    9a03527
    a24c1bd
    e5689e2
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

area/storageIssues or PRs related to storage (linstor, seaweedfs, bucket, velero, harbor)kind/bugCategorizes issue or PR as related to a bugpriority/critical-urgentHighest priority. Must be actively worked on as someone's top priority right nowtriage/acceptedIndicates an issue is ready to be actively worked on

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions