[PATCH libnbd v3 0/3] copy: Use preferred block size for copying
v2 was: https://listman.redhat.com/archives/libguestfs/2022-June/029321.html v3: - adds a test - force sparseness detection size to be at least as large as the target preferred size (otherwise you end up creating sub-requests which are smaller than the preferred size) Rich.
This will be used in a subsequent commit. At the moment the preferred block size for all sources / destinations is simply calculated and stored. --- copy/file-ops.c | 9 ++++++++- copy/main.c | 9 ++++++--- copy/nbd-ops.c | 9 +++++++++ copy/nbdcopy.h | 4 +++- copy/null-ops.c | 1 + copy/pipe-ops.c | 1 + 6 files changed, 28 insertions(+), 5 deletions(-) diff --git a/copy/file-ops.c b/copy/file-ops.c index ab3787545c..3b901bcdb8 100644 --- a/copy/file-ops.c +++ b/copy/file-ops.c @@ -241,7 +241,8 @@ seek_hole_supported (int fd) struct rw * file_create (const char *name, int fd, - off_t st_size, bool is_block, direction d) + off_t st_size, blksize_t st_blksize, + bool is_block, direction d) { struct rw_file *rwf = calloc (1, sizeof *rwf); if (rwf == NULL) { perror ("calloc"); exit (EXIT_FAILURE); } @@ -262,6 +263,11 @@ file_create (const char *name, int fd, perror ("lseek"); exit (EXIT_FAILURE); } + rwf->rw.preferred = 4096; +#ifdef BLKIOOPT + if (ioctl (fd, BLKIOOPT, &rwf->rw.preferred)) + fprintf (stderr, "warning: cannot get optimal I/O size: %s: %m", name); +#endif rwf->seek_hole_supported = seek_hole_supported (fd); rwf->sector_size = 4096; #ifdef BLKSSZGET @@ -282,6 +288,7 @@ file_create (const char *name, int fd, else { /* Regular file. */ rwf->rw.size = st_size; + rwf->rw.preferred = st_blksize; rwf->seek_hole_supported = seek_hole_supported (fd); /* Possible efficient zero methods for regular file. */ #ifdef FALLOC_FL_PUNCH_HOLE diff --git a/copy/main.c b/copy/main.c index cc379e98bc..9b551de664 100644 --- a/copy/main.c +++ b/copy/main.c @@ -513,7 +513,9 @@ open_local (const char *filename, direction d) exit (EXIT_FAILURE); } if (S_ISBLK (stat.st_mode) || S_ISREG (stat.st_mode)) - return file_create (filename, fd, stat.st_size, S_ISBLK (stat.st_mode), d); + return file_create (filename, fd, + stat.st_size, stat.st_blksize, S_ISBLK (stat.st_mode), + d); else { /* Probably stdin/stdout, a pipe or a socket. */ synchronous = true; /* Force synchronous mode for pipes. */ @@ -528,8 +530,9 @@ print_rw (struct rw *rw, const char *prefix, FILE *fp) char buf[HUMAN_SIZE_LONGEST]; fprintf (fp, "%s: %s \"%s\"\n", prefix, rw->ops->ops_name, rw->name); - fprintf (fp, "%s: size=%" PRIi64 " (%s)\n", - prefix, rw->size, human_size (buf, rw->size, NULL)); + fprintf (fp, "%s: size=%" PRIi64 " (%s), preferred block size=%" PRIu64 "\n", + prefix, rw->size, human_size (buf, rw->size, NULL), + rw->preferred); } /* Default implementation of rw->ops->get_extents for backends which diff --git a/copy/nbd-ops.c b/copy/nbd-ops.c index 3bc26ba613..6c5d59c578 100644 --- a/copy/nbd-ops.c +++ b/copy/nbd-ops.c @@ -113,11 +113,20 @@ open_one_nbd_handle (struct rw_nbd *rwn) */ if (rwn->handles.len == 0) { rwn->can_zero = nbd_can_zero (nbd) > 0; + rwn->rw.size = nbd_get_size (nbd); if (rwn->rw.size == -1) { fprintf (stderr, "%s: %s: %s\n", prog, rwn->rw.name, nbd_get_error ()); exit (EXIT_FAILURE); } + + rwn->rw.preferred = nbd_get_block_size (nbd, LIBNBD_SIZE_PREFERRED); + if (rwn->rw.preferred == -1) { + fprintf (stderr, "%s: %s: %s\n", prog, rwn->rw.name, nbd_get_error ()); + exit (EXIT_FAILURE); + } + if (rwn->rw.preferred == 0) + rwn->rw.preferred = 4096; } if (handles_append (&rwn->handles, nbd) == -1) { diff --git a/copy/nbdcopy.h b/copy/nbdcopy.h index 19797dfd66..9b02ddf270 100644 --- a/copy/nbdcopy.h +++ b/copy/nbdcopy.h @@ -43,6 +43,7 @@ struct rw { struct rw_ops *ops; /* Operations. */ const char *name; /* Printable name, for error messages etc. */ int64_t size; /* May be -1 for streams. */ + uint64_t preferred; /* Preferred block size. */ /* Followed by private data for the particular subtype. */ }; @@ -53,7 +54,8 @@ typedef enum { READING, WRITING } direction; /* Create subtypes. */ extern struct rw *file_create (const char *name, int fd, - off_t st_size, bool is_block, direction d); + off_t st_size, blksize_t st_blksize, + bool is_block, direction d); extern struct rw *nbd_rw_create_uri (const char *name, const char *uri, direction d); extern struct rw *nbd_rw_create_subprocess (const char **argv, size_t argc, diff --git a/copy/null-ops.c b/copy/null-ops.c index 1218a623e2..854e291613 100644 --- a/copy/null-ops.c +++ b/copy/null-ops.c @@ -45,6 +45,7 @@ null_create (const char *name) rw->rw.ops = &null_ops; rw->rw.name = name; rw->rw.size = INT64_MAX; + rw->rw.preferred = 4 * 1024 * 1024; return &rw->rw; } diff --git a/copy/pipe-ops.c b/copy/pipe-ops.c index 3c8b6c2b39..3815f824fc 100644 --- a/copy/pipe-ops.c +++ b/copy/pipe-ops.c @@ -43,6 +43,7 @@ pipe_create (const char *name, int fd) rwp->rw.ops = &pipe_ops; rwp->rw.name = name; rwp->rw.size = -1; + rwp->rw.preferred = 4096; rwp->fd = fd; return &rwp->rw; } -- 2.37.0.rc2
You're not supposed to read or write NBD servers at a granularity less than the advertised minimum block size. nbdcopy has ignored this requirement, and this is usually fine because the NBD servers we care about support 512-byte sector granularity, and never advertise sizes / extents less granular than sectors (even if it's a bit suboptimal in a few cases). However there is one new case where we do care: When writing to a compressed qcow2 file, qemu advertises a minimum and preferred block size of 64K, and it really means it. You cannot write blocks smaller than this because of the way qcow2 compression is implemented. This commit attempts to do the least work possible to fix this. The previous multi-thread-copying loop was driven by the extent map received from the source. I have modified the loop so that it iterates over request_size blocks. request_size is set from the command line (--request-size) but will be adjusted upwards if either the source or destination preferred block size is larger. So this will always copy blocks which are at least the preferred block size (except for the very last block of the disk). While copying these blocks we consult the source extent map. If it contains only zero regions covering the whole block (only_zeroes function) then we can skip straight to zeroing the target (fill_dst_range_with_zeroes), else we do read + write as before. I only modified the multi-thread-copying loop, not the synchronous loop. That should be updated in the same way later. One side effect of this change is it always makes larger requests, even for regions we know are sparse. This is clear in the copy-sparse.sh and copy-sparse-allocated.sh tests which were previously driven by the 32K sparse map granularity of the source. Without changing these tests, they would make make 256K reads & writes (and also read from areas of the disk even though we know they are sparse). I adjusted these tests to use --request-size=32768 to force the existing behaviour. Note this doesn't attempt to limit the maximum block size when reading or writing. That is for future work. This is a partial fix for https://bugzilla.redhat.com/2047660. Further changes will be required in virt-v2v. Link: https://lists.gnu.org/archive/html/qemu-block/2022-01/threads.html#00729 Link: https://bugzilla.redhat.com/show_bug.cgi?id=2047660 --- TODO | 4 +- copy/Makefile.am | 6 +- copy/copy-file-to-qcow2-compressed.sh | 64 ++++++++++++ copy/copy-sparse-allocated.sh | 4 +- copy/copy-sparse.sh | 7 +- copy/main.c | 13 +++ copy/multi-thread-copying.c | 143 +++++++++++++++++++------- copy/nbdcopy.pod | 5 +- 8 files changed, 197 insertions(+), 49 deletions(-) diff --git a/TODO b/TODO index 7c9c15e245..bc38d7045e 100644 --- a/TODO +++ b/TODO @@ -28,7 +28,9 @@ Performance: Chart it over various buffer sizes and threads, as that Examine other fuzzers: https://gitlab.com/akihe/radamsa nbdcopy: - - Minimum/preferred/maximum block size. + - Enforce maximum block size. + - Synchronous loop should be adjusted to take into account + the NBD preferred block size, as was done for multi-thread loop. - Benchmark. - Better page cache usage, see nbdkit-file-plugin options fadvise=sequential cache=none. diff --git a/copy/Makefile.am b/copy/Makefile.am index e729f86a2b..25f75c5706 100644 --- a/copy/Makefile.am +++ b/copy/Makefile.am @@ -23,6 +23,7 @@ EXTRA_DIST = \ copy-file-to-nbd.sh \ copy-file-to-null.sh \ copy-file-to-qcow2.sh \ + copy-file-to-qcow2-compressed.sh \ copy-nbd-to-block.sh \ copy-nbd-to-file.sh \ copy-nbd-to-hexdump.sh \ @@ -142,7 +143,10 @@ TESTS += \ $(NULL) if HAVE_QEMU_NBD -TESTS += copy-file-to-qcow2.sh +TESTS += \ + copy-file-to-qcow2.sh \ + copy-file-to-qcow2-compressed.sh \ + $(NULL) endif if HAVE_GNUTLS diff --git a/copy/copy-file-to-qcow2-compressed.sh b/copy/copy-file-to-qcow2-compressed.sh new file mode 100755 index 0000000000..dfe4fa583c --- /dev/null +++ b/copy/copy-file-to-qcow2-compressed.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +# nbd client library in userspace +# Copyright (C) 2020-2022 Red Hat Inc. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +. ../tests/functions.sh + +set -e +set -x + +requires $QEMU_NBD --version +requires nbdkit --exit-with-parent --version +requires nbdkit sparse-random --dump-plugin +requires qemu-img --version +requires stat --version + +file1=copy-file-to-qcow2-compressed.file1 +file2=copy-file-to-qcow2-compressed.file2 +rm -f $file1 $file2 +cleanup_fn rm -f $file1 $file2 + +size=1G +seed=$RANDOM + +# Create a compressed qcow2 file1. +# +# sparse-random files should compress easily because by default each +# block uses repeated bytes. +qemu-img create -f qcow2 $file1 $size +nbdcopy -- [ nbdkit --exit-with-parent sparse-random $size seed=$seed ] \ + [ $QEMU_NBD --image-opts driver=compress,file.driver=qcow2,file.file.driver=file,file.file.filename=$file1 ] + +ls -l $file1 + +# Create an uncompressed qcow2 file2 with the same data. +qemu-img create -f qcow2 $file2 $size +nbdcopy -- [ nbdkit --exit-with-parent sparse-random $size seed=$seed ] \ + [ $QEMU_NBD --image-opts driver=qcow2,file.driver=file,file.filename=$file2 ] + +ls -l $file2 + +# file1 < file2 (shows the compression is having some effect). +size1="$( stat -c %s $file1 )" +size2="$( stat -c %s $file2 )" +if [ $size1 -ge $size2 ]; then + echo "$0: qcow2 compression did not make the file smaller" + exit 1 +fi + +# Logical content of the files should be identical. +qemu-img compare -f qcow2 $file1 -F qcow2 $file2 diff --git a/copy/copy-sparse-allocated.sh b/copy/copy-sparse-allocated.sh index 203c3b9850..465e347067 100755 --- a/copy/copy-sparse-allocated.sh +++ b/copy/copy-sparse-allocated.sh @@ -17,8 +17,6 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA # Adapted from copy-sparse.sh. -# -# This test depends on the nbdkit default sparse block size (32K). . ../tests/functions.sh @@ -33,7 +31,7 @@ requires nbdkit eval --version out=copy-sparse-allocated.out cleanup_fn rm -f $out -$VG nbdcopy --allocated -- \ +$VG nbdcopy --allocated --request-size=32768 -- \ [ nbdkit --exit-with-parent data data=' 1 @1073741823 1 diff --git a/copy/copy-sparse.sh b/copy/copy-sparse.sh index 1a6da868e9..7912a2120a 100755 --- a/copy/copy-sparse.sh +++ b/copy/copy-sparse.sh @@ -16,8 +16,6 @@ # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -# This test depends on the nbdkit default sparse block size (32K). - . ../tests/functions.sh set -e @@ -34,8 +32,9 @@ cleanup_fn rm -f $out # Copy from a sparse data disk to an nbdkit-eval-plugin instance which # is logging everything. This allows us to see exactly what nbdcopy # is writing, to ensure it is writing and zeroing the target as -# expected. -$VG nbdcopy -S 0 -- \ +# expected. Force request size to match nbdkit default sparse +# allocator block size (32K). +$VG nbdcopy -S 0 --request-size=32768 -- \ [ nbdkit --exit-with-parent data data=' 1 @1073741823 1 diff --git a/copy/main.c b/copy/main.c index 9b551de664..32e8f370bd 100644 --- a/copy/main.c +++ b/copy/main.c @@ -40,6 +40,7 @@ #include "ispowerof2.h" #include "human-size.h" +#include "minmax.h" #include "version.h" #include "nbdcopy.h" @@ -379,10 +380,22 @@ main (int argc, char *argv[]) if (threads < connections) connections = threads; + /* request_size must always be larger than preferred size of + * source or destination. + */ + request_size = MAX (request_size, src->preferred); + request_size = MAX (request_size, dst->preferred); + /* Adapt queue to size to request size if needed. */ if (request_size > queue_size) queue_size = request_size; + /* Sparse size (if using) must not be smaller than the destination + * preferred size, otherwise we end up creating too small requests. + */ + if (sparse_size) + sparse_size = MAX (sparse_size, dst->preferred); + /* Truncate the destination to the same size as the source. Only * has an effect on regular files. */ diff --git a/copy/multi-thread-copying.c b/copy/multi-thread-copying.c index 06cdb8eef6..7194f4fafd 100644 --- a/copy/multi-thread-copying.c +++ b/copy/multi-thread-copying.c @@ -166,6 +166,58 @@ decrease_queue_size (struct worker *worker, size_t len) worker->queue_size -= len; } +/* Using the extents map 'exts', check if the region + * [offset..offset+len-1] contains only zero extents. + * + * The invariant for '*i' is always an extent which starts before or + * equal to the current offset. + */ +static bool +only_zeroes (const extent_list exts, size_t *i, + uint64_t offset, unsigned len) +{ + size_t j; + + /* Invariant. */ + assert (0 <= *i && *i < exts.len); + assert (exts.ptr[*i].offset <= offset); + + /* Update the variant. Search for the last possible extent in the + * list which is <= offset. + */ + for (j = *i + 1; j < exts.len; ++j) { + if (exts.ptr[j].offset <= offset) + *i = j; + else + break; + } + + /* Check invariant again. */ + assert (0 <= *i && *i < exts.len); + assert (exts.ptr[*i].offset <= offset); + + /* Search forward, look for any non-zero extents covering the region. */ + for (j = *i; j < exts.len; ++j) { + uint64_t start, end; + + /* [start..end] of the current extent. */ + start = exts.ptr[j].offset; + end = exts.ptr[j].offset + exts.ptr[j].length; + + if (end <= offset) + continue; + + if (start >= offset + len) + break; + + /* Non-zero extent covering this region => test failed. */ + if (!exts.ptr[j].zero) + return false; + } + + return true; +} + /* There are 'threads' worker threads, each copying work ranges from * src to dst until there are no more work ranges. */ @@ -177,7 +229,10 @@ worker_thread (void *wp) extent_list exts = empty_vector; while (get_next_offset (&offset, &count)) { + struct command *command; size_t i; + bool is_zeroing = false; + uint64_t zeroing_start = 0; assert (0 < count && count <= THREAD_WORK_SIZE); if (extents) @@ -185,52 +240,64 @@ worker_thread (void *wp) else default_get_extents (src, w->index, offset, count, &exts); - for (i = 0; i < exts.len; ++i) { - struct command *command; - size_t len; + i = 0; // index into extents array + while (count) { + const size_t len = MIN (count, request_size); - if (exts.ptr[i].zero) { + if (only_zeroes (exts, &i, offset, len)) { /* The source is zero so we can proceed directly to skipping, - * fast zeroing, or writing zeroes at the destination. + * fast zeroing, or writing zeroes at the destination. Defer + * zeroing so we can send it as a single large command. */ - command = create_command (exts.ptr[i].offset, exts.ptr[i].length, - true, w); - fill_dst_range_with_zeroes (command); + if (!is_zeroing) { + is_zeroing = true; + zeroing_start = offset; + } } - else /* data */ { - /* As the extent might be larger than permitted for a single - * command, we may have to split this into multiple read - * requests. - */ - while (exts.ptr[i].length > 0) { - len = exts.ptr[i].length; - if (len > request_size) - len = request_size; - - command = create_command (exts.ptr[i].offset, len, - false, w); - - wait_for_request_slots (w); - - /* NOTE: Must increase the queue size after waiting. */ - increase_queue_size (w, len); - - /* Begin the asynch read operation. */ - src->ops->asynch_read (src, command, - (nbd_completion_callback) { - .callback = finished_read, - .user_data = command, - }); - - exts.ptr[i].offset += len; - exts.ptr[i].length -= len; + /* If we were in the middle of deferred zeroing, do it now. */ + if (is_zeroing) { + /* Note that offset-zeroing_start can never exceed + * THREAD_WORK_SIZE, so there is no danger of overflowing + * size_t. + */ + command = create_command (zeroing_start, offset-zeroing_start, + true, w); + fill_dst_range_with_zeroes (command); + is_zeroing = false; } + + /* Issue the asynchronous read command. */ + command = create_command (offset, len, false, w); + + wait_for_request_slots (w); + + /* NOTE: Must increase the queue size after waiting. */ + increase_queue_size (w, len); + + /* Begin the asynch read operation. */ + src->ops->asynch_read (src, command, + (nbd_completion_callback) { + .callback = finished_read, + .user_data = command, + }); } - offset += count; - count = 0; - } /* for extents */ + offset += len; + count -= len; + } /* while (count) */ + + /* If we were in the middle of deferred zeroing, do it now. */ + if (is_zeroing) { + /* Note that offset-zeroing_start can never exceed + * THREAD_WORK_SIZE, so there is no danger of overflowing + * size_t. + */ + command = create_command (zeroing_start, offset - zeroing_start, + true, w); + fill_dst_range_with_zeroes (command); + is_zeroing = false; + } } /* Wait for in flight NBD requests to finish. */ diff --git a/copy/nbdcopy.pod b/copy/nbdcopy.pod index 54e43915f3..7a4b57f1b5 100644 --- a/copy/nbdcopy.pod +++ b/copy/nbdcopy.pod @@ -182,8 +182,9 @@ Set the maximum number of requests in flight per NBD connection. =item B<--sparse=>N Detect all zero blocks of size N (bytes) and make them sparse on the -output. You can also turn off sparse detection using S<I<-S 0>>. -The default is 4096 bytes. +output. You can also turn off sparse detection using S<I<-S 0>>. The +default is 4096 bytes, or the destination preferred block size, +whichever is larger. =item B<--synchronous> -- 2.37.0.rc2
On 06/29/22 15:36, Richard W.M. Jones wrote:
You're not supposed to read or write NBD servers at a granularity less than the advertised minimum block size. nbdcopy has ignored this requirement, and this is usually fine because the NBD servers we care about support 512-byte sector granularity, and never advertise sizes / extents less granular than sectors (even if it's a bit suboptimal in a few cases).
However there is one new case where we do care: When writing to a compressed qcow2 file, qemu advertises a minimum and preferred block size of 64K, and it really means it. You cannot write blocks smaller than this because of the way qcow2 compression is implemented.
This commit attempts to do the least work possible to fix this.
The previous multi-thread-copying loop was driven by the extent map received from the source. I have modified the loop so that it iterates over request_size blocks. request_size is set from the command line (--request-size) but will be adjusted upwards if either the source or destination preferred block size is larger. So this will always copy blocks which are at least the preferred block size (except for the very last block of the disk).
While copying these blocks we consult the source extent map. If it contains only zero regions covering the whole block (only_zeroes function) then we can skip straight to zeroing the target (fill_dst_range_with_zeroes), else we do read + write as before.
I only modified the multi-thread-copying loop, not the synchronous loop. That should be updated in the same way later.
One side effect of this change is it always makes larger requests, even for regions we know are sparse. This is clear in the copy-sparse.sh and copy-sparse-allocated.sh tests which were previously driven by the 32K sparse map granularity of the source. Without changing these tests, they would make make 256K reads & writes (and also read from areas of the disk even though we know they are sparse). I adjusted these tests to use --request-size=32768 to force the existing behaviour.
Note this doesn't attempt to limit the maximum block size when reading or writing. That is for future work.
This is a partial fix for https://bugzilla.redhat.com/2047660. Further changes will be required in virt-v2v.
Link: https://lists.gnu.org/archive/html/qemu-block/2022-01/threads.html#00729 Link: https://bugzilla.redhat.com/show_bug.cgi?id=2047660 --- TODO | 4 +- copy/Makefile.am | 6 +- copy/copy-file-to-qcow2-compressed.sh | 64 ++++++++++++ copy/copy-sparse-allocated.sh | 4 +- copy/copy-sparse.sh | 7 +- copy/main.c | 13 +++ copy/multi-thread-copying.c | 143 +++++++++++++++++++------- copy/nbdcopy.pod | 5 +- 8 files changed, 197 insertions(+), 49 deletions(-)
I'm going to perform a very rudimentary review of this patch, as a good chunk of it is still over my head. Please bear with me...
diff --git a/TODO b/TODO index 7c9c15e245..bc38d7045e 100644 --- a/TODO +++ b/TODO @@ -28,7 +28,9 @@ Performance: Chart it over various buffer sizes and threads, as that Examine other fuzzers: https://gitlab.com/akihe/radamsa
nbdcopy: - - Minimum/preferred/maximum block size. + - Enforce maximum block size. + - Synchronous loop should be adjusted to take into account + the NBD preferred block size, as was done for multi-thread loop. - Benchmark. - Better page cache usage, see nbdkit-file-plugin options fadvise=sequential cache=none. diff --git a/copy/Makefile.am b/copy/Makefile.am index e729f86a2b..25f75c5706 100644 --- a/copy/Makefile.am +++ b/copy/Makefile.am @@ -23,6 +23,7 @@ EXTRA_DIST = \ copy-file-to-nbd.sh \ copy-file-to-null.sh \ copy-file-to-qcow2.sh \ + copy-file-to-qcow2-compressed.sh \ copy-nbd-to-block.sh \ copy-nbd-to-file.sh \ copy-nbd-to-hexdump.sh \ @@ -142,7 +143,10 @@ TESTS += \ $(NULL)
if HAVE_QEMU_NBD -TESTS += copy-file-to-qcow2.sh +TESTS += \ + copy-file-to-qcow2.sh \ + copy-file-to-qcow2-compressed.sh \ + $(NULL) endif
if HAVE_GNUTLS diff --git a/copy/copy-file-to-qcow2-compressed.sh b/copy/copy-file-to-qcow2-compressed.sh new file mode 100755 index 0000000000..dfe4fa583c --- /dev/null +++ b/copy/copy-file-to-qcow2-compressed.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +# nbd client library in userspace +# Copyright (C) 2020-2022 Red Hat Inc. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +. ../tests/functions.sh + +set -e +set -x + +requires $QEMU_NBD --version +requires nbdkit --exit-with-parent --version +requires nbdkit sparse-random --dump-plugin +requires qemu-img --version +requires stat --version + +file1=copy-file-to-qcow2-compressed.file1 +file2=copy-file-to-qcow2-compressed.file2 +rm -f $file1 $file2 +cleanup_fn rm -f $file1 $file2 + +size=1G +seed=$RANDOM + +# Create a compressed qcow2 file1. +# +# sparse-random files should compress easily because by default each +# block uses repeated bytes. +qemu-img create -f qcow2 $file1 $size +nbdcopy -- [ nbdkit --exit-with-parent sparse-random $size seed=$seed ] \ + [ $QEMU_NBD --image-opts driver=compress,file.driver=qcow2,file.file.driver=file,file.file.filename=$file1 ] + +ls -l $file1 + +# Create an uncompressed qcow2 file2 with the same data. +qemu-img create -f qcow2 $file2 $size +nbdcopy -- [ nbdkit --exit-with-parent sparse-random $size seed=$seed ] \ + [ $QEMU_NBD --image-opts driver=qcow2,file.driver=file,file.filename=$file2 ] + +ls -l $file2 + +# file1 < file2 (shows the compression is having some effect). +size1="$( stat -c %s $file1 )" +size2="$( stat -c %s $file2 )" +if [ $size1 -ge $size2 ]; then + echo "$0: qcow2 compression did not make the file smaller" + exit 1 +fi + +# Logical content of the files should be identical. +qemu-img compare -f qcow2 $file1 -F qcow2 $file2
(1) I suggest splitting the new test to a separate patch; to minimize the amount one has to review in one go.
diff --git a/copy/copy-sparse-allocated.sh b/copy/copy-sparse-allocated.sh index 203c3b9850..465e347067 100755 --- a/copy/copy-sparse-allocated.sh +++ b/copy/copy-sparse-allocated.sh @@ -17,8 +17,6 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
# Adapted from copy-sparse.sh. -# -# This test depends on the nbdkit default sparse block size (32K).
. ../tests/functions.sh
@@ -33,7 +31,7 @@ requires nbdkit eval --version out=copy-sparse-allocated.out cleanup_fn rm -f $out
-$VG nbdcopy --allocated -- \ +$VG nbdcopy --allocated --request-size=32768 -- \ [ nbdkit --exit-with-parent data data=' 1 @1073741823 1 diff --git a/copy/copy-sparse.sh b/copy/copy-sparse.sh index 1a6da868e9..7912a2120a 100755 --- a/copy/copy-sparse.sh +++ b/copy/copy-sparse.sh @@ -16,8 +16,6 @@ # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
-# This test depends on the nbdkit default sparse block size (32K). - . ../tests/functions.sh
set -e @@ -34,8 +32,9 @@ cleanup_fn rm -f $out # Copy from a sparse data disk to an nbdkit-eval-plugin instance which # is logging everything. This allows us to see exactly what nbdcopy # is writing, to ensure it is writing and zeroing the target as -# expected. -$VG nbdcopy -S 0 -- \ +# expected. Force request size to match nbdkit default sparse +# allocator block size (32K). +$VG nbdcopy -S 0 --request-size=32768 -- \ [ nbdkit --exit-with-parent data data=' 1 @1073741823 1 diff --git a/copy/main.c b/copy/main.c index 9b551de664..32e8f370bd 100644 --- a/copy/main.c +++ b/copy/main.c @@ -40,6 +40,7 @@
#include "ispowerof2.h" #include "human-size.h" +#include "minmax.h" #include "version.h" #include "nbdcopy.h"
@@ -379,10 +380,22 @@ main (int argc, char *argv[]) if (threads < connections) connections = threads;
+ /* request_size must always be larger than preferred size of + * source or destination. + */
(2) "larger than" is incorrect here, "at least as large as" is precise.
+ request_size = MAX (request_size, src->preferred); + request_size = MAX (request_size, dst->preferred); + /* Adapt queue to size to request size if needed. */ if (request_size > queue_size) queue_size = request_size;
+ /* Sparse size (if using) must not be smaller than the destination + * preferred size, otherwise we end up creating too small requests. + */ + if (sparse_size) + sparse_size = MAX (sparse_size, dst->preferred); +
(3) Not wrong, but I think this is better expressed as if (sparse_size > 0 && sparse_size < dst->preferred) sparse_size = dst->preferred;
/* Truncate the destination to the same size as the source. Only * has an effect on regular files. */ diff --git a/copy/multi-thread-copying.c b/copy/multi-thread-copying.c index 06cdb8eef6..7194f4fafd 100644 --- a/copy/multi-thread-copying.c +++ b/copy/multi-thread-copying.c @@ -166,6 +166,58 @@ decrease_queue_size (struct worker *worker, size_t len) worker->queue_size -= len; }
+/* Using the extents map 'exts', check if the region + * [offset..offset+len-1] contains only zero extents.
(4) I suggest replacing "contains" with "overlaps" or "intersects with".
+ * + * The invariant for '*i' is always an extent which starts before or + * equal to the current offset. + */ +static bool +only_zeroes (const extent_list exts, size_t *i,
(5) I think it's a bit wasteful (or at least strange) to pass in the entire vector structure; we certainly don't need the "cap" field. Other functions in libnbd seem to take pointers to vectors (which is what I'd expect).
+ uint64_t offset, unsigned len) +{ + size_t j; + + /* Invariant. */ + assert (0 <= *i && *i < exts.len);
(6) I suggest dropping the non-negativity check; size_t is an unsigned type.
+ assert (exts.ptr[*i].offset <= offset); + + /* Update the variant. Search for the last possible extent in the
(7) Typo: s/variant/invariant/
+ * list which is <= offset. + */ + for (j = *i + 1; j < exts.len; ++j) { + if (exts.ptr[j].offset <= offset) + *i = j; + else + break; + } + + /* Check invariant again. */ + assert (0 <= *i && *i < exts.len);
(8) Same comment as (6).
+ assert (exts.ptr[*i].offset <= offset);
(9) We can append another assertion: assert (*i == exts.len - 1 || exts.ptr[*i + 1].offset > offset); I.e., if *i is not the last extent, then the next extent starts strictly beyond "offset"
+ + /* Search forward, look for any non-zero extents covering the region. */
(10) I propose s/covering/overlapping/
+ for (j = *i; j < exts.len; ++j) { + uint64_t start, end; + + /* [start..end] of the current extent. */
(11) The notation is wrong, "end" is exclusive, not inclusive, therefore we should write [start..end) (note the closing paren).
+ start = exts.ptr[j].offset; + end = exts.ptr[j].offset + exts.ptr[j].length; + + if (end <= offset) + continue;
(12) This condition always evaluates to false. Previously, we looked up the "rightmost" extent that is not a successor of "offset", therefore the (exclusive) end of that extent will certainly be greater than "offset". Furthermore, whenever we move to the next extent, the (exclusive) end of that extent will be even greater. The *purpose* of the condition is valid however (see below), so I suggest adding assert (end > offset);
+ + if (start >= offset + len) + break; +
(13) I think we can insert a micro-optimization here: *i = j; Namely: - at this point we have: offset < end && start < offset + len which is precisely the definition of a non-empty intersection. That is, we're sure that the extent and the subject region overlap. - the next entry to this function will be with a subject region that starts (inclusively) where the current subject region ends (exclusively) -- next_request_offset := offset + len. That means the start of the next subject region will *either* fall again into this same extent, *or* into some extent *beyond* the current extent (j). But never into an extent with a smaller subscript than "j". Basically, consider the second subcondition above: start < offset + len If you substitute the above definition for next_request_offset, and expand "start", you get: exts.ptr[j].offset < next_request_offset and that means we need not perform the initial loop for extents that are "to the left" of extent "j". The loop at the start of the function is correct, but here we can save some work for its next execution (in the next invocation of this function).
+ /* Non-zero extent covering this region => test failed. */ + if (!exts.ptr[j].zero) + return false; + } + + return true; +} + /* There are 'threads' worker threads, each copying work ranges from * src to dst until there are no more work ranges. */ @@ -177,7 +229,10 @@ worker_thread (void *wp) extent_list exts = empty_vector;
while (get_next_offset (&offset, &count)) { + struct command *command; size_t i; + bool is_zeroing = false; + uint64_t zeroing_start = 0;
(14) I think we should not initialize "zeroing_start". It suggests that the initializer value is meaningful, but it is not.
assert (0 < count && count <= THREAD_WORK_SIZE); if (extents) @@ -185,52 +240,64 @@ worker_thread (void *wp) else default_get_extents (src, w->index, offset, count, &exts);
- for (i = 0; i < exts.len; ++i) { - struct command *command; - size_t len; + i = 0; // index into extents array
(15) We might as well rename "i" to something more telling, like "extent_index" :)
+ while (count) { + const size_t len = MIN (count, request_size);
- if (exts.ptr[i].zero) { + if (only_zeroes (exts, &i, offset, len)) { /* The source is zero so we can proceed directly to skipping, - * fast zeroing, or writing zeroes at the destination. + * fast zeroing, or writing zeroes at the destination. Defer + * zeroing so we can send it as a single large command. */ - command = create_command (exts.ptr[i].offset, exts.ptr[i].length, - true, w); - fill_dst_range_with_zeroes (command); + if (!is_zeroing) { + is_zeroing = true; + zeroing_start = offset; + } } - else /* data */ { - /* As the extent might be larger than permitted for a single - * command, we may have to split this into multiple read - * requests. - */ - while (exts.ptr[i].length > 0) { - len = exts.ptr[i].length; - if (len > request_size) - len = request_size; - - command = create_command (exts.ptr[i].offset, len, - false, w); - - wait_for_request_slots (w); - - /* NOTE: Must increase the queue size after waiting. */ - increase_queue_size (w, len); - - /* Begin the asynch read operation. */ - src->ops->asynch_read (src, command, - (nbd_completion_callback) { - .callback = finished_read, - .user_data = command, - }); - - exts.ptr[i].offset += len; - exts.ptr[i].length -= len; + /* If we were in the middle of deferred zeroing, do it now. */ + if (is_zeroing) { + /* Note that offset-zeroing_start can never exceed + * THREAD_WORK_SIZE, so there is no danger of overflowing + * size_t. + */ + command = create_command (zeroing_start, offset-zeroing_start, + true, w); + fill_dst_range_with_zeroes (command); + is_zeroing = false; } + + /* Issue the asynchronous read command. */ + command = create_command (offset, len, false, w); + + wait_for_request_slots (w); + + /* NOTE: Must increase the queue size after waiting. */ + increase_queue_size (w, len); + + /* Begin the asynch read operation. */ + src->ops->asynch_read (src, command, + (nbd_completion_callback) { + .callback = finished_read, + .user_data = command, + }); }
- offset += count; - count = 0; - } /* for extents */ + offset += len; + count -= len; + } /* while (count) */ + + /* If we were in the middle of deferred zeroing, do it now. */ + if (is_zeroing) { + /* Note that offset-zeroing_start can never exceed + * THREAD_WORK_SIZE, so there is no danger of overflowing + * size_t. + */ + command = create_command (zeroing_start, offset - zeroing_start, + true, w); + fill_dst_range_with_zeroes (command); + is_zeroing = false; + } }
/* Wait for in flight NBD requests to finish. */
This looks OK to me. Thanks Laszlo
diff --git a/copy/nbdcopy.pod b/copy/nbdcopy.pod index 54e43915f3..7a4b57f1b5 100644 --- a/copy/nbdcopy.pod +++ b/copy/nbdcopy.pod @@ -182,8 +182,9 @@ Set the maximum number of requests in flight per NBD connection. =item B<--sparse=>N
Detect all zero blocks of size N (bytes) and make them sparse on the -output. You can also turn off sparse detection using S<I<-S 0>>. -The default is 4096 bytes. +output. You can also turn off sparse detection using S<I<-S 0>>. The +default is 4096 bytes, or the destination preferred block size, +whichever is larger.
=item B<--synchronous>
On Thu, Jun 30, 2022 at 05:38:14PM +0200, Laszlo Ersek wrote:
On 06/29/22 15:36, Richard W.M. Jones wrote:
+ * The invariant for '*i' is always an extent which starts before or + * equal to the current offset. + */ +static bool +only_zeroes (const extent_list exts, size_t *i,
(5) I think it's a bit wasteful (or at least strange) to pass in the entire vector structure; we certainly don't need the "cap" field. Other functions in libnbd seem to take pointers to vectors (which is what I'd expect).
Note it's only passing in 3 fields (24 bytes), not the entire array (and I guess it should inline the whole function). Does that change things?
@@ -177,7 +229,10 @@ worker_thread (void *wp) extent_list exts = empty_vector;
while (get_next_offset (&offset, &count)) { + struct command *command; size_t i; + bool is_zeroing = false; + uint64_t zeroing_start = 0;
(14) I think we should not initialize "zeroing_start". It suggests that the initializer value is meaningful, but it is not.
GCC warns if you don't initialize it! It doesn't understand that is_zeroing is related to the validity of zeroing_start. Thanks for the other suggestions, I'll rework things. Rich. -- Richard Jones, Virtualization Group, Red Hat http://people.redhat.com/~rjones Read my programming and virtualization blog: http://rwmj.wordpress.com Fedora Windows cross-compiler. Compile Windows programs, test, and build Windows installers. Over 100 libraries supported. http://fedoraproject.org/wiki/MinGW
On 06/30/22 17:47, Richard W.M. Jones wrote:
On Thu, Jun 30, 2022 at 05:38:14PM +0200, Laszlo Ersek wrote:
On 06/29/22 15:36, Richard W.M. Jones wrote:
+ * The invariant for '*i' is always an extent which starts before or + * equal to the current offset. + */ +static bool +only_zeroes (const extent_list exts, size_t *i,
(5) I think it's a bit wasteful (or at least strange) to pass in the entire vector structure; we certainly don't need the "cap" field. Other functions in libnbd seem to take pointers to vectors (which is what I'd expect).
Note it's only passing in 3 fields (24 bytes), not the entire array (and I guess it should inline the whole function). Does that change things?
It's not wrong by any means, just unusual :) Whole-structure passing and returning is really rare in my understanding, div() is one standard function that does it. I don't insist :)
@@ -177,7 +229,10 @@ worker_thread (void *wp) extent_list exts = empty_vector;
while (get_next_offset (&offset, &count)) { + struct command *command; size_t i; + bool is_zeroing = false; + uint64_t zeroing_start = 0;
(14) I think we should not initialize "zeroing_start". It suggests that the initializer value is meaningful, but it is not.
GCC warns if you don't initialize it! It doesn't understand that is_zeroing is related to the validity of zeroing_start.
We use "#pragma GCC diagnostic" elsewhere in libnbd (in particular in "copy/progress.c"), can we use it here too? (Just for this one variable?) If not, I'd suggest appending a small comment: uint64_t zeroing_start = 0; /* shut up invalid GCC warning */
Thanks for the other suggestions, I'll rework things.
Rich.
Thanks! Laszlo
On Thu, Jun 30, 2022 at 05:38:14PM +0200, Laszlo Ersek wrote:
+static bool +only_zeroes (const extent_list exts, size_t *i, [...] + + if (start >= offset + len) + break; +
(13) I think we can insert a micro-optimization here:
*i = j; [...] The loop at the start of the function is correct, but here we can save some work for its next execution (in the next invocation of this function).
Just a note that I didn't do this optimization. The reason is that it breaks the ability (which we don't use) of calling only_zeroes twice on the same offset. I just feel that it might cause unexpected results if someone hoisted out the only_zeroes call into a debug printf or similar. I made the rest of the suggested changes, except for splitting out the test into another commit - we surely want to keep the test with the change that is being tested. Rich. -- Richard Jones, Virtualization Group, Red Hat http://people.redhat.com/~rjones Read my programming and virtualization blog: http://rwmj.wordpress.com virt-builder quickly builds VMs from scratch http://libguestfs.org/virt-builder.1.html
On 06/30/22 18:34, Richard W.M. Jones wrote:
On Thu, Jun 30, 2022 at 05:38:14PM +0200, Laszlo Ersek wrote:
+static bool +only_zeroes (const extent_list exts, size_t *i, [...] + + if (start >= offset + len) + break; +
(13) I think we can insert a micro-optimization here:
*i = j; [...] The loop at the start of the function is correct, but here we can save some work for its next execution (in the next invocation of this function).
Just a note that I didn't do this optimization.
The reason is that it breaks the ability (which we don't use) of calling only_zeroes twice on the same offset. I just feel that it might cause unexpected results if someone hoisted out the only_zeroes call into a debug printf or similar.
I made the rest of the suggested changes, except for splitting out the test into another commit - we surely want to keep the test with the change that is being tested.
Sounds good, thank you. Laszlo
Because the test previously used error rates of 50%, it could sometimes "fail to fail". This is noticable if you run the test repeatedly: $ while make -C copy check TESTS=copy-nbd-error.sh >& /tmp/log ; do echo -n . ; done This now happens more often because of the larger requests made by the new multi-threaded loop, resulting in fewer calls to the error filter, so a greater chance that a series of 50% coin tosses will come up all heads in the test. Fix this by making the test non-stocastic. Fixes: commit 8d444b41d09a700c7ee6f9182a649f3f2d325abb --- copy/copy-nbd-error.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/copy/copy-nbd-error.sh b/copy/copy-nbd-error.sh index 0088807f54..01524a890c 100755 --- a/copy/copy-nbd-error.sh +++ b/copy/copy-nbd-error.sh @@ -40,7 +40,7 @@ $VG nbdcopy -- [ nbdkit --exit-with-parent -v --filter=error pattern 5M \ # Failure to read should be fatal echo "Testing read failures on non-sparse source" $VG nbdcopy -- [ nbdkit --exit-with-parent -v --filter=error pattern 5M \ - error-pread-rate=0.5 ] null: && fail=1 + error-pread-rate=1 ] null: && fail=1 # However, reliable block status on a sparse image can avoid the need to read echo "Testing read failures on sparse source" @@ -51,7 +51,7 @@ $VG nbdcopy -- [ nbdkit --exit-with-parent -v --filter=error null 5M \ echo "Testing write data failures on arbitrary destination" $VG nbdcopy -- [ nbdkit --exit-with-parent -v pattern 5M ] \ [ nbdkit --exit-with-parent -v --filter=error --filter=noextents \ - memory 5M error-pwrite-rate=0.5 ] && fail=1 + memory 5M error-pwrite-rate=1 ] && fail=1 # However, writing zeroes can bypass the need for normal writes echo "Testing write data failures from sparse source" -- 2.37.0.rc2
On 06/29/22 15:36, Richard W.M. Jones wrote:
Because the test previously used error rates of 50%, it could sometimes "fail to fail". This is noticable if you run the test repeatedly:
$ while make -C copy check TESTS=copy-nbd-error.sh >& /tmp/log ; do echo -n . ; done
This now happens more often because of the larger requests made by the new multi-threaded loop, resulting in fewer calls to the error filter, so a greater chance that a series of 50% coin tosses will come up all heads in the test.
Fix this by making the test non-stocastic.
Fixes: commit 8d444b41d09a700c7ee6f9182a649f3f2d325abb --- copy/copy-nbd-error.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/copy/copy-nbd-error.sh b/copy/copy-nbd-error.sh index 0088807f54..01524a890c 100755 --- a/copy/copy-nbd-error.sh +++ b/copy/copy-nbd-error.sh @@ -40,7 +40,7 @@ $VG nbdcopy -- [ nbdkit --exit-with-parent -v --filter=error pattern 5M \ # Failure to read should be fatal echo "Testing read failures on non-sparse source" $VG nbdcopy -- [ nbdkit --exit-with-parent -v --filter=error pattern 5M \ - error-pread-rate=0.5 ] null: && fail=1 + error-pread-rate=1 ] null: && fail=1
# However, reliable block status on a sparse image can avoid the need to read echo "Testing read failures on sparse source" @@ -51,7 +51,7 @@ $VG nbdcopy -- [ nbdkit --exit-with-parent -v --filter=error null 5M \ echo "Testing write data failures on arbitrary destination" $VG nbdcopy -- [ nbdkit --exit-with-parent -v pattern 5M ] \ [ nbdkit --exit-with-parent -v --filter=error --filter=noextents \ - memory 5M error-pwrite-rate=0.5 ] && fail=1 + memory 5M error-pwrite-rate=1 ] && fail=1
# However, writing zeroes can bypass the need for normal writes echo "Testing write data failures from sparse source"
Wasn't the original intent of the 50% error rate that the first error manifest usually at a different offset every time? If we change the error rate to 1, the tests will fail upon the first access, which kind of breaks the original intent. I wonder if we could determine a random offset in advance, and make sure that the read or write access fails 100%, but only if the request covers that offset. ... The probability that n subsequent accesses *don't* fail is (1-error_rate)^n. (The probability that at least one access fails is 1-(1-error_rate)^n.) And n is given by (I think?) image_size/request_size. So, if we change the request_size, we can recalculate "n", for the test not to fail with the same probability as before. (1-err1)^(imgsz/rsz1) = (1-err2)^(imgsz/rsz2) draw the imgsz'th root of both sides (1-err1)^(1/rsz1) = (1-err2)^(1/rsz2) raise both sides to the rsz2'nd power (1-err1)^(rsz2/rsz1) = 1-err2 err2 = 1 - (1-err1)^(rsz2/rsz1) I know that err1=0.5, but don't know rsz2 and rsz1 (the request sizes after, and before, the last patch in the series). Assuming (just guessing!) we increased the request size 8-fold, we'd have to go from error rate 0.5 to: err2 = 1 - (1-0.5)^8 = 1 - (1/2)^8 = 1 - (1 / 256) = 255/256 = 0.99609375 We basically group every eight coin tosses into one super-toss, and want the latter to show "failure" with the same probability as *at least one* of the original 8 tosses failing. Laszlo
On Wed, Jun 29, 2022 at 06:01:54PM +0200, Laszlo Ersek wrote:
On 06/29/22 15:36, Richard W.M. Jones wrote:
Because the test previously used error rates of 50%, it could sometimes "fail to fail". This is noticable if you run the test repeatedly:
$ while make -C copy check TESTS=copy-nbd-error.sh >& /tmp/log ; do echo -n . ; done
This now happens more often because of the larger requests made by the new multi-threaded loop, resulting in fewer calls to the error filter, so a greater chance that a series of 50% coin tosses will come up all heads in the test.
Fix this by making the test non-stocastic.
Fixes: commit 8d444b41d09a700c7ee6f9182a649f3f2d325abb --- copy/copy-nbd-error.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/copy/copy-nbd-error.sh b/copy/copy-nbd-error.sh index 0088807f54..01524a890c 100755 --- a/copy/copy-nbd-error.sh +++ b/copy/copy-nbd-error.sh @@ -40,7 +40,7 @@ $VG nbdcopy -- [ nbdkit --exit-with-parent -v --filter=error pattern 5M \ # Failure to read should be fatal echo "Testing read failures on non-sparse source" $VG nbdcopy -- [ nbdkit --exit-with-parent -v --filter=error pattern 5M \ - error-pread-rate=0.5 ] null: && fail=1 + error-pread-rate=1 ] null: && fail=1
# However, reliable block status on a sparse image can avoid the need to read echo "Testing read failures on sparse source" @@ -51,7 +51,7 @@ $VG nbdcopy -- [ nbdkit --exit-with-parent -v --filter=error null 5M \ echo "Testing write data failures on arbitrary destination" $VG nbdcopy -- [ nbdkit --exit-with-parent -v pattern 5M ] \ [ nbdkit --exit-with-parent -v --filter=error --filter=noextents \ - memory 5M error-pwrite-rate=0.5 ] && fail=1 + memory 5M error-pwrite-rate=1 ] && fail=1
# However, writing zeroes can bypass the need for normal writes echo "Testing write data failures from sparse source"
Wasn't the original intent of the 50% error rate that the first error manifest usually at a different offset every time? If we change the error rate to 1, the tests will fail upon the first access, which kind of breaks the original intent.
I wonder if we could determine a random offset in advance, and make sure that the read or write access fails 100%, but only if the request covers that offset.
It wasn't very clear why the error rate was originally set to 50%, but it could be as you say. It is possible to inject an error at a specific offset (though not with the error filter): ---------------------------------------------------------------------- #!/bin/bash case "$1" in get_size) echo 64M ;; pread) if [ $4 -le 100000 ] && [ $(( $4+$3 )) -gt 100000 ]; then echo EIO Bad block >&2 exit 1 else dd if=/dev/zero count=$3 iflag=count_bytes fi ;; *) exit 2 ;; esac ---------------------------------------------------------------------- $ nbdkit sh read-error.sh - - - Anyway, looking closer at the bug, I now understand why it happens much more frequently after applying this patch series. It's because the test uses the null: destination (libnbd.git/copy/null-ops.c). This advertises a preferred block size of 4M, because I reasoned that we should prefer large blocks for this target (since it throws away the data). This causes request-size to be increased to 4M, so we're copying 4M blocks (versus 256K before). With 5M total disk size, we're reading 4M + 1M in two requests, so in 1 in 4 cases we're going to get two heads in a row, and the test will fail to fail. Although there is still definitely a bug in this test, I think I will adjust the null: target so it sets a small preferred block size, eg. 4K, so that copying to null: doesn't have this unexpected effect on request size. Rich. -- Richard Jones, Virtualization Group, Red Hat http://people.redhat.com/~rjones Read my programming and virtualization blog: http://rwmj.wordpress.com libguestfs lets you edit virtual machines. Supports shell scripting, bindings from many languages. http://libguestfs.org
participants (2)
-
Laszlo Ersek -
Richard W.M. Jones