Re: [PATCH] bcache: improve writeback throughput when frontend I/O is idle

Coly Li <[email protected]> Sat, 30 Aug 2025 15:28:41 +0800
Newsgroups org.kernel.vger.linux-bcache
Message-ID <[email protected]>
On Sat, Aug 30, 2025 at 02:17:29AM +0800, Coly Li wrote:
> 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(-)
>>> 

[snipped]
>>> 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.
> 

I want to say thank you! I re-read the refill_dirty() implementation, it seems in
read_dirty(), it is unnecessary to call bch_keybuf_del(). Let me try whethere
there is chance to do more improvement.

Coly Li