Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save XanClic/54d9bbd0f06d76444bac59df1e798500 to your computer and use it in GitHub Desktop.
Save XanClic/54d9bbd0f06d76444bac59df1e798500 to your computer and use it in GitHub Desktop.
From 0d158423eb3a4713d9a3b3ec07869956d20da752 Mon Sep 17 00:00:00 2001
From: Jean-Louis Dupond <jean-louis@dupond.be>
Date: Mon, 15 May 2023 09:36:44 +0200
Subject: [PATCH] qcow2: add discard-no-unref option
When we for example have a sparse qcow2 image and discard: unmap is enabled,
there can be a lot of fragmentation in the image after some time. Surely on VM's
that do a lot of writes/deletes.
This causes the qcow2 image to grow even over 110% of its virtual size,
because the free gaps in the image get to small to allocate new
continuous clusters. So it allocates new space as the end of the image.
Disabling discard is not an option, as discard is needed to keep the
incremental backup size as low as possible. Without discard, the
incremental backups would become large, as qemu thinks it's just dirty
blocks but it doesn't know the blocks are empty/useless.
So we need to avoid fragmentation but also 'empty' the useless blocks in
the image to have a small incremental backup.
Next to that we also want to send the discards futher down the stack, so
the underlying blocks are still discarded.
Therefor we introduce a new qcow2 option "discard-no-unref". When
setting this option to true (defaults to false), the discard requests
will still be executed, but it will keep the offset of the cluster. And
it will also pass the discard request further down the stack (if
discard:unmap is enabled).
This will avoid fragmentation and for example on a fully preallocated
qcow2 image, this will make sure the image is perfectly continuous.
Fixes: https://gitlab.com/qemu-project/qemu/-/issues/1621
Signed-off-by: Jean-Louis Dupond <jean-louis@dupond.be>
Signed-off-by: Hanna Czenczek <hreitz@redhat.com>
---
qapi/block-core.json | 4 ++++
block/qcow2.h | 3 +++
block/qcow2-cluster.c | 32 ++++++++++++++++++++++++++++----
block/qcow2.c | 12 ++++++++++++
qemu-options.hx | 6 ++++++
5 files changed, 53 insertions(+), 4 deletions(-)
diff --git a/qapi/block-core.json b/qapi/block-core.json
index 98d9116dae..552c499793 100644
--- a/qapi/block-core.json
+++ b/qapi/block-core.json
@@ -3478,6 +3478,9 @@
# @pass-discard-other: whether discard requests for the data source
# should be issued on other occasions where a cluster gets freed
#
+# @discard-no-unref: don't dereference the cluster when we do a discard
+# this to avoid fragmentation of the qcow2 image (since 8.1)
+#
# @overlap-check: which overlap checks to perform for writes to the
# image, defaults to 'cached' (since 2.2)
#
@@ -3516,6 +3519,7 @@
'*pass-discard-request': 'bool',
'*pass-discard-snapshot': 'bool',
'*pass-discard-other': 'bool',
+ '*discard-no-unref': 'bool',
'*overlap-check': 'Qcow2OverlapChecks',
'*cache-size': 'int',
'*l2-cache-size': 'int',
diff --git a/block/qcow2.h b/block/qcow2.h
index 4f67eb912a..ea9adb5706 100644
--- a/block/qcow2.h
+++ b/block/qcow2.h
@@ -133,6 +133,7 @@
#define QCOW2_OPT_DISCARD_REQUEST "pass-discard-request"
#define QCOW2_OPT_DISCARD_SNAPSHOT "pass-discard-snapshot"
#define QCOW2_OPT_DISCARD_OTHER "pass-discard-other"
+#define QCOW2_OPT_DISCARD_NO_UNREF "discard-no-unref"
#define QCOW2_OPT_OVERLAP "overlap-check"
#define QCOW2_OPT_OVERLAP_TEMPLATE "overlap-check.template"
#define QCOW2_OPT_OVERLAP_MAIN_HEADER "overlap-check.main-header"
@@ -385,6 +386,8 @@ typedef struct BDRVQcow2State {
bool discard_passthrough[QCOW2_DISCARD_MAX];
+ bool discard_no_unref;
+
int overlap_check; /* bitmask of Qcow2MetadataOverlap values */
bool signaled_corruption;
diff --git a/block/qcow2-cluster.c b/block/qcow2-cluster.c
index 39cda7f907..e6776ff1f4 100644
--- a/block/qcow2-cluster.c
+++ b/block/qcow2-cluster.c
@@ -1925,6 +1925,9 @@ static int discard_in_l2_slice(BlockDriverState *bs, uint64_t offset,
uint64_t new_l2_bitmap = old_l2_bitmap;
QCow2ClusterType cluster_type =
qcow2_get_cluster_type(bs, old_l2_entry);
+ bool keep_reference = (cluster_type != QCOW2_CLUSTER_COMPRESSED) &&
+ (s->discard_no_unref &&
+ type == QCOW2_DISCARD_REQUEST);
/*
* If full_discard is true, the cluster should not read back as zeroes,
@@ -1943,10 +1946,22 @@ static int discard_in_l2_slice(BlockDriverState *bs, uint64_t offset,
new_l2_entry = new_l2_bitmap = 0;
} else if (bs->backing || qcow2_cluster_is_allocated(cluster_type)) {
if (has_subclusters(s)) {
- new_l2_entry = 0;
+ if (keep_reference) {
+ new_l2_entry = old_l2_entry;
+ } else {
+ new_l2_entry = 0;
+ }
new_l2_bitmap = QCOW_L2_BITMAP_ALL_ZEROES;
} else {
- new_l2_entry = s->qcow_version >= 3 ? QCOW_OFLAG_ZERO : 0;
+ if (s->qcow_version >= 3) {
+ if (keep_reference) {
+ new_l2_entry |= QCOW_OFLAG_ZERO;
+ } else {
+ new_l2_entry = QCOW_OFLAG_ZERO;
+ }
+ } else {
+ new_l2_entry = 0;
+ }
}
}
@@ -1960,8 +1975,17 @@ static int discard_in_l2_slice(BlockDriverState *bs, uint64_t offset,
if (has_subclusters(s)) {
set_l2_bitmap(s, l2_slice, l2_index + i, new_l2_bitmap);
}
- /* Then decrease the refcount */
- qcow2_free_any_cluster(bs, old_l2_entry, type);
+
+ if (!keep_reference) {
+ /* Then decrease the refcount */
+ qcow2_free_any_cluster(bs, old_l2_entry, type);
+ } else if (s->discard_passthrough[type] &&
+ (cluster_type == QCOW2_CLUSTER_NORMAL ||
+ cluster_type == QCOW2_CLUSTER_ZERO_ALLOC)) {
+ /* If we keep the reference, pass on the discard still */
+ bdrv_pdiscard(s->data_file, new_l2_entry & L2E_OFFSET_MASK,
+ s->cluster_size);
+ }
}
qcow2_cache_put(s->l2_table_cache, (void **) &l2_slice);
diff --git a/block/qcow2.c b/block/qcow2.c
index 7f3948360d..ca0113e511 100644
--- a/block/qcow2.c
+++ b/block/qcow2.c
@@ -682,6 +682,7 @@ static const char *const mutable_opts[] = {
QCOW2_OPT_DISCARD_REQUEST,
QCOW2_OPT_DISCARD_SNAPSHOT,
QCOW2_OPT_DISCARD_OTHER,
+ QCOW2_OPT_DISCARD_NO_UNREF,
QCOW2_OPT_OVERLAP,
QCOW2_OPT_OVERLAP_TEMPLATE,
QCOW2_OPT_OVERLAP_MAIN_HEADER,
@@ -726,6 +727,11 @@ static QemuOptsList qcow2_runtime_opts = {
.type = QEMU_OPT_BOOL,
.help = "Generate discard requests when other clusters are freed",
},
+ {
+ .name = QCOW2_OPT_DISCARD_NO_UNREF,
+ .type = QEMU_OPT_BOOL,
+ .help = "Do not dereference discarded clusters",
+ },
{
.name = QCOW2_OPT_OVERLAP,
.type = QEMU_OPT_STRING,
@@ -969,6 +975,7 @@ typedef struct Qcow2ReopenState {
bool use_lazy_refcounts;
int overlap_check;
bool discard_passthrough[QCOW2_DISCARD_MAX];
+ bool discard_no_unref;
uint64_t cache_clean_interval;
QCryptoBlockOpenOptions *crypto_opts; /* Disk encryption runtime options */
} Qcow2ReopenState;
@@ -1140,6 +1147,9 @@ static int qcow2_update_options_prepare(BlockDriverState *bs,
r->discard_passthrough[QCOW2_DISCARD_OTHER] =
qemu_opt_get_bool(opts, QCOW2_OPT_DISCARD_OTHER, false);
+ r->discard_no_unref = qemu_opt_get_bool(opts, QCOW2_OPT_DISCARD_NO_UNREF,
+ false);
+
switch (s->crypt_method_header) {
case QCOW_CRYPT_NONE:
if (encryptfmt) {
@@ -1220,6 +1230,8 @@ static void qcow2_update_options_commit(BlockDriverState *bs,
s->discard_passthrough[i] = r->discard_passthrough[i];
}
+ s->discard_no_unref = r->discard_no_unref;
+
if (s->cache_clean_interval != r->cache_clean_interval) {
cache_clean_timer_del(bs);
s->cache_clean_interval = r->cache_clean_interval;
diff --git a/qemu-options.hx b/qemu-options.hx
index b37eb9662b..2f5e5119e6 100644
--- a/qemu-options.hx
+++ b/qemu-options.hx
@@ -1431,6 +1431,12 @@ SRST
issued on other occasions where a cluster gets freed
(on/off; default: off)
+ ``discard-no-unref``
+ When enabled, a discard in the guest does not cause the
+ cluster inside the qcow2 image to be dereferenced. This
+ was added to avoid qcow2 fragmentation whithin the image.
+ (on/off; default: off)
+
``overlap-check``
Which overlap checks to perform for writes to the image
(none/constant/cached/all; default: cached). For details or
--
2.40.1
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment