From: Dominik Csapak <d.csapak@proxmox.com>
To: Erik Fastermann <e.fastermann@proxmox.com>, pve-devel@lists.proxmox.com
Subject: Re: [PATCH qemu-server 1/3] partially fix #1989: disk: add qcow2 cache options
Date: Thu, 16 Jul 2026 10:25:13 +0200 [thread overview]
Message-ID: <42f032f0-4be2-4bc7-8f60-494d11a04b5f@proxmox.com> (raw)
In-Reply-To: <20260605153512.265703-2-e.fastermann@proxmox.com>
a few comments inline
personally i would have liked the size=0 -> auto approach more than a
separate option, but i guess this is personal preference...
On 6/5/26 5:35 PM, Erik Fastermann wrote:
> Add multiple options to configure the qcow2 L2/refcount cache. This
> can provide significant performance gains in some cases.
>
> For a detailed explanation of the options see the QEMU docs [0].
> Additionally the cache size can be configured based on the size of the
> disk image automatically.
>
> Both blockdev and the older drive commandline are supported, which makes
> the feature accessible to all QEMU machine versions supported by PVE.
> Options which only apply to disk creation (cluster_size, refcount_bits)
> are not considered in this patch.
>
> [0] https://gitlab.com/qemu-project/qemu/-/blob/master/docs/qcow2-cache.txt
>
> Signed-off-by: Erik Fastermann <e.fastermann@proxmox.com>
> ---
> src/PVE/QemuServer.pm | 71 ++++++++++++++++++-
> src/PVE/QemuServer/Blockdev.pm | 44 ++++++++++++
> src/PVE/QemuServer/Drive.pm | 122 ++++++++++++++++++++++++++++++++
> src/PVE/QemuServer/QemuImage.pm | 24 +++++++
> 4 files changed, 260 insertions(+), 1 deletion(-)
>
> diff --git a/src/PVE/QemuServer.pm b/src/PVE/QemuServer.pm
> index 55e9f520..211927db 100644
> --- a/src/PVE/QemuServer.pm
> +++ b/src/PVE/QemuServer.pm
> @@ -78,6 +78,7 @@ use PVE::QemuServer::Drive qw(
> parse_drive
> print_drive
> storage_allows_io_uring_default
> + qcow2_cache_size_by_disk_size
> );
> use PVE::QemuServer::DriveDevice qw(print_drivedevice_full scsihw_infos);
> use PVE::QemuServer::Machine;
> @@ -1294,6 +1295,49 @@ sub print_drive_commandline_full {
> $opts .= ",auto-remove=on";
> }
>
> + for my $qcow2_cache_option (
> + 'qcow2-cache-size',
> + 'qcow2-l2-cache-size',
> + 'qcow2-l2-cache-entry-size',
> + 'qcow2-refcount-cache-size',
> + 'qcow2-cache-clean-interval',
what about the auto-size option?
> + ) {
> + if (defined($drive->{$qcow2_cache_option}) && $format ne 'qcow2') {
> + log_warn("$drive_id: $qcow2_cache_option requires disk format qcow2");
> + }
> + }
nit: this could avoid some checks by pulling the format comparison out:
if ($format ne 'qcow2') {
# log warning loop here
} else {
# assignments here
}
> +
> + if ($format eq 'qcow2') {
> + if (
> + defined($drive->{'qcow2-cache-size-based-on-disk'})
> + && $drive->{'qcow2-cache-size-based-on-disk'}
> + ) {
> + my $cache_size = qcow2_cache_size_by_disk_size($storecfg, $drive);
> + $opts .= ",cache-size=" . int($cache_size);
> + }
> +
> + if (defined($drive->{'qcow2-cache-size'})) {
> + $opts .= ",cache-size=" . int($drive->{'qcow2-cache-size'}) * 1024 * 1024;
> + }
> +
> + if (defined($drive->{'qcow2-l2-cache-size'})) {
> + $opts .= ",l2-cache-size=" . int($drive->{'qcow2-l2-cache-size'}) * 1024 * 1024;
> + }
> +
> + if (defined($drive->{'qcow2-l2-cache-entry-size'})) {
> + $opts .= ",l2-cache-entry-size=" . int($drive->{'qcow2-l2-cache-entry-size'});
> + }
> +
> + if (defined($drive->{'qcow2-refcount-cache-size'})) {
> + $opts .=
> + ",refcount-cache-size=" . int($drive->{'qcow2-refcount-cache-size'}) * 1024 * 1024;
> + }
> +
> + if (defined($drive->{'qcow2-cache-clean-interval'})) {
> + $opts .= ",cache-clean-interval=" . int($drive->{'qcow2-cache-clean-interval'});
> + }
> + }
> +
> # my $file_param = $live_restore_name ? "file.file.filename" : "file";
> my $file_param = "file";
> if ($live_restore_name) {
> @@ -5217,6 +5266,26 @@ sub vmconfig_update_disk {
> || safe_string_ne($drive->{ssd}, $old_drive->{ssd})
> || safe_string_ne($drive->{vendor}, $old_drive->{vendor})
> || safe_string_ne($drive->{ro}, $old_drive->{ro})
> + || safe_string_ne(
> + $drive->{'qcow2-cache-size'},
> + $old_drive->{'qcow2-cache-size'},
> + )
> + || safe_num_ne(
> + $drive->{'qcow2-l2-cache-size'},
> + $old_drive->{'qcow2-l2-cache-size'},
> + )
> + || safe_num_ne(
> + $drive->{'qcow2-l2-cache-entry-size'},
> + $old_drive->{'qcow2-l2-cache-entry-size'},
> + )
> + || safe_num_ne(
> + $drive->{'qcow2-refcount-cache-size'},
> + $old_drive->{'qcow2-refcount-cache-size'},
> + )
> + || safe_num_ne(
> + $drive->{'qcow2-cache-clean-interval'},
> + $old_drive->{'qcow2-cache-clean-interval'},
> + )
what about the 'cache-size-based-on-disk' option here?
even if it's not necessary to check, some kind of comment would
be good to have so we know it was not accidentally missed
> ) {
> die "skip\n";
> }
> diff --git a/src/PVE/QemuServer/Blockdev.pm b/src/PVE/QemuServer/Blockdev.pm
> index 101c747c..0dd5067d 100644
> --- a/src/PVE/QemuServer/Blockdev.pm
> +++ b/src/PVE/QemuServer/Blockdev.pm
> @@ -9,6 +9,7 @@ use File::stat;
> use JSON;
>
> use PVE::JSONSchema qw(json_bool);
> +use PVE::RESTEnvironment qw(log_warn);
> use PVE::Storage;
>
> use PVE::QemuServer::Drive qw(drive_is_cdrom);
> @@ -403,6 +404,49 @@ sub generate_format_blockdev {
> $blockdev->{'discard-no-unref'} = JSON::true if $format eq 'qcow2';
> }
>
> + for my $qcow2_cache_option (
> + 'qcow2-cache-size',
> + 'qcow2-l2-cache-size',
> + 'qcow2-l2-cache-entry-size',
> + 'qcow2-refcount-cache-size',
> + 'qcow2-cache-clean-interval',
> + ) {
> + if (defined($drive->{$qcow2_cache_option}) && $format ne 'qcow2') {
> + log_warn("$drive_id: $qcow2_cache_option requires disk format qcow2");
> + }
> + }
> +
> + if ($format eq 'qcow2') {
> + if (
> + defined($drive->{'qcow2-cache-size-based-on-disk'})
> + && $drive->{'qcow2-cache-size-based-on-disk'}
> + ) {
> + my $cache_size = qcow2_cache_size_by_disk_size($storecfg, $drive);
> + $blockdev->{'cache-size'} = int($cache_size);
> + }
> +
> + if (defined($drive->{'qcow2-cache-size'})) {
> + $blockdev->{'cache-size'} = int($drive->{'qcow2-cache-size'}) * 1024 * 1024;
> + }
> +
> + if (defined($drive->{'qcow2-l2-cache-size'})) {
> + $blockdev->{'l2-cache-size'} = int($drive->{'qcow2-l2-cache-size'}) * 1024 * 1024;
> + }
> +
> + if (defined($drive->{'qcow2-l2-cache-entry-size'})) {
> + $blockdev->{'l2-cache-entry-size'} = int($drive->{'qcow2-l2-cache-entry-size'});
> + }
> +
> + if (defined($drive->{'qcow2-refcount-cache-size'})) {
> + $blockdev->{'refcount-cache-size'} =
> + int($drive->{'qcow2-refcount-cache-size'}) * 1024 * 1024;
> + }
> +
> + if (defined($drive->{'qcow2-cache-clean-interval'})) {
> + $blockdev->{'cache-clean-interval'} = int($drive->{'qcow2-cache-clean-interval'});
> + }
> + }
> +
these checks/calculations are the same as above,
just the assignment is different.
Maybe we can factor that out and reuse it?
> return $blockdev;
> }
>
> diff --git a/src/PVE/QemuServer/Drive.pm b/src/PVE/QemuServer/Drive.pm
> index b80b7dbb..e123947e 100644
> --- a/src/PVE/QemuServer/Drive.pm
> +++ b/src/PVE/QemuServer/Drive.pm
> @@ -7,6 +7,7 @@ use Storable qw(dclone);
>
> use IO::File;
> use List::Util qw(first);
> +use POSIX;
>
> use PVE::RESTEnvironment qw(log_warn);
> use PVE::Storage;
> @@ -26,6 +27,7 @@ our @EXPORT_OK = qw(
> parse_drive
> print_drive
> storage_allows_io_uring_default
> + qcow2_cache_size_by_disk_size
> );
>
> my $DROPPED_PROPERTIES = ['cyls', 'heads', 'secs', 'trans'];
> @@ -256,6 +258,46 @@ my %drivedesc_base = (
> optional => 1,
> default => 0,
> },
> + 'qcow2-cache-size' => {
> + type => 'integer',
> + minimum => 1,
> + maximum => 10 * 1024,
> + description => 'Cache size for qcow2 disks in MiB',
> + optional => 1,
i think it would be good to document the default qemu uses
(if that does not change too often), so a user can get a hint
what that is without having to read qemu documentation
> + },
> + 'qcow2-cache-size-based-on-disk' => {
> + type => 'boolean',
> + description =>
> + 'Automatically pick a qcow2 cache size based on the configured disk size',
> + optional => 1,
> + },
> + 'qcow2-l2-cache-size' => {
> + type => 'integer',
> + minimum => 1,
> + maximum => 10 * 1024,
> + description => 'L2 cache size for qcow2 disks in MiB',
> + optional => 1,
> + },
> + 'qcow2-l2-cache-entry-size' => {
> + type => 'integer',
> + minimum => 512,
> + maximum => 2 * 1024 * 1024,
> + description => 'L2 cache entry size for qcow2 disks in bytes',
> + optional => 1,
> + },
> + 'qcow2-refcount-cache-size' => {
> + type => 'integer',
> + minimum => 1,
> + maximum => 10 * 1024,
> + description => 'Refcount cache size for qcow2 disks in MiB',
> + optional => 1,
> + },
> + 'qcow2-cache-clean-interval' => {
> + type => 'integer',
> + minimum => 0,
> + description => 'Cache clean interval for qcow2 disks in seconds',
> + optional => 1,
> + },
> );
>
> my %iothread_fmt = (
> @@ -838,6 +880,52 @@ sub parse_drive {
> }
> }
>
> + my $cache_option_count =
> + defined($res->{'qcow2-cache-size'}) +
> + defined($res->{'qcow2-l2-cache-size'}) +
> + defined($res->{'qcow2-refcount-cache-size'});
> +
> + if ($cache_option_count > 2) {
> + warn "at most two of qcow2-cache-size, qcow2-l2-cache-size, qcow2-refcount-cache-size"
> + . " can be set simultaneously\n";
> + ++$error;
> + }
i guess this behavior (that cache-size is the overall limit of l2 and
refcount cache) should be documented on our side (e.g. api description)
> +
> + if (
> + defined($res->{'qcow2-l2-cache-entry-size'})
> + && !is_power_of_two($res->{'qcow2-l2-cache-entry-size'})
> + ) {
> + warn "qcow2-l2-cache-entry-size must be a power of two\n";
> + ++$error;
> + }
> +
same for this, e.g. an additional sentence in the description:
"(Must be a power of two)"
would be nice
> + if (
> + defined($res->{'qcow2-cache-size-based-on-disk'})
> + && $res->{'qcow2-cache-size-based-on-disk'}
> + ) {
> + if (
> + defined($res->{'qcow2-cache-size'})
> + || defined($res->{'qcow2-l2-cache-size'})
> + || defined($res->{'qcow2-refcount-cache-size'})
> + ) {
> + warn "qcow2-cache-size-based-on-disk is not compatible with cache-size,"
> + . " l2-cache-size or refcount-cache-size being set\n";
> + ++$error;
> + }
> + }
same with this
> +
> + if (defined($res->{'qcow2-cache-size'})) {
> + if (($res->{'qcow2-l2-cache-size'} // 0) >= $res->{'qcow2-cache-size'}) {
> + warn "qcow2-l2-cache-size is larger than or equal to qcow2-cache-size\n";
> + ++$error;
> + }
> +
> + if (($res->{'qcow2-refcount-cache-size'} // 0) >= $res->{'qcow2-cache-size'}) {
> + warn "qcow2-refcount-cache-size is larger than or equal to qcow2-cache-size\n";
> + ++$error;
> + }
> + }
> +
and this.
Basically the api description should give a good hint on what values to
use. As a user, it's always cumbersome to have to test which values
are allowed by experimenting. if the api description states the limits
and constraints, it's much better (IMO)."
> return if $error;
>
> return if $res->{mbps_rd} && $res->{mbps};
> @@ -857,6 +945,11 @@ sub parse_drive {
> return $res;
> }
>
> +sub is_power_of_two {
> + my ($n) = @_;
> + return $n > 0 && (($n & ($n - 1)) == 0);
> +}
> +
> sub print_drive {
> my ($drive, $with_alloc) = @_;
> my $skip = ['index', 'interface'];
> @@ -1179,4 +1272,33 @@ sub drive_qmp_peer {
> return drive_uses_qsd_fuse($storecfg, $drive) ? qsd_qmp_peer($vmid) : vm_qmp_peer($vmid);
> }
>
> +sub qcow2_cache_size_by_disk_size {
> + # The calculation is adapted from here:
> + # https://gitlab.com/qemu-project/qemu/-/blob/master/docs/qcow2-cache.txt#L97-98
> + # As this combines the refcount and L2 cache sizes, QEMU is free to
> + # choose a different value for each of them, which means the L2 cache
> + # is not necessarily 4 times bigger than the refcount cache with the
> + # default cluster_size, refcount_bits and no extended L2 entries.
> +
> + my ($storecfg, $drive) = @_;
> +
> + my $img_info = PVE::QemuServer::QemuImage::info($storecfg, $drive->{file});
> + my $cluster_size = $img_info->{'cluster-size'} // 65536;
> + my $refcount_bits = $img_info->{'format-specific'}->{data}->{'refcount-bits'} // 16;
> + my $has_extended_l2 = $img_info->{'format-specific'}->{data}->{'extended-l2'} // 0;
> +
> + my $l2_multiplier = $has_extended_l2 ? 16 : 8;
> + my $l2_cache_size = ($drive->{size} * $l2_multiplier) / $cluster_size;
> +
> + $l2_cache_size = 2 * $cluster_size if $l2_cache_size < (2 * $cluster_size);
> + $l2_cache_size = ceil($l2_cache_size / $cluster_size) * $cluster_size;
> +
> + my $refcount_cache_size = ($drive->{size} * $refcount_bits) / (8 * $cluster_size);
> +
> + $refcount_cache_size = 4 * $cluster_size if $refcount_cache_size < (4 * $cluster_size);
> + $refcount_cache_size = ceil($refcount_cache_size / $cluster_size) * $cluster_size;
> +
> + return $l2_cache_size + $refcount_cache_size;
> +}
> +
> 1;
> diff --git a/src/PVE/QemuServer/QemuImage.pm b/src/PVE/QemuServer/QemuImage.pm
> index 71be3abb..9c9db4e8 100644
> --- a/src/PVE/QemuServer/QemuImage.pm
> +++ b/src/PVE/QemuServer/QemuImage.pm
> @@ -15,6 +15,30 @@ use PVE::QemuServer::Blockdev;
> use PVE::QemuServer::Drive qw(checked_volume_format);
> use PVE::QemuServer::Helpers;
>
> +sub info {
> + my ($storecfg, $volid) = @_;
> +
> + my $disk_path = PVE::Storage::abs_filesystem_path($storecfg, $volid, 1);
> + my $cmd = ['/usr/bin/qemu-img', 'info', '--output=json', $disk_path];
> +
> + my $res = eval {
> + my $output = '';
> + PVE::Tools::run_command(
> + $cmd,
> + outfunc => sub {
> + my $line = shift;
> + $output .= $line;
> + },
> + );
> + decode_json($output);
> + };
> +
> + my $err = $@;
> + die "qemu-img info failed: $err" if $err;
> +
> + return $res;
> +}
> +
there is already `PVE::Storage::Common::qemu_img_info`, please reuse that.
> sub convert_iscsi_path {
> my ($path) = @_;
>
next prev parent reply other threads:[~2026-07-16 8:25 UTC|newest]
Thread overview: 7+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-06-05 15:35 [PATCH docs/manager/qemu-server 0/3] partially fix #1989: add QEMU qcow2 cache options Erik Fastermann
2026-06-05 15:35 ` [PATCH qemu-server 1/3] partially fix #1989: disk: add " Erik Fastermann
2026-07-16 8:25 ` Dominik Csapak [this message]
2026-06-05 15:35 ` [PATCH pve-manager 2/3] partially fix #1989: ui: qemu: disk: add qcow2 cache size config Erik Fastermann
2026-07-16 8:25 ` Dominik Csapak
2026-06-05 15:35 ` [PATCH pve-docs 3/3] partially fix #1989: qm: document " Erik Fastermann
2026-07-16 8:25 ` Dominik Csapak
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=42f032f0-4be2-4bc7-8f60-494d11a04b5f@proxmox.com \
--to=d.csapak@proxmox.com \
--cc=e.fastermann@proxmox.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