public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: Ciro Iriarte <cyruspy@gmail.com>
To: pve-devel@lists.proxmox.com
Subject: [RFC PATCH storage, qemu-server 0/5] offload full clone to the storage backend
Date: Mon, 20 Jul 2026 07:29:33 -0700 (PDT)	[thread overview]
Message-ID: <20260720.0.copyoffload@cyruspy.gmail.com> (raw)

This implements the contract discussed in "[RFC storage, qemu-server] offload
full/live clone to storage backend" (2026-06-30). Still RFC: three questions
from that thread are open, listed at the end.

Fiona asked to see the qemu-server side alongside the storage patches, so 5/5
is that consumer. It is included for context and is the least finished part.

Design
------

Three phases rather than one call:

  copy_image_prepare()  runs under the target storage lock, reserves a real
                        and freeable target volume, returns its volname
  copy_image_start()    fixes the source's point in time and begins the copy
  copy_image_status()   polled outside both, reports 'complete' only when the
                        copy is INDEPENDENT of the source

A synchronous hook is not workable. cfs_lock_storage() sets alarm(60) around
the locked code, and an array-side clone can run for minutes or hours, so the
worker is killed and the lock broken. The existing host path already holds the
lock only for vdisk_alloc() and runs qemu-img convert outside it.

The split also gives the caller something cheap enough to put inside a guest
freeze. start() does the minimum that pins the source; the data movement is
reported by status(). Allocation is not cheap enough for that, which is why it
is a separate phase.

'complete' means independent, not readable. An RBD clone is readable the
moment it exists but still depends on its parent snapshot until the flatten
finishes, and deleting the source before that destroys it. Distinguishing the
two is the main thing the status phase exists for.

Two features rather than a capability argument on volume_has_feature(), per
Fiona's suggestion: copy-offload-atomic and copy-offload-bulk. Only the atomic
class has implementations here. Enabled per storage with copy-offload, and
ignored unless both storages are instances of the same plugin type.

Implemented
-----------

  rbd      snapshot + clone v2, flatten handed to the Ceph manager
  dir      reflink (FICLONE), where the filesystem supports it
  btrfs    subvolume snapshot
  lvmthin  thin snapshot

The last three are instant and independent immediately. RBD is the case that
motivated the asynchronous shape.

Verified on Ceph (Tentacle 20.2.1), on loop-backed XFS with reflink=1, and on
a loop-backed btrfs and thin pool. In each case the copy is byte-identical, a
copy taken from a snapshot holds the snapshot's content rather than live data,
and the copy still reads correctly after the source is deleted outright.
src/test/copy_offload_functional_test.pl drives the hooks against real loop
devices; it needs root, so it is not part of run_plugin_tests.pl and skips
when it cannot run.

One thing worth flagging
------------------------

The caller drops the target storage lock between prepare() and start(), so the
reserved name has to stay reserved across a window the plugin does not
control. Whether it does depends on two regexes that do not obviously belong
together: volume listers are anchored, while $get_vm_disk_number is not. A
placeholder parked under a prefixed name is therefore invisible to
list_images() but still reserves the disk number, and a '.copytmp' suffix is
the opposite on both counts.

Getting that wrong is not cosmetic. If the parked name stops reserving, a
concurrent allocation can take it, and the failing copy's rollback then frees
that other volume. btrfs and RBD both had this; btrfs now swaps with
renameat2(RENAME_EXCHANGE) so the name is never free, and RBD parks under a
prefix instead of removing the placeholder first. copy_offload_naming_test.pm
pins the property since nothing about the naming looks load-bearing.

This is really an argument for question 3 below.

Open questions
--------------

1. move_disk. It needs the bulk path, and may be wanted in the same series
   rather than after it.

2. copy_images_start(), a batch entry point so a multi-disk guest can start
   every copy inside one freeze. There is no in-tree user today, so it is not
   in this series; it would be a droppable last patch.

3. Whether prepare() should return an opaque backend token instead of a
   volname. It no longer blocks anything here, but it is the general answer to
   the rollback problem above: a volname alone is not proof of ownership once
   start() is allowed to move things around outside the lock. A token the
   rollback verifies would let a plugin refuse to free a volume that is no
   longer its own, rather than each backend arranging its own naming.

Ciro Iriarte (5):
  storage: add asynchronous copy-offload hook for full copies
  rbd: implement copy-offload via snapshot + clone + flatten
  dir: implement copy-offload via reflink (FICLONE)
  btrfs, lvmthin: implement copy-offload (atomic class)
  qemu-server: use storage copy-offload for full clone

 pve-storage:
  ApiChangeLog                             |  24 +++
  src/PVE/Storage.pm                       | 237 ++++++++++++++++++++++-
  src/PVE/Storage/BTRFSPlugin.pm           | 250 ++++++++++++++++++++++++
  src/PVE/Storage/DirPlugin.pm             | 140 ++++++++++++++
  src/PVE/Storage/LvmThinPlugin.pm         | 189 ++++++++++++++++++
  src/PVE/Storage/Plugin.pm                | 176 +++++++++++++++++
  src/PVE/Storage/RBDPlugin.pm             | 318 +++++++++++++++++++++++++++++++
  src/test/copy_offload_feature_test.pm    |  98 ++++++++++
  src/test/copy_offload_functional_test.pl | 286 +++++++++++++++++++++++++++
  src/test/copy_offload_naming_test.pm     |  82 ++++++++
  src/test/copy_offload_test.pm            |  90 +++++++++
  src/test/run_plugin_tests.pl             |   3 +
  12 files changed, 1891 insertions(+), 2 deletions(-)

 qemu-server:
  src/PVE/API2/Qemu.pm           |  17 ++++
  src/PVE/QemuServer.pm          | 201 ++++++++++++++++++++++++++++++++++++++++-
  src/PVE/QemuServer/BlockJob.pm |  32 ++++++-
  3 files changed, 245 insertions(+), 5 deletions(-)



             reply	other threads:[~2026-07-20 14:30 UTC|newest]

Thread overview: 6+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-07-20 14:29 Ciro Iriarte [this message]
2026-07-20 14:29 ` [RFC PATCH storage 1/5] storage: add asynchronous copy-offload hook for full copies Ciro Iriarte
2026-07-20 14:29 ` [RFC PATCH storage 2/5] rbd: implement copy-offload via snapshot + clone + flatten Ciro Iriarte
2026-07-20 14:30 ` [RFC PATCH storage 3/5] dir: implement copy-offload via reflink (FICLONE) Ciro Iriarte
2026-07-20 14:30 ` [RFC PATCH storage 4/5] btrfs, lvmthin: implement copy-offload (atomic class) Ciro Iriarte
2026-07-20 14:30 ` [RFC PATCH qemu-server 5/5] use storage copy-offload for full clone Ciro Iriarte

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260720.0.copyoffload@cyruspy.gmail.com \
    --to=cyruspy@gmail.com \
    --cc=pve-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal