Improve zoned (SMR) HDD write throughput
Subsystems (2)
Damien Le Moal
maintainerv4 cover Improve zoned (SMR) HDD write throughput
Jens,
This patch series cleans up the zone write plugging code and introduces the ability to issue all write BIOs from a single context (a kthread) instead of allowing multiple zones to be written at the same time using a per zone work. As shown in patch 6, raw block device tests and XFS tests with an SMR HDD show that this can significantly increase write throughput (up to 40% over the current zone write plugging).
Changes from v3:
- Corrected patch 6 to remove the unnecessary blk_queue_is_zoned() check in queue_zoned_qd1_writes_store(), as well as the commit message to mention the visibility of the zoned_qd1_writes sysfs attribute.
- Corrected the documentation text in patch 8 to better describe the visibility of the zoned_qd1_writes sysfs attribute.
- Added review tags.
Changes from v2:
- Modified patch 1 slightly to not restore dead zones and instead check for such state in the BIO submission path and fail BIOs that are submitted to dead zones as that represents an erroneous use of zones.
- Added patch 3
- Added review tags.
Changes from v1:
- Reworked patch 1 to remove the fragile use of refcount_read. Zone write plug removal is now more solid by being controlled with refcount_dec_and_test().
- Changed patch 2 to keep the refcount increase before scheduling the BIO work but drop that reference if scheduling the work did nothing.
- Dropped the patch "block: remove BLK_ZONE_WPLUG_UNHASHED" as the patch 1 changes make it unnecessary.
- Modified patch 5 to have the zoned_qd1_writes attribute visible only for zoned block devices.
- Added review tags
Damien Le Moal (8):
block: fix zone write plug removal
block: fix zone write plugs refcount handling in
disk_zone_wplug_schedule_bio_work()
block: rename and simplify disk_get_and_lock_zone_wplug()
block: remove disk_zone_is_full()
block: rename struct gendisk zone_wplugs_lock field
block: allow submitting all zone writes from a single context
block: default to QD=1 writes for blk-mq rotational zoned devices
Documentation: ABI: stable: document the zoned_qd1_writes attribute
Documentation/ABI/stable/sysfs-block | 15 + block/blk-mq-debugfs.c | 1 + block/blk-sysfs.c | 43 ++- block/blk-zoned.c | 402 +++++++++++++++++---------- include/linux/blkdev.h | 10 +- 5 files changed, 320 insertions(+), 151 deletions(-)
Signature · 1 line
2.53.0
Damien Le Moal
maintainerPATCH v4 1/8 block: fix zone write plug removal
Commit 7b295187287e ("block: Do not remove zone write plugs still in use") modified disk_should_remove_zone_wplug() to add a check on the reference count of a zone write plug to prevent removing zone write plugs from a disk hash table when the plugs are still being referenced by BIOs or requests in-flight. However, this check does not take into account that a BIO completion may happen right after its submission by a zone write plug BIO work, and before the zone write plug BIO work releases the zone write plug reference count. This situation leads to disk_should_remove_zone_wplug() returning false as in this case the zone write plug reference count is at least equal to 3. If the BIO that completes in such manner transitioned the zone to the FULL condition, the zone write plug for the FULL zone will remain in the disk hash table.
Furthermore, relying on a particular value of a zone write plug reference count to set the BLK_ZONE_WPLUG_UNHASHED flag is fragile as reading the atomic reference count and doing a comparison with some value is not overall atomic at all.
Address these issues by reworking the reference counting of zone write plugs so that removing plugs from a disk hash table can be done directly from disk_put_zone_wplug() when the last reference on a plug is dropped.
To do so, replace the function disk_remove_zone_wplug() with disk_mark_zone_wplug_dead(). This new function sets the zone write plug flag BLK_ZONE_WPLUG_DEAD (which replaces BLK_ZONE_WPLUG_UNHASHED) and drops the initial reference on the zone write plug taken when the plug was added to the disk hash table. This function is called either for zones that are empty or full, or directly in the case of a forced plug removal (e.g. when the disk hash table is being destroyed on disk removal). With this change, disk_should_remove_zone_wplug() is also removed.
disk_put_zone_wplug() is modified to call the function disk_free_zone_wplug() to remove a zone write plug from a disk hash table and free the plug structure (with a call_rcu()), when the last reference on a zone write plug is dropped. disk_free_zone_wplug() always checks that the BLK_ZONE_WPLUG_DEAD flag is set.
In order to avoid having multiple zone write plugs for the same zone in the disk hash table, disk_get_and_lock_zone_wplug() checked for the BLK_ZONE_WPLUG_UNHASHED flag. This check is removed and a check for the new BLK_ZONE_WPLUG_DEAD flag is added to blk_zone_wplug_handle_write(). With this change, we continue preventing adding multiple zone write plugs for the same zone and at the same time re-inforce checks on the user behavior by failing new incoming write BIOs targeting a zone that is marked as dead. This case can happen only if the user erroneously issues write BIOs to zones that are full, or to zones that are currently being reset or finished.
--- block/blk-zoned.c | 151 +++++++++++++++++----------------------------- 1 file changed, 57 insertions(+), 94 deletions(-)
block/blk-zoned.c +57 -94
@@ -99,17 +99,17 @@ static inline unsigned int disk_zone_wplugs_hash_size(struct gendisk *disk)* being executed or the zone write plug bio list is not empty. * - BLK_ZONE_WPLUG_NEED_WP_UPDATE: Indicates that we lost track of a zone * write pointer offset and need to update it. -* - BLK_ZONE_WPLUG_UNHASHED: Indicates that the zone write plug was removed -* from the disk hash table and that the initial reference to the zone -* write plug set when the plug was first added to the hash table has been -* dropped. This flag is set when a zone is reset, finished or become full, -* to prevent new references to the zone write plug to be taken for -* newly incoming BIOs. A zone write plug flagged with this flag will be -* freed once all remaining references from BIOs or functions are dropped. +* - BLK_ZONE_WPLUG_DEAD: Indicates that the zone write plug will be +* removed from the disk hash table of zone write plugs when the last +* reference on the zone write plug is dropped. If set, this flag also +* indicates that the initial extra reference on the zone write plug was +* dropped, meaning that the reference count indicates the current number of +* active users (code context or BIOs and requests in flight). This flag is +* set when a zone is reset, finished or becomes full. */ #define BLK_ZONE_WPLUG_PLUGGED (1U << 0)#define BLK_ZONE_WPLUG_NEED_WP_UPDATE (1U << 1) -#define BLK_ZONE_WPLUG_UNHASHED (1U << 2) +#define BLK_ZONE_WPLUG_DEAD (1U << 2)/** * blk_zone_cond_str - Return a zone condition name string
@@ -587,64 +587,15 @@ static void disk_free_zone_wplug_rcu(struct rcu_head *rcu_head)mempool_free (zwplug, zwplug->disk->zone_wplugs_pool); } -static inline void disk_put_zone_wplug (struct blk_zone_wplug *zwplug) -{ -if (refcount_dec_and_test (&zwplug->ref)) { -WARN_ON_ONCE (!bio_list_empty (&zwplug->bio_list)); -WARN_ON_ONCE (zwplug->flags &BLK_ZONE_WPLUG_PLUGGED ); -WARN_ON_ONCE (!(zwplug->flags &BLK_ZONE_WPLUG_UNHASHED )); - -call_rcu (&zwplug->rcu_head, disk_free_zone_wplug_rcu); - } -} - -static inline bool disk_should_remove_zone_wplug (struct gendisk *disk, -struct blk_zone_wplug *zwplug) -{ -lockdep_assert_held (&zwplug->lock); - -/* If the zone write plug was already removed, we are done. */ -if (zwplug->flags &BLK_ZONE_WPLUG_UNHASHED ) -return false; - -/* If the zone write plug is still plugged, it cannot be removed. */ -if (zwplug->flags &BLK_ZONE_WPLUG_PLUGGED ) -return false; - -/* -* Completions of BIOs with blk_zone_write_plug_bio_endio() may -* happen after handling a request completion with -* blk_zone_write_plug_finish_request() (e.g. with split BIOs -* that are chained). In such case, disk_zone_wplug_unplug_bio() -* should not attempt to remove the zone write plug until all BIO -* completions are seen. Check by looking at the zone write plug -* reference count, which is 2 when the plug is unused (one reference -* taken when the plug was allocated and another reference taken by the -* caller context). -*/ -if (refcount_read (&zwplug->ref) >2 ) -return false; - -/* We can remove zone write plugs for zones that are empty or full. */ -return !zwplug->wp_offset ||disk_zone_wplug_is_full (disk, zwplug); -} - -static void disk_remove_zone_wplug (struct gendisk *disk, -struct blk_zone_wplug *zwplug) +static void disk_free_zone_wplug (struct blk_zone_wplug *zwplug) { +struct gendisk *disk = zwplug->disk;unsigned long flags; -/* If the zone write plug was already removed, we have nothing to do. */ -if (zwplug->flags &BLK_ZONE_WPLUG_UNHASHED ) -return ; +WARN_ON_ONCE (!(zwplug->flags &BLK_ZONE_WPLUG_DEAD )); +WARN_ON_ONCE (zwplug->flags &BLK_ZONE_WPLUG_PLUGGED ); +WARN_ON_ONCE (!bio_list_empty (&zwplug->bio_list)); -/* -* Mark the zone write plug as unhashed and drop the extra reference we -* took when the plug was inserted in the hash table. Also update the -* disk zone condition array with the current condition of the zone -* write plug. -*/ - zwplug->flags |=BLK_ZONE_WPLUG_UNHASHED ;spin_lock_irqsave (&disk->zone_wplugs_lock, flags); blk_zone_set_cond(rcu_dereference_check(disk->zones_cond, lockdep_is_held(&disk->zone_wplugs_lock)),
@@ -652,7 +603,29 @@ static void disk_remove_zone_wplug(struct gendisk *disk,hlist_del_init_rcu (&zwplug->node);atomic_dec (&disk->nr_zone_wplugs);spin_unlock_irqrestore (&disk->zone_wplugs_lock, flags); -disk_put_zone_wplug (zwplug); + +call_rcu (&zwplug->rcu_head, disk_free_zone_wplug_rcu); +} + +static inline void disk_put_zone_wplug (struct blk_zone_wplug *zwplug) +{ +if (refcount_dec_and_test (&zwplug->ref)) +disk_free_zone_wplug (zwplug); +} + +/* +* Flag the zone write plug as dead and drop the initial reference we got when +* the zone write plug was added to the hash table. The zone write plug will be +* unhashed when its last reference is dropped. +*/ +static void disk_mark_zone_wplug_dead (struct blk_zone_wplug *zwplug) +{ +lockdep_assert_held (&zwplug->lock); + +if (!(zwplug->flags &BLK_ZONE_WPLUG_DEAD )) { + zwplug->flags |=BLK_ZONE_WPLUG_DEAD ; +disk_put_zone_wplug (zwplug); + } }static void blk_zone_wplug_bio_work (struct work_struct *work);
@@ -672,18 +645,7 @@ static struct blk_zone_wplug *disk_get_and_lock_zone_wplug(struct gendisk *disk, again: zwplug =disk_get_zone_wplug (disk, sector);if (zwplug) { -/* -* Check that a BIO completion or a zone reset or finish -* operation has not already removed the zone write plug from -* the hash table and dropped its reference count. In such case, -* we need to get a new plug so start over from the beginning. -*/ spin_lock_irqsave (&zwplug->lock, *flags); -if (zwplug->flags &BLK_ZONE_WPLUG_UNHASHED ) { -spin_unlock_irqrestore (&zwplug->lock, *flags); -disk_put_zone_wplug (zwplug); - goto again; - }return zwplug; }
@@ -788,14 +750,8 @@ static void disk_zone_wplug_set_wp_offset(struct gendisk *disk,disk_zone_wplug_update_cond (disk, zwplug);disk_zone_wplug_abort (zwplug); - -/* -* The zone write plug now has no BIO plugged: remove it from the -* hash table so that it cannot be seen. The plug will be freed -* when the last reference is dropped. -*/ -if (disk_should_remove_zone_wplug (disk, zwplug)) -disk_remove_zone_wplug (disk, zwplug); +if (!zwplug->wp_offset ||disk_zone_wplug_is_full (disk, zwplug)) +disk_mark_zone_wplug_dead (zwplug); }static unsigned int blk_zone_wp_offset (struct blk_zone *zone)
@@ -1447,6 +1403,19 @@ static bool blk_zone_wplug_handle_write(struct bio *bio, unsigned int nr_segs)return true; } +/* +* If we got a zone write plug marked as dead, then the user is issuing +* writes to a full zone, or without synchronizing with zone reset or +* zone finish operations. In such case, fail the BIO to signal this +* invalid usage. +*/ +if (zwplug->flags &BLK_ZONE_WPLUG_DEAD ) { +spin_unlock_irqrestore (&zwplug->lock, flags); +disk_put_zone_wplug (zwplug); +bio_io_error (bio); +return true; + } +/* Indicate that this BIO is being handled using zone write plugging. */ bio_set_flag (bio,BIO_ZONE_WRITE_PLUGGING );
@@ -1527,7 +1496,7 @@ static void blk_zone_wplug_handle_native_zone_append(struct bio *bio) disk->disk_name, zwplug->zone_no);disk_zone_wplug_abort (zwplug); } -disk_remove_zone_wplug (disk, zwplug); +disk_mark_zone_wplug_dead (zwplug);spin_unlock_irqrestore (&zwplug->lock, flags);disk_put_zone_wplug (zwplug);
@@ -1630,14 +1599,8 @@ static void disk_zone_wplug_unplug_bio(struct gendisk *disk, } zwplug->flags &= ~BLK_ZONE_WPLUG_PLUGGED ; - -/* -* If the zone is full (it was fully written or finished, or empty -* (it was reset), remove its zone write plug from the hash table. -*/ -if (disk_should_remove_zone_wplug (disk, zwplug)) -disk_remove_zone_wplug (disk, zwplug); - +if (!zwplug->wp_offset ||disk_zone_wplug_is_full (disk, zwplug)) +disk_mark_zone_wplug_dead (zwplug);spin_unlock_irqrestore (&zwplug->lock, flags); }
@@ -1848,9 +1811,9 @@ static void disk_destroy_zone_wplugs_hash_table(struct gendisk *disk)while (!hlist_empty (&disk->zone_wplugs_hash[i])) { zwplug =hlist_entry (disk->zone_wplugs_hash[i].first, struct blk_zone_wplug, node); -refcount_inc (&zwplug->ref); -disk_remove_zone_wplug (disk, zwplug); -disk_put_zone_wplug (zwplug); +spin_lock_irq (&zwplug->lock); +disk_mark_zone_wplug_dead (zwplug); +spin_unlock_irq (&zwplug->lock); } }
Signature · 1 line
2.53.0
Re: [PATCH v4 1/8] block: fix zone write plug removal
Re: [PATCH v4 1/8] block: fix zone write plug removal
In reply to Damien Le MoalYou're reposting way too fast for me to keep up, but as this is unchanged from the last round, also here:
Jens Axboe
maintainerRe: [PATCH v4 1/8] block: fix zone write plug removal
Quoted 2 lines · Christoph Hellwig
You're reposting way too fast for me to keep up, but as this is unchanged from the last round, also here:
Indeed, please in general wait at least 24h between version reposts.
Signature · 1 line
Jens Axboe
Damien Le Moal
maintainerPATCH v4 2/8 block: fix zone write plugs refcount handling in disk_zone_wplug_schedule_bio_work()
In reply to Damien Le MoalThe function disk_zone_wplug_schedule_bio_work() always takes a reference on the zone write plug of the BIO work being scheduled. This ensures that the zone write plug cannot be freed while the BIO work is being scheduled but has not run yet. However, this unconditional reference taking is fragile since the reference taken is released by the BIO work blk_zone_wplug_bio_work() function, which implies that there always must be a 1:1 relation between the work being scheduled and the work running.
Make sure to drop the reference taken when scheduling the BIO work if the work is already scheduled, that is, when queue_work() returns false.
--- block/blk-zoned.c | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-)
block/blk-zoned.c +8 -4
@@ -1154,13 +1154,17 @@ static void disk_zone_wplug_schedule_bio_work(struct gendisk *disk,lockdep_assert_held (&zwplug->lock);/* -* Take a reference on the zone write plug and schedule the submission -* of the next plugged BIO. blk_zone_wplug_bio_work() will release the -* reference we take here. +* Schedule the submission of the next plugged BIO. Taking a reference +* to the zone write plug is required as the bio_work belongs to the +* plug, and thus we must ensure that the write plug does not go away +* while the work is being scheduled but has not run yet. +* blk_zone_wplug_bio_work() will release the reference we take here, +* and we also drop this reference if the work is already scheduled. */ WARN_ON_ONCE (!(zwplug->flags &BLK_ZONE_WPLUG_PLUGGED ));refcount_inc (&zwplug->ref); -queue_work (disk->zone_wplugs_wq, &zwplug->bio_work); +if (!queue_work (disk->zone_wplugs_wq, &zwplug->bio_work)) +disk_put_zone_wplug (zwplug); } static inline void disk_zone_wplug_add_bio(struct gendisk *disk,
Signature · 1 line
2.53.0
Damien Le Moal
maintainerPATCH v4 3/8 block: rename and simplify disk_get_and_lock_zone_wplug()
In reply to Damien Le Moaldisk_get_and_lock_zone_wplug() always returns a zone write plug with the plug lock held. This is unnecessary since this function does not look at the fields of existing plugs, and new plugs need to be locked only after their insertion in the disk hash table, when they are being used.
Remove the zone write plug locking from disk_get_and_lock_zone_wplug() and rename this function disk_get_or_alloc_zone_wplug(). blk_zone_wplug_handle_write() is modified to add locking of the zone write plug after calling disk_get_or_alloc_zone_wplug() and before starting to use the plug. This change also simplifies blk_revalidate_seq_zone() as unlocking the plug becomes unnecessary.
--- block/blk-zoned.c | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-)
block/blk-zoned.c +10 -16
@@ -631,23 +631,20 @@ static void disk_mark_zone_wplug_dead(struct blk_zone_wplug *zwplug)static void blk_zone_wplug_bio_work (struct work_struct *work);/* -* Get a reference on the write plug for the zone containing @sector. -* If the plug does not exist, it is allocated and hashed. -* Return a pointer to the zone write plug with the plug spinlock held. +* Get a zone write plug for the zone containing @sector. +* If the plug does not exist, it is allocated and inserted in the disk hash +* table. */ -static struct blk_zone_wplug *disk_get_and_lock_zone_wplug (struct gendisk *disk, -sector_t sector,gfp_t gfp_mask, -unsigned long *flags) +static struct blk_zone_wplug *disk_get_or_alloc_zone_wplug (struct gendisk *disk, +sector_t sector,gfp_t gfp_mask) {unsigned int zno =disk_zone_no (disk, sector);struct blk_zone_wplug *zwplug; again: zwplug =disk_get_zone_wplug (disk, sector); -if (zwplug) { -spin_lock_irqsave (&zwplug->lock, *flags); +if (zwplug)return zwplug; - }/* * Allocate and initialize a zone write plug with an extra reference
@@ -668,15 +665,12 @@ static struct blk_zone_wplug *disk_get_and_lock_zone_wplug(struct gendisk *disk,INIT_WORK (&zwplug->bio_work, blk_zone_wplug_bio_work); zwplug->disk = disk; -spin_lock_irqsave (&zwplug->lock, *flags); -/* * Insert the new zone write plug in the hash table. This can fail only * if another context already inserted a plug. Retry from the beginning * in such case. */ if (!disk_insert_zone_wplug (disk, zwplug)) { -spin_unlock_irqrestore (&zwplug->lock, *flags);mempool_free (zwplug, disk->zone_wplugs_pool); goto again; }
@@ -1398,7 +1392,7 @@ static bool blk_zone_wplug_handle_write(struct bio *bio, unsigned int nr_segs)if (bio->bi_opf &REQ_NOWAIT ) gfp_mask =GFP_NOWAIT ; - zwplug =disk_get_and_lock_zone_wplug (disk, sector, gfp_mask, &flags); + zwplug =disk_get_or_alloc_zone_wplug (disk, sector, gfp_mask);if (!zwplug) {if (bio->bi_opf &REQ_NOWAIT )bio_wouldblock_error (bio);
@@ -1407,6 +1401,8 @@ static bool blk_zone_wplug_handle_write(struct bio *bio, unsigned int nr_segs)return true; } +spin_lock_irqsave (&zwplug->lock, flags); +/* * If we got a zone write plug marked as dead, then the user is issuing * writes to a full zone, or without synchronizing with zone reset or
@@ -2045,7 +2041,6 @@ static int blk_revalidate_seq_zone(struct blk_zone *zone, unsigned int idx,struct gendisk *disk = args->disk;struct blk_zone_wplug *zwplug;unsigned int wp_offset; -unsigned long flags;/* * Remember the capacity of the first sequential zone and check
@@ -2075,10 +2070,9 @@ static int blk_revalidate_seq_zone(struct blk_zone *zone, unsigned int idx,if (!wp_offset || wp_offset >= zone->capacity)return 0 ; - zwplug =disk_get_and_lock_zone_wplug (disk, zone->wp,GFP_NOIO , &flags); + zwplug =disk_get_or_alloc_zone_wplug (disk, zone->wp,GFP_NOIO );if (!zwplug)return -ENOMEM ; -spin_unlock_irqrestore (&zwplug->lock, flags);disk_put_zone_wplug (zwplug);return 0 ;
Signature · 1 line
2.53.0
Re: [PATCH v4 3/8] block: rename and simplify disk_get_and_lock_zone_wplug()
Damien Le Moal
maintainerPATCH v4 4/8 block: remove disk_zone_is_full()
In reply to Damien Le MoalThe helper function disk_zone_is_full() is only used in disk_zone_wplug_is_full(). So remove it and open code it directly in this single caller.
--- block/blk-zoned.c | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-)
block/blk-zoned.c +3 -9
@@ -492,18 +492,12 @@ static bool disk_zone_is_last(struct gendisk *disk, struct blk_zone *zone)return zone->start + zone->len >=get_capacity (disk); } -static bool disk_zone_is_full (struct gendisk *disk, -unsigned int zno,unsigned int offset_in_zone) -{ -if (zno < disk->nr_zones -1 ) -return offset_in_zone >= disk->zone_capacity; -return offset_in_zone >= disk->last_zone_capacity; -} -static bool disk_zone_wplug_is_full (struct gendisk *disk,struct blk_zone_wplug *zwplug) { -return disk_zone_is_full (disk, zwplug->zone_no, zwplug->wp_offset); +if (zwplug->zone_no < disk->nr_zones -1 ) +return zwplug->wp_offset >= disk->zone_capacity; +return zwplug->wp_offset >= disk->last_zone_capacity; } static bool disk_insert_zone_wplug(struct gendisk *disk,
Signature · 1 line
2.53.0
Damien Le Moal
maintainerPATCH v4 5/8 block: rename struct gendisk zone_wplugs_lock field
In reply to Damien Le MoalRename struct gendisk zone_wplugs_lock field to zone_wplugs_hash_lock to clearly indicates that this is the spinlock used for manipulating the hash table of zone write plugs.
--- block/blk-zoned.c | 23 ++++++++++++----------- include/linux/blkdev.h | 2 +- 2 files changed, 13 insertions(+), 12 deletions(-)
block/blk-zoned.c +12 -11
@@ -514,10 +514,11 @@ static bool disk_insert_zone_wplug(struct gendisk *disk,* are racing with other submission context, so we may already have a * zone write plug for the same zone. */ -spin_lock_irqsave (&disk->zone_wplugs_lock, flags); +spin_lock_irqsave (&disk->zone_wplugs_hash_lock, flags);hlist_for_each_entry_rcu (zwplg, &disk->zone_wplugs_hash[idx], node) {if (zwplg->zone_no == zwplug->zone_no) { -spin_unlock_irqrestore (&disk->zone_wplugs_lock, flags); +spin_unlock_irqrestore (&disk->zone_wplugs_hash_lock, + flags);return false; } }
@@ -529,7 +530,7 @@ static bool disk_insert_zone_wplug(struct gendisk *disk,* necessarilly in the active condition. */ zones_cond =rcu_dereference_check (disk->zones_cond, -lockdep_is_held (&disk->zone_wplugs_lock)); +lockdep_is_held (&disk->zone_wplugs_hash_lock));if (zones_cond) zwplug->cond = zones_cond[zwplug->zone_no];else
@@ -537,7 +538,7 @@ static bool disk_insert_zone_wplug(struct gendisk *disk,hlist_add_head_rcu (&zwplug->node, &disk->zone_wplugs_hash[idx]);atomic_inc (&disk->nr_zone_wplugs); -spin_unlock_irqrestore (&disk->zone_wplugs_lock, flags); +spin_unlock_irqrestore (&disk->zone_wplugs_hash_lock, flags);return true; }
@@ -590,13 +591,13 @@ static void disk_free_zone_wplug(struct blk_zone_wplug *zwplug)WARN_ON_ONCE (zwplug->flags &BLK_ZONE_WPLUG_PLUGGED );WARN_ON_ONCE (!bio_list_empty (&zwplug->bio_list)); -spin_lock_irqsave (&disk->zone_wplugs_lock, flags); +spin_lock_irqsave (&disk->zone_wplugs_hash_lock, flags);blk_zone_set_cond (rcu_dereference_check (disk->zones_cond, -lockdep_is_held (&disk->zone_wplugs_lock)), +lockdep_is_held (&disk->zone_wplugs_hash_lock)), zwplug->zone_no, zwplug->cond);hlist_del_init_rcu (&zwplug->node);atomic_dec (&disk->nr_zone_wplugs); -spin_unlock_irqrestore (&disk->zone_wplugs_lock, flags); +spin_unlock_irqrestore (&disk->zone_wplugs_hash_lock, flags);call_rcu (&zwplug->rcu_head, disk_free_zone_wplug_rcu); }
@@ -1739,7 +1740,7 @@ static void blk_zone_wplug_bio_work(struct work_struct *work)void disk_init_zone_resources (struct gendisk *disk) { -spin_lock_init (&disk->zone_wplugs_lock); +spin_lock_init (&disk->zone_wplugs_hash_lock); }/*
@@ -1829,10 +1830,10 @@ static void disk_set_zones_cond_array(struct gendisk *disk, u8 *zones_cond) {unsigned long flags; -spin_lock_irqsave (&disk->zone_wplugs_lock, flags); +spin_lock_irqsave (&disk->zone_wplugs_hash_lock, flags); zones_cond =rcu_replace_pointer (disk->zones_cond, zones_cond, -lockdep_is_held (&disk->zone_wplugs_lock)); -spin_unlock_irqrestore (&disk->zone_wplugs_lock, flags); +lockdep_is_held (&disk->zone_wplugs_hash_lock)); +spin_unlock_irqrestore (&disk->zone_wplugs_hash_lock, flags);kfree_rcu_mightsleep (zones_cond); }
include/linux/blkdev.h +1 -1
@@ -200,7 +200,7 @@ struct gendisk {u8 __rcu *zones_cond;unsigned int zone_wplugs_hash_bits;atomic_t nr_zone_wplugs; -spinlock_t zone_wplugs_lock; +spinlock_t zone_wplugs_hash_lock;struct mempool *zone_wplugs_pool;struct hlist_head *zone_wplugs_hash;struct workqueue_struct *zone_wplugs_wq;
Signature · 1 line
2.53.0
Damien Le Moal
maintainerPATCH v4 6/8 block: allow submitting all zone writes from a single context
In reply to Damien Le MoalIn order to maintain sequential write patterns per zone with zoned block devices, zone write plugging issues only a single write BIO per zone at any time. This works well but has the side effect that when large sequential write streams are issued by the user and these streams cross zone boundaries, the device ends up receiving a discontiguous set of write commands for different zones. The same also happens when a user writes simultaneously at high queue depth multiple zones: the device does not see all sequential writes per zone and receives discontiguous writes to different zones. While this does not affect the performance of solid state zoned block devices, when using an SMR HDD, this pattern change from sequential writes to discontiguous writes to different zones significantly increases head seek which results in degraded write throughput.
In order to reduce this seek overhead for rotational media devices, introduce a per disk zone write plugs kernel thread to issue all write BIOs to zones. This single zone write issuing context is enabled for any zoned block device that has a request queue flagged with the new QUEUE_ZONED_QD1_WRITES flag.
The flag QUEUE_ZONED_QD1_WRITES is visible as the sysfs queue attribute zoned_qd1_writes for zoned devices. For regular block devices, this attribute is not visible. For zoned block devices, a user can override the default value set to force the global write maximum queue depth of 1 for a zoned block device, or clear this attribute to fallback to the default behavior of zone write plugging which limits writes to QD=1 per sequential zone.
Writing to a zoned block device flagged with QUEUE_ZONED_QD1_WRITES is implemented using a list of zone write plugs that have a non-empty BIO list. Listed zone write plugs are processed by the disk zone write plugs worker kthread in FIFO order, and all BIOs of a zone write plug are all processed before switching to the next listed zone write plug. A newly submitted BIO for a non-FULL zone write plug that is not yet listed causes the addition of the zone write plug at the end of the disk list of zone write plugs.
Since the write BIOs queued in a zone write plug BIO list are necessarilly sequential, for rotational media, using the single zone write plugs kthread to issue all BIOs maintains a sequential write pattern and thus reduces seek overhead and improves write throughput. This processing essentially result in always writing to HDDs at QD=1, which is not an issue for HDDs operating with write caching enabled. Performance with write cache disabled is also not degraded thanks to the efficient write handling of modern SMR HDDs.
A disk list of zone write plugs is defined using the new struct gendisk zone_wplugs_list, and accesses to this list is protected using the zone_wplugs_list_lock spinlock. The per disk kthread (zone_wplugs_worker) code is implemented by the function disk_zone_wplugs_worker(). A reference on listed zone write plugs is always held until all BIOs of the zone write plug are processed by the worker kthread. BIO issuing at QD=1 is driven using a completion structure (zone_wplugs_worker_bio_done) and calls to blk_io_wait().
With this change, performance when sequentially writing the zones of a 30 TB SMR SATA HDD connected to an AHCI adapter changes as follows (1MiB direct I/Os, results in MB/s unit):
+--------------------+ | Write BW (MB/s) | +------------------+----------+---------+ | Sequential write | Baseline | Patched | | Queue Depth | 6.19-rc8 | | +------------------+----------+---------+ | 1 | 244 | 245 | | 2 | 244 | 245 | | 4 | 245 | 245 | | 8 | 242 | 245 | | 16 | 222 | 246 | | 32 | 211 | 245 | | 64 | 193 | 244 | | 128 | 112 | 246 | +------------------+----------+---------+
With the current code (baseline), as the sequential write stream crosses a zone boundary, higher queue depth creates a gap between the last IO to the previous zone and the first IOs to the following zones, causing head seeks and degrading performance. Using the disk zone write plugs worker thread, this pattern disappears and the maximum throughput of the drive is maintained, leading to over 100% improvements in throughput for high queue depth write.
Using 16 fio jobs all writing to randomly chosen zones at QD=32 with 1 MiB direct IOs, write throughput also increases significantly.
+--------------------+ | Write BW (MB/s) | +------------------+----------+---------+ | Random write | Baseline | Patched | | Number of zones | 6.19-rc7 | | +------------------+----------+---------+ | 1 | 191 | 192 | | 2 | 101 | 128 | | 4 | 115 | 123 | | 8 | 90 | 120 | | 16 | 64 | 115 | | 32 | 58 | 105 | | 64 | 56 | 101 | | 128 | 55 | 99 | +------------------+----------+---------+
Tests using XFS shows that buffered write speed with 8 jobs writing files increases by 12% to 35% depending on the workload.
+--------------------+ | Write BW (MB/s) | +------------------+----------+---------+ | Workload | Baseline | Patched | | | 6.19-rc7 | | +------------------+----------+---------+ | 256MiB file size | 212 | 238 | +------------------+----------+---------+ | 4MiB .. 128 MiB | 213 | 243 | | random file size | | | +------------------+----------+---------+ | 2MiB .. 8 MiB | 179 | 242 | | random file size | | | +------------------+----------+---------+
Performance gains are even more significant when using an HBA that limits the maximum size of commands to a small value, e.g. HBAs controlled with the mpi3mr driver limit commands to a maximum of 1 MiB. In such case, the write throughput gains are over 40%.
+--------------------+ | Write BW (MB/s) | +------------------+----------+---------+ | Workload | Baseline | Patched | | | 6.19-rc7 | | +------------------+----------+---------+ | 256MiB file size | 175 | 245 | +------------------+----------+---------+ | 4MiB .. 128 MiB | 174 | 244 | | random file size | | | +------------------+----------+---------+ | 2MiB .. 8 MiB | 171 | 243 | | random file size | | | +------------------+----------+---------+
--- block/blk-mq-debugfs.c | 1 + block/blk-sysfs.c | 35 +++++++- block/blk-zoned.c | 190 ++++++++++++++++++++++++++++++++++++----- include/linux/blkdev.h | 8 ++ 4 files changed, 212 insertions(+), 22 deletions(-)
block/blk-mq-debugfs.c +1 -0
@@ -97,6 +97,7 @@ static const char *const blk_queue_flag_name[] = {QUEUE_FLAG_NAME (NO_ELV_SWITCH ),QUEUE_FLAG_NAME (QOS_ENABLED ),QUEUE_FLAG_NAME (BIO_ISSUE_TIME ), + QUEUE_FLAG_NAME(ZONED_QD1_WRITES), };#undef QUEUE_FLAG_NAME
block/blk-sysfs.c +34 -1
@@ -384,6 +384,36 @@ static ssize_t queue_nr_zones_show(struct gendisk *disk, char *page)return queue_var_show (disk_nr_zones (disk), page); } +static ssize_t queue_zoned_qd1_writes_show (struct gendisk *disk,char *page) +{ +return queue_var_show (!!blk_queue_zoned_qd1_writes (disk->queue), + page); +} + +static ssize_t queue_zoned_qd1_writes_store (struct gendisk *disk, +const char *page,size_t count) +{ +struct request_queue *q = disk->queue; +unsigned long qd1_writes; +unsigned int memflags; +ssize_t ret; + + ret =queue_var_store (&qd1_writes, page, count); +if (ret <0 ) +return ret; + + memflags =blk_mq_freeze_queue (q); +blk_mq_quiesce_queue (q); +if (qd1_writes) +blk_queue_flag_set (QUEUE_FLAG_ZONED_QD1_WRITES , q); +else +blk_queue_flag_clear (QUEUE_FLAG_ZONED_QD1_WRITES , q); +blk_mq_unquiesce_queue (q); +blk_mq_unfreeze_queue (q, memflags); + +return count; +} +static ssize_t queue_iostats_passthrough_show (struct gendisk *disk,char *page) {return queue_var_show (!!blk_queue_passthrough_stat (disk->queue), page);
@@ -611,6 +641,7 @@ QUEUE_LIM_RO_ENTRY(queue_max_zone_append_sectors, "zone_append_max_bytes");QUEUE_LIM_RO_ENTRY (queue_zone_write_granularity,"zone_write_granularity" );QUEUE_LIM_RO_ENTRY (queue_zoned,"zoned" ); +QUEUE_RW_ENTRY (queue_zoned_qd1_writes,"zoned_qd1_writes" );QUEUE_RO_ENTRY (queue_nr_zones,"nr_zones" );QUEUE_LIM_RO_ENTRY (queue_max_open_zones,"max_open_zones" );QUEUE_LIM_RO_ENTRY (queue_max_active_zones,"max_active_zones" );
@@ -748,6 +779,7 @@ static struct attribute *queue_attrs[] = { &queue_nomerges_entry.attr, &queue_poll_entry.attr, &queue_poll_delay_entry.attr, + &queue_zoned_qd1_writes_entry.attr, NULL, };
@@ -780,7 +812,8 @@ static umode_t queue_attr_visible(struct kobject *kobj, struct attribute *attr,struct request_queue *q = disk->queue;if ((attr == &queue_max_open_zones_entry.attr || - attr == &queue_max_active_zones_entry.attr) && + attr == &queue_max_active_zones_entry.attr || + attr == &queue_zoned_qd1_writes_entry.attr) && !blk_queue_is_zoned (q))return 0 ;
block/blk-zoned.c +169 -21
333 lines not on this page · raw message
include/linux/blkdev.h +8 -0
@@ -13,6 +13,7 @@#include <linux/minmax.h> #include <linux/timer.h> #include <linux/workqueue.h> +#include <linux/completion.h> #include <linux/wait.h> #include <linux/bio.h> #include <linux/gfp.h>
@@ -204,6 +205,10 @@ struct gendisk {struct mempool *zone_wplugs_pool;struct hlist_head *zone_wplugs_hash;struct workqueue_struct *zone_wplugs_wq; +spinlock_t zone_wplugs_list_lock; +struct list_head zone_wplugs_list; +struct task_struct *zone_wplugs_worker; +struct completion zone_wplugs_worker_bio_done;#endif /* CONFIG_BLK_DEV_ZONED */ #if IS_ENABLED (CONFIG_CDROM )
@@ -668,6 +673,7 @@ enum {QUEUE_FLAG_NO_ELV_SWITCH ,/* can't switch elevator any more */ QUEUE_FLAG_QOS_ENABLED ,/* qos is enabled */ QUEUE_FLAG_BIO_ISSUE_TIME ,/* record bio->issue_time_ns */ +QUEUE_FLAG_ZONED_QD1_WRITES ,/* Limit zoned devices writes to QD=1 */ QUEUE_FLAG_MAX };
@@ -707,6 +713,8 @@ void blk_queue_flag_clear(unsigned int flag, struct request_queue *q); test_bit(QUEUE_FLAG_DISABLE_WBT_DEF, &(q)->queue_flags)#define blk_queue_no_elv_switch (q) \ test_bit(QUEUE_FLAG_NO_ELV_SWITCH, &(q)->queue_flags) +#define blk_queue_zoned_qd1_writes (q) \ + test_bit(QUEUE_FLAG_ZONED_QD1_WRITES, &(q)->queue_flags)extern void blk_set_pm_only (struct request_queue *q);extern void blk_clear_pm_only (struct request_queue *q);
Signature · 1 line
2.53.0
Re: [PATCH v4 6/8] block: allow submitting all zone writes from a single context
Looks good,
Damien Le Moal
maintainerPATCH v4 7/8 block: default to QD=1 writes for blk-mq rotational zoned devices
In reply to Damien Le MoalFor blk-mq rotational zoned block devices (e.g. SMR HDDs), default to having zone write plugging limit write operations to a maximum queue depth of 1 for all zones. This significantly reduce write seek overhead and improves SMR HDD write throughput.
For remotely connected disks with a very high network latency this features might not be useful. However, remotely connected zoned devices are rare at the moment, and we cannot know the round trip latency to pick a good default for network attached devices. System administrators can however disable this feature in that case.
For BIO based (non blk-mq) rotational zoned block devices, the device driver (e.g. a DM target driver) can directly set an appropriate default.
--- block/blk-sysfs.c | 8 ++++++++ 1 file changed, 8 insertions(+)
block/blk-sysfs.c +8 -0
@@ -961,6 +961,14 @@ int blk_register_queue(struct gendisk *disk)blk_mq_debugfs_register (q);blk_debugfs_unlock (q, memflags); +/* +* For blk-mq rotational zoned devices, default to using QD=1 +* writes. For non-mq rotational zoned devices, the device driver can +* set an appropriate default. +*/ +if (queue_is_mq (q) &&blk_queue_rot (q) &&blk_queue_is_zoned (q)) +blk_queue_flag_set (QUEUE_FLAG_ZONED_QD1_WRITES , q); + ret =disk_register_independent_access_ranges (disk);if (ret) goto out_debugfs_remove;
Signature · 1 line
2.53.0
Damien Le Moal
maintainerPATCH v4 8/8 Documentation: ABI: stable: document the zoned_qd1_writes attribute
In reply to Damien Le MoalUpdate the documentation file Documentation/ABI/stable/sysfs-block to describe the zoned_qd1_writes sysfs queue attribute file.
--- Documentation/ABI/stable/sysfs-block | 15 +++++++++++++++ 1 file changed, 15 insertions(+)
Documentation/ABI/stable/sysfs-block +15 -0
@@ -886,6 +886,21 @@ Description: zone commands, they will be treated as regular block devices and zoned will report "none". +What: /sys/block/<disk>/queue/zoned_qd1_writes +Date: January 2026 +Contact: Damien Le Moal <[email protected]> +Description: + [RW] zoned_qd1_writes indicates if write operations to a zoned + block device are being handled using a single issuer context (a + kernel thread) operating at a maximum queue depth of 1. This + attribute is visible only for zoned block devices. The default + value for zoned block devices that are not rotational devices + (e.g. ZNS SSDs or zoned UFS devices) is 0. For rotational zoned + block devices (e.g. SMR HDDs) the default value is 1. Since + this default may not be appropriate for some devices, e.g. + remotely connected devices over high latency networks, the user + can disable this feature by setting this attribute to 0. + What: /sys/block/<disk>/hidden Date: March 2023
Signature · 1 line
2.53.0
Quoted 10 lines · Damien Le Moal
Jens,
This patch series cleans up the zone write plugging code and introduces the ability to issue all write BIOs from a single context (a kthread) instead of allowing multiple zones to be written at the same time using a per zone work. As shown in patch 6, raw block device tests and XFS tests with an SMR HDD show that this can significantly increase write throughput (up to 40% over the current zone write plugging).
[...]
Applied, thanks!
[1/8] block: fix zone write plug removal
commit: 6ff36afa5eaf22db6c7a8b43d7bc7fb55efb8822
[2/8] block: fix zone write plugs refcount handling in disk_zone_wplug_schedule_bio_work()
commit: fe2696d207b4d8da382fa6239a4689b68fcc0cc7
[3/8] block: rename and simplify disk_get_and_lock_zone_wplug()
commit: 9fe9d5db6e28211eb1e93be584ee538cef8ad242
[4/8] block: remove disk_zone_is_full()
commit: 56d54df82aaef511d7f6d716f761202f67e3dd05
[5/8] block: rename struct gendisk zone_wplugs_lock field
commit: e05624be8150a73a25cf8612013b2847c0a38459
[6/8] block: allow submitting all zone writes from a single context
commit: 2f0d2401a412ce84929c369240c1f900262bc96d
[7/8] block: default to QD=1 writes for blk-mq rotational zoned devices
commit: 904100e435d1eb71dfb3d9ee5ac6ed8c0b7791af
[8/8] Documentation: ABI: stable: document the zoned_qd1_writes attribute
commit: b154532ca0f25ba89bb3494dc0ae1a8f17bd83ccBest regards,
Signature · 1 line
Jens Axboe