From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from gate001.proxmox.com (gate001.proxmox.com [IPv6:2a0f:8001:1:32::40]) by lore.proxmox.com (Postfix) with ESMTPS id E8AA81FF0E0 for ; Thu, 23 Jul 2026 15:57:36 +0200 (CEST) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id B7EE12149D; Thu, 23 Jul 2026 15:57:36 +0200 (CEST) From: Lukas Sichert To: pve-devel@lists.proxmox.com Subject: [PATCH storage v11 2/6] lvm: saferemove: zero out volumes range by range Date: Thu, 23 Jul 2026 15:56:50 +0200 Message-ID: <20260723135656.102972-3-l.sichert@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260723135656.102972-1-l.sichert@proxmox.com> References: <20260723135656.102972-1-l.sichert@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1784814993148 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.097 Adjusted score from AWL reputation of From: address DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment (newer systems) RCVD_IN_DNSWL_LOW -0.7 Sender listed at https://www.dnswl.org/, low trust SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Message-ID-Hash: EN3YWPZW3WJUA2L2MB6NCKGD7N5INYG5 X-Message-ID-Hash: EN3YWPZW3WJUA2L2MB6NCKGD7N5INYG5 X-MailFrom: l.sichert@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header CC: Lukas Sichert X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: saferemove currently uses different full-volume zero-out paths: `blkdiscard --zeroout` for devices with write-zeroes support and `cstream` otherwise. This makes consistent progress reporting and throttling difficult and prevents interleaving future discard cleanup with zeroing. On thin-provisioned backing storage, zeroing the whole LV first can force unnecessary allocation. Move zeroing into an explicit range loop. Use BLKZEROOUT when supported, cap to the device limit, and fall back to manually writing zeroes via syswrite otherwise. Add progress reporting in the shared loop, and apply a configured saferemove throughput limit there as well. Without an explicit limit, keep BLKZEROOUT unthrottled and throttle only syswrites to 10 MiB/s. Signed-off-by: Lukas Sichert --- src/PVE/Storage/LVMPlugin.pm | 165 ++++++++++++++++++++++++++--------- 1 file changed, 122 insertions(+), 43 deletions(-) diff --git a/src/PVE/Storage/LVMPlugin.pm b/src/PVE/Storage/LVMPlugin.pm index 0035c67..b8eb1ee 100644 --- a/src/PVE/Storage/LVMPlugin.pm +++ b/src/PVE/Storage/LVMPlugin.pm @@ -8,7 +8,9 @@ use File::Basename; use IO::File; use JSON; use List::Util qw(max); +use Time::HiRes qw(clock_gettime CLOCK_MONOTONIC); +use PVE::Format qw(render_bytes render_duration); use PVE::JSONSchema qw(get_standard_option); use PVE::RESTEnvironment qw(log_warn); use PVE::Tools qw(run_command file_read_firstline trim); @@ -20,6 +22,10 @@ use base qw(PVE::Storage::Plugin); # lvm helper functions +use constant { + BLKZEROOUT => 0x127f, +}; + my $ignore_no_medium_warnings = sub { my $line = shift; # ignore those, most of the time they're from (virtual) IPMI/iKVM devices @@ -324,6 +330,13 @@ my sub rename_after_failed_cleanup { } } +my sub blockdev_ioctl_range { + my ($fh, $ioctl, $offset, $length) = @_; + + my $range = pack('QQ', $offset, $length); + ioctl($fh, $ioctl, $range) or die "$!\n"; +} + my sub free_lvm_volumes_locked { my ($class, $scfg, $storeid, $volnames) = @_; @@ -349,56 +362,122 @@ my sub free_lvm_volumes_locked { file_read_firstline("$sysdir/queue/write_zeroes_max_bytes") // 0; ($write_zeroes_max_bytes) = $write_zeroes_max_bytes =~ m/^(\d+)$/; #untaint + my $size = file_read_firstline("$sysdir/size") + or die "size from $sysdir cannot be read\n"; + ($size) = $size =~ m/^(\d+)$/; # untaint + $size *= 512; # sysfs size is in 512-byte sectors + + my $zeroout_variant = 'blkzeroout'; + my $throughput = undef; + if ($scfg->{saferemove_throughput}) { + # use abs as legacy cstream accepted negative values + $throughput = abs($scfg->{saferemove_throughput}); + my $rendered_throughput = render_bytes($throughput); + print "using saferemove throughput limit: $rendered_throughput/s\n"; + } + + # If the storage does not support write_zeroes fall back to writing zeroes manually using + # syswrite. Otherwise if the storage supports write_zeroes but stepsize is too big, + # reduce the stepsize to the maximum supported by the storage. + my $zeroes; if ($write_zeroes_max_bytes == 0) { - # If the storage does not support 'write zeroes', we fallback to cstream. - # wipe throughput up to 10MB/s by default; may be overwritten with saferemove_throughput - my $throughput = '-10485760'; - if ($scfg->{saferemove_throughput}) { - $throughput = $scfg->{saferemove_throughput}; + print "WRITE_ZEROES operation not supported," + . " falling back to syswrite to zero-out '$lvmpath'\n"; + $zeroout_variant = 'syswrite'; + $stepsize = 1024 * 1024; # 1 MiB + print "reduce stepsize to 1 MiB for syswrite\n"; + $zeroes = "\0" x $stepsize; + # limit throughput to 10MiB/s for syswrite, if throughput was not set + if (!defined($throughput)) { + # FIXME: MAJOR VERSION: increase to 100 MiB/s + $throughput = 10485760; + print "using default syswrite-saferemove throughput limit: 10 MiB/s\n"; } + } elsif ($stepsize > $write_zeroes_max_bytes) { + print "reduce stepsize to the maximum supported by the storage:" + . " $write_zeroes_max_bytes bytes\n"; + $stepsize = $write_zeroes_max_bytes; + } + open(my $fh, '+<', $lvmpath) or die "can't open '$lvmpath' - $!\n"; - my $cmd = [ - '/usr/bin/cstream', - '-i', - '/dev/zero', - '-o', - $lvmpath, - '-T', - '10', - '-v', - '1', - '-b', - '1048576', - '-t', - "$throughput", - ]; - # FIXME: handle cstream's expected ENOSPC failure explicitly and let other - # errors propagate. For now, preserve the old behavior where cstream can - # fail successfully with ENOSPC after writing until the device is full. - eval { - run_command( - $cmd, - errmsg => "zero out finished (note: 'No space left on device' is ok here)", - ); - }; - warn $@ if $@; - } else { - # If the storage supports write_zeroes but stepsize is too big, reduce the stepsize to - # the maximum supported by the storage. - if ($write_zeroes_max_bytes > 0 && $stepsize > $write_zeroes_max_bytes) { - print "reduce stepsize to the maximum supported by the storage:" - . " $write_zeroes_max_bytes bytes\n"; + # eval block, so filehandle is closed even if something fails below + eval { + my $start = clock_gettime(CLOCK_MONOTONIC); + my $written_total = 0; + my $lastprint = -1; + my $written; - $stepsize = $write_zeroes_max_bytes; - } + for (my $offset = 0; $offset < $size; $offset += $written) { - my $cmd = ['blkdiscard', $lvmpath, '-v', '--zeroout', '--step', "${stepsize}"]; - run_command($cmd); + if ($offset + $stepsize > $size) { + $stepsize = $size - $offset; + } + + if ($zeroout_variant eq 'blkzeroout') { + eval { blockdev_ioctl_range($fh, BLKZEROOUT, $offset, $stepsize); }; + if (my $err = $@) { + die "blkzeroout for $stepsize bytes at offset $offset failed: $err"; + } + $written = $stepsize; + } elsif ($zeroout_variant eq 'syswrite') { + + # allow retrying once if syswrite writes zero bytes + $written = syswrite($fh, $zeroes, $stepsize, 0); + if (!defined($written)) { + die "syswrite failed: $!\n"; + } elsif ($written == 0) { + warn "syswrite wrote 0 bytes, retrying"; + } + + while ($written < $stepsize) { + my $remaining = $stepsize - $written; + my $retried_write = syswrite($fh, $zeroes, $remaining, $written); + if (!defined($retried_write)) { + die "syswrite failed: $!\n"; + } elsif ($retried_write == 0) { + die "syswrite failed: wrote 0 bytes"; + } + $written += $retried_write; + } + + } + $written_total += $written; + + my $curr_time = clock_gettime(CLOCK_MONOTONIC); + if (($curr_time - $lastprint) >= 3) { + my $percent_finished = 100 * $written_total / $size; + my $curr_seconds = $curr_time - $start; + + printf( + "zeroed out %s of %s (%.2f%%) using %s in %s\n", + render_bytes($written_total), + render_bytes($size), + $percent_finished, + $zeroout_variant, + render_duration($curr_seconds), + ); + $lastprint = $curr_time; + } + + if (defined($throughput)) { + my $expected_elapsed = $written_total / $throughput; + my $actual_elapsed = $curr_time - $start; + my $delay = $expected_elapsed - $actual_elapsed; + if ($delay > 0) { + Time::HiRes::sleep($delay); + } + } + } + }; + # close filehandle before throwing an error + my $err = $@; + close($fh); + if ($err) { + die "$err"; } }; - # we need to zero out LVM data for security reasons - # and to allow thin provisioning + # we need to zero out LVM data for security reasons and to allow thin provisioning my $zero_out_worker = sub { my $total_cleanup_errors = 0; @@ -507,7 +586,7 @@ sub properties { type => 'integer', }, saferemove_throughput => { - description => "Wipe throughput (cstream -t parameter value).", + description => "Wipe throughput in bytes.", type => 'string', }, tagged_only => { -- 2.47.3