Re: [PATCH] bcache: improve writeback throughput when frontend I/O is idle
jifeng zhou <[email protected]> Fri, 29 Aug 2025 15:56:42 +0800
| Newsgroups | org.kernel.vger.linux-bcache |
|---|---|
| Message-ID | <CAOtvtNA++zLk0TcMwScJitDNGKUiVAVpMw803cYVG5vrtK2P_g@mail.gmail.com> |
On Fri, 29 Aug 2025 at 00:20, <[email protected]> wrote: > > Currently in order to write dirty blocks to backend device in LBA order > for better performance, inside write_dirty() the I/O is issued only when > its sequence matches current expected sequence. Otherwise the kworker > will repeat check-wait-woken loop until the sequence number matches. > > When frontend I/O is idle, the writeback rate is set to INT_MAX, but the > writeback thoughput doesn't increase much. There are two reasons, > - The check-wait-woken loop is inefficient. > - I/O depth on backing device is every low. > > To improve the writeback throughput, this patch continues to use LBA re- > order idea, but improves it by the following means, > - Do the reorder from write_dirty() to read_dirty(). > Inside read_dirty(), use a min_heap to order all the to-be-writebacked > keys, and read dirty blocks in LBA order. Although each read requests > are not completed in issue order, there is no check-wait-woken loop so > that the dirty blocks are issued in a small time range and they can be > ordered by I/O schedulers efficiently. > > - Read more dirty keys when frontend I/O is idle > Define WRITEBACKS_IN_PASS (5), MAX_WRITEBACKS_IN_PASS (80) for write- > back dirty keys in each pass, and define WRITESIZE_IN_PASS (5000) and > MAX_WRITESIZE_IN_PASS (80000) for total writeback data size in each > pass. When frontend I/O is idle, new values MAX_WRITEBACKS_IN_PASS and > MAX_WRITESIZE_IN_PASS are used to read more dirty keys and data size > from cache deice, then more dirty blocks will be written to backend > device in almost LBA order. > > By this effort, when there is frontend I/O, the IOPS and latency almost > has no difference observed, identical from previous read_dirty() and > write_dirty() implementation. When frontend I/O is idle, with this patch > the average queue size increases from 2.5 to 21, writeback thoughput on > backing device increases from 12MiB/s to 20MiB/s. > > Writeback throughput increases around 67% when frontend I/O is idle. > > Signed-off-by: Coly Li <[email protected]> > --- > drivers/md/bcache/bcache.h | 1 + > drivers/md/bcache/util.h | 8 ++++ > drivers/md/bcache/writeback.c | 82 +++++++++++++++++------------------ > drivers/md/bcache/writeback.h | 6 ++- > 4 files changed, 52 insertions(+), 45 deletions(-) > > diff --git a/drivers/md/bcache/bcache.h b/drivers/md/bcache/bcache.h > index d43fcccf297c..88fb9bb69ce9 100644 > --- a/drivers/md/bcache/bcache.h > +++ b/drivers/md/bcache/bcache.h > @@ -345,6 +345,7 @@ struct cached_dev { > struct workqueue_struct *writeback_write_wq; > > struct keybuf writeback_keys; > + DECLARE_HEAP(struct keybuf_key *, read_dirty_heap); > > struct task_struct *status_update_thread; > /* > diff --git a/drivers/md/bcache/util.h b/drivers/md/bcache/util.h > index f61ab1bada6c..3f5f85bdeafe 100644 > --- a/drivers/md/bcache/util.h > +++ b/drivers/md/bcache/util.h > @@ -46,6 +46,14 @@ struct closure; > (heap)->data; \ > }) > > +#define reset_heap(heap) \ > +({ \ > + size_t _bytes; \ > + _bytes = (heap)->size * sizeof(*(heap)->data); \ > + memset((heap)->data, 0, _bytes); \ > + (heap)->used = 0; \ > +}) > + > #define free_heap(heap) \ > do { \ > kvfree((heap)->data); \ > diff --git a/drivers/md/bcache/writeback.c b/drivers/md/bcache/writeback.c > index 302e75f1fc4b..4f0e47c841aa 100644 > --- a/drivers/md/bcache/writeback.c > +++ b/drivers/md/bcache/writeback.c > @@ -406,26 +406,6 @@ static CLOSURE_CALLBACK(write_dirty) > struct keybuf_key *w = io->bio.bi_private; > struct cached_dev *dc = io->dc; > > - uint16_t next_sequence; > - > - if (atomic_read(&dc->writeback_sequence_next) != io->sequence) { > - /* Not our turn to write; wait for a write to complete */ > - closure_wait(&dc->writeback_ordering_wait, cl); > - > - if (atomic_read(&dc->writeback_sequence_next) == io->sequence) { > - /* > - * Edge case-- it happened in indeterminate order > - * relative to when we were added to wait list.. > - */ > - closure_wake_up(&dc->writeback_ordering_wait); > - } > - > - continue_at(cl, write_dirty, io->dc->writeback_write_wq); > - return; > - } > - > - next_sequence = io->sequence + 1; > - > /* > * IO errors are signalled using the dirty bit on the key. > * If we failed to read, we should not attempt to write to the > @@ -443,7 +423,6 @@ static CLOSURE_CALLBACK(write_dirty) > closure_bio_submit(io->dc->disk.c, &io->bio, cl); > } > > - atomic_set(&dc->writeback_sequence_next, next_sequence); > closure_wake_up(&dc->writeback_ordering_wait); > > continue_at(cl, write_dirty_finish, io->dc->writeback_write_wq); > @@ -471,18 +450,25 @@ static CLOSURE_CALLBACK(read_dirty_submit) > continue_at(cl, write_dirty, io->dc->writeback_write_wq); > } > > +static uint64_t keybuf_key_cmp(const struct keybuf_key *l, > + const struct keybuf_key *r) > +{ > + if (unlikely((KEY_INODE(&l->key) != KEY_INODE(&r->key)))) > + return KEY_INODE(&l->key) > KEY_INODE(&r->key); > + else > + return KEY_OFFSET(&l->key) > KEY_OFFSET(&r->key); > +} > + > static void read_dirty(struct cached_dev *dc) > { > unsigned int delay = 0; > - struct keybuf_key *next, *keys[MAX_WRITEBACKS_IN_PASS], *w; > - size_t size; > - int nk, i; > + struct keybuf_key *next, *w; > struct dirty_io *io; > struct closure cl; > - uint16_t sequence = 0; > + size_t size; > + int nk, i; > > BUG_ON(!llist_empty(&dc->writeback_ordering_wait.list)); > - atomic_set(&dc->writeback_sequence_next, sequence); > closure_init_stack(&cl); > > /* > @@ -495,46 +481,49 @@ static void read_dirty(struct cached_dev *dc) > while (!kthread_should_stop() && > !test_bit(CACHE_SET_IO_DISABLE, &dc->disk.c->flags) && > next) { > + size_t max_size_in_pass; > + int max_writebacks_in_pass; > + > size = 0; > nk = 0; > + reset_heap(&dc->read_dirty_heap); > > do { > BUG_ON(ptr_stale(dc->disk.c, &next->key, 0)); > > + if (atomic_read(&dc->disk.c->at_max_writeback_rate)) { > + max_writebacks_in_pass = MAX_WRITEBACKS_IN_PASS; > + max_size_in_pass = MAX_WRITESIZE_IN_PASS; > + } else { > + max_writebacks_in_pass = WRITEBACKS_IN_PASS; > + max_size_in_pass = WRITESIZE_IN_PASS; > + } > + > /* > * Don't combine too many operations, even if they > * are all small. > */ > - if (nk >= MAX_WRITEBACKS_IN_PASS) > + if (nk >= max_writebacks_in_pass) > break; > > /* > * If the current operation is very large, don't > * further combine operations. > */ > - if (size >= MAX_WRITESIZE_IN_PASS) > + if (size >= max_size_in_pass) > break; > > - /* > - * Operations are only eligible to be combined > - * if they are contiguous. > - * > - * TODO: add a heuristic willing to fire a > - * certain amount of non-contiguous IO per pass, > - * so that we can benefit from backing device > - * command queueing. > - */ > - if ((nk != 0) && bkey_cmp(&keys[nk-1]->key, > - &START_KEY(&next->key))) > + if (!heap_add(&dc->read_dirty_heap, next, > + keybuf_key_cmp)) > break; The bkeys retrieved from the dc->writeback_keys rbtree are in a specific order. Can the heap sorting here be omitted? Zhou Jifeng > > size += KEY_SIZE(&next->key); > - keys[nk++] = next; > + nk++; > } while ((next = bch_keybuf_next(&dc->writeback_keys))); > > /* Now we have gathered a set of 1..5 keys to write back. */ > for (i = 0; i < nk; i++) { > - w = keys[i]; > + heap_pop(&dc->read_dirty_heap, w, keybuf_key_cmp); > > io = kzalloc(struct_size(io, bio.bi_inline_vecs, > DIV_ROUND_UP(KEY_SIZE(&w->key), PAGE_SECTORS)), > @@ -544,7 +533,6 @@ static void read_dirty(struct cached_dev *dc) > > w->private = io; > io->dc = dc; > - io->sequence = sequence++; > > dirty_init(w); > io->bio.bi_opf = REQ_OP_READ; > @@ -835,6 +823,7 @@ static int bch_writeback_thread(void *arg) > if (dc->writeback_write_wq) > destroy_workqueue(dc->writeback_write_wq); > > + free_heap(&dc->read_dirty_heap); > cached_dev_put(dc); > wait_for_kthread_stop(); > > @@ -1080,12 +1069,19 @@ int bch_cached_dev_writeback_start(struct cached_dev *dc) > if (!dc->writeback_write_wq) > return -ENOMEM; > > + if (!init_heap(&dc->read_dirty_heap, MAX_WRITEBACKS_IN_PASS, > + GFP_KERNEL)) { > + destroy_workqueue(dc->writeback_write_wq); > + return -ENOMEM; > + } > + > cached_dev_get(dc); > dc->writeback_thread = kthread_create(bch_writeback_thread, dc, > "bcache_writeback"); > if (IS_ERR(dc->writeback_thread)) { > - cached_dev_put(dc); > destroy_workqueue(dc->writeback_write_wq); > + free_heap(&dc->read_dirty_heap); > + cached_dev_put(dc); > return PTR_ERR(dc->writeback_thread); > } > dc->writeback_running = true; > diff --git a/drivers/md/bcache/writeback.h b/drivers/md/bcache/writeback.h > index 31df716951f6..7e6b75768cad 100644 > --- a/drivers/md/bcache/writeback.h > +++ b/drivers/md/bcache/writeback.h > @@ -8,8 +8,10 @@ > #define CUTOFF_WRITEBACK_MAX 70 > #define CUTOFF_WRITEBACK_SYNC_MAX 90 > > -#define MAX_WRITEBACKS_IN_PASS 5 > -#define MAX_WRITESIZE_IN_PASS 5000 /* *512b */ > +#define WRITEBACKS_IN_PASS 5 > +#define MAX_WRITEBACKS_IN_PASS 80 > +#define WRITESIZE_IN_PASS 5000 /* *512b */ > +#define MAX_WRITESIZE_IN_PASS 80000 /* *512b */ > > #define WRITEBACK_RATE_UPDATE_SECS_MAX 60 > #define WRITEBACK_RATE_UPDATE_SECS_DEFAULT 5 > -- > 2.47.2 > >