[PATCH] bcache: improve writeback throughput when frontend I/O is idle
[email protected] Fri, 29 Aug 2025 00:19:51 +0800
| Newsgroups | org.kernel.vger.linux-bcache |
|---|---|
| Message-ID | <[email protected]> |
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; 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