Re: [PATCH] bcache: improve writeback throughput when frontend I/O is idle
Coly Li <[email protected]> Sat, 30 Aug 2025 02:17:29 +0800
| Newsgroups | org.kernel.vger.linux-bcache |
|---|---|
| Message-ID | <2wpmy6joztl2uc5v5mrv4v4edkqtijpbwe7pqlfrbv6hh3mgks@bgab27rwjkka> |
On Fri, Aug 29, 2025 at 03:56:42PM +0800, jifeng zhou wrote: > 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? Indeed the keys in dc->writeback_keys are not in a specific order, yes they are in the order from oldest to newest, but there is *NO* overlap betwen any two keys in dc->writeback_keys. See RB_INSERT() inside refill_keybuf_fn(). Because there is no overlap inside keys of dc->writeback_keys, re-order them by LBA incremental order won't make trouble here. Thanks. Coly Li