天天看點

Block Throttle

block throttle 是 block QoS 的重要組成部分,也是最早的一個 QoS,其功能是限制每個 cgroup 的 IOPS/BPS 上限。

Group Hiererchy

每個 blkdev 會為每個 block group 建立一個對應的 throttle group

Block Throttle

throttle data

每個 blkdev 對應一個 throttle data,儲存該 blkdev 的 blk-throttle policy 相關的資訊

struct throtl_data           

系統初始化過程中,每次注冊一個 blkdev 時,會建立對應的 throttle data

blk_alloc_queue
    blkcg_init_queue
        blk_throtl_init(q)
            # allocate throttle data           

throttle group

類似地,每個 blkcg_gq 即 (cgroup, request queue) pair 具有一個對應的 throttle group,儲存在 blkcg_gq 的 @pd[] 數組

block group 層定義有一系列的 policy,例如 blk-throttle/iolatency/iocost 等,這裡每個 policy 都會配置設定 policy specific data,這些 policy specific data 都會内嵌 struct blkg_policy_data,同時最終都儲存在 @pd[] 數組

struct blkcg_gq {
    struct blkg_policy_data     *pd[BLKCG_MAX_POLS];
    ...
};           

blk-throttle 的 specific data 就是 struct throtl_grp

struct throtl_grp {
    /* must be the first member */
    struct blkg_policy_data pd;
    ...
};           
  • root cgroup
系統初始化過程中,每次注冊一個 blkdev 時,會建立對應的 blkcg_gq 結構,使得該 blkdev 與 root block group 建立聯系,這一過程中會依次調用各個 policy 的 pd_alloc_fn() 回調函數,建立 policy specific data

對于 blk-throttle 來說,此時就會建立該 blkcg_gq 對應的 throttle group

blk_alloc_queue
    blkcg_init_queue
        blkg_alloc
            # allocate blkcg_gq (@blkg)
            (for each policy)
                blkg->pd[...] = pol->pd_alloc_fn(), e.g., throtl_pd_alloc() // allocate throttle group           
  • child cgroup
child block group 對應的 blkcg_gq 結構,則是在 child block group 下發 bio 的時候延遲建立的,此時會建立目前下發的 bio 的 blkdev 對應的 blkcg_gq 結構
bio_set_dev
    bio_associate_blkg
        css = blkcg_css() // get css of current task
        bio_associate_blkg_from_css
            blkg_tryget_closest
                blkg_lookup_create(css_to_blkcg(css), queue)
                    blkg_create
                        blkg_alloc
                            # allocate blkcg_gq (@blkg)
                            (for each policy)
                                blkg->pd[...] = pol->pd_alloc_fn(), e.g., throtl_pd_alloc() // allocate throttle group
                            
                        (for each policy)
                            blkg->pd[...] = pol->pd_init_fn(), e.g., throtl_pd_init()           

Throttle Routine

throttle limit

使用者可以配置 throttle group 的參數上限,這些參數儲存在 @iops[]/@bps[] 數組

struct throtl_grp {
    uint64_t bps[2][LIMIT_CNT]; /* internally used bps limits */
    unsigned int iops[2][LIMIT_CNT]; /* internally used IOPS limits */
    ...
}           

@bps2 描述該 throttle group 的 read/write BPS 限制

@iops2 描述該 throttle group 的 read/write IOPS 限制

throttle check

throttle policy 是按照時間片 (time slice) 為機關對 IO 進行限流的,在一個時間片以内,下發的 IO 流量超過這個時間片對應的配額,才會觸發限流;當下一個時間片來臨的時候,配額會重新重新整理

@slice_start[rw]                @slice_end[rw]
--------+-------------------------------+--------
                      ^
                current jiffies           
  1. usage in slice

首先每個 throttle group 需要統計一個時間片内截止目前為止,該 block group 對該 block device 的使用量

struct throtl_grp {
    /* When did we start a new slice */
    unsigned long slice_start[2];
    unsigned long slice_end[2];

    /* Number of bytes disptached in current slice */
    uint64_t bytes_disp[2];
    /* Number of bio's dispatched in current slice */
    unsigned int io_disp[2];
    ...
}           

在 (@slice_start[READ], @slice_end[READ]) 時間段内,資料讀取的總量為 @bytes_disp[READ] 位元組,處理的讀 IO 的數量為 @io_disp[READ]

在 (@slice_start[WRITE], @slice_end[WRITE]) 時間段内,資料寫入的總量為 @bytes_disp[WRITE] 位元組,處理的寫 IO 的數量為 @io_disp[WRITE]

@slice_start[] 與 @slice_end[] 的間隔為 @throtl_data->throtl_slice,一般預設為 HZ/10 即 100 ms

submit_bio
    submit_bio_noacct
        submit_bio_checks
            blk_throtl_bio
                tg_may_dispatch
                    # check if within limit
                
                # if pass the thottle check
                throtl_charge_bio
                        @bytes_disp[rw] += bio_size;
                        @io_disp[rw]++;           
  1. quota in slice

之後需要計算目前這個時間片内,目前為止可用的配額,即 (jiffies - @slice_start[rw]) / HZ * limit

  1. check

那麼 IO 下發過程中,如果目前時間片内的使用量 (iops/bps) 超過了目前時間片内的可用的配額,那麼就會觸發限流操作

throttle

上述介紹到,IO 下發過程中可能會觸發限流操作,此時目前下發的 IO 就會暫時緩存在目前 throttle group 中,之後恢複配額時會重新送出這些緩存的 IO,此時相關資料結構的層次如圖所示

Block Throttle
throttle service queue

每個 throttle group 維護有一個 throttle service queue,該 throttle group 下被限流的 bio 就組織在該 throttle service queue 中

struct throtl_grp {
    /* this group's service queue */
    struct throtl_service_queue service_queue;
    ...
}           

值得注意的是,per blkdev 的 throttle data 也維護有一個 throttle service queue

struct throtl_data {
    /* service tree for active throtl groups */
    struct throtl_service_queue service_queue;
    ...
}           

這些 throttle service queue 通過 @parent_sq 字段組成一個樹狀關系

struct throtl_service_queue {
    struct throtl_service_queue *parent_sq; /* the parent service_queue */
    ...
};           

這裡需要注意的是,所有 throttle group 的 @parent_sq 字段都指向 throttle data 對應的 service queue

service queue of throttle data
                            +-------+
                            |       |
                            +-------+
                                ^
                                | @parent_sq
        +-----------------------+
        |                       |
service queue               service queue
of throttle group           of child throttle group
    +-------+               +-------+
    |       |               |       |
    +-------+               +-------+
           
pending bio list

被限流的 bio 就暫時緩存在其所在的 throttle group 對應的 service queue 的 @queued[rw] 連結清單中

但是需要注意的是,被限流的 bio 并不是直接緩存在 @queued[rw] 連結清單中,而是緩存在 qnode 的 @bios 連結清單中,@queued[rw] 連結清單組織所有的 qnode

struct throtl_qnode {
    struct bio_list        bios;     /* queued bios */
    ...
};           

qnode 概念的提出,是為了解決在 dispatch 階段,各個 throttle group 能公平配置設定配額的問題。其實在 qnode 概念提出來之前,限流的 bio 就是直接緩存到 @queued[rw] 連結清單的

需要注意的是,目前 throttle group 的 @queued[rw] 連結清單中緩存的 bio,有可能是來自目前 throttle group 的,也有可能是來自 child throttle group 的,甚至是 grandchild throttle group 的。這需要了解 throttle check 檢查的過程,其中首先檢查所在的 throttle group 的配額,如果目前所在的 throttle group 的配額已經用盡了,即該 bio 被限流在目前 throttle group,那麼該 bio 就會緩存在目前 throttle group 的 @queued[rw] 連結清單中;而如果目前 throttle group 的配額還非常充足,此時會向上一層,檢查 parent throttle group 的配額,如果 parent throttle group 的配額用盡,那麼該 bio 就會被限流在 parent throttle group,此時該 bio 就會緩存在 parent throttle group 的 @queued[rw] 連結清單中

此時一個 throttle group 的 @queued[rw] 連結清單中緩存的 bio,既有來自目前 throttle group 的,也有來自 child throttle group、grandchild throttle group ... 之後當這個 throttle group 的配額恢複、進入 dispatch 階段的時候,往往隻能按照順序依次對 @queued[rw] 連結清單中緩存的 bio 進行 dispatch 操作;此時試想某一時刻某一個 child throttle group 大量下發 bio 觸發限流操作,緩存在其 parent throttle group 的 @queued[rw] 連結清單中,那麼之後該 parent throttle group 進入 dispatch 階段,此時大部分的配額配置設定給了之前大量下發 IO 的那個 child throttle group,而其他 child/grandchild throttle group 就存在餓死的風險

為了解決上述的公平配置設定配額的問題,就引入了 qnode 的概念。每個 throttle group 對應一個 qnode,在下發 IO 過程中,

  • 如果目前 throttle group 配額用盡,導緻 bio 被限流在目前 throttle group 時,會使用 @qnode_on_self[rw] 這一套 qnode,此時被限流的 bio 緩存到 @qnode_on_self[rw]->bios 連結清單,同時 @qnode_on_self[rw] 被添加到目前 throttle group 的 @queued[rw] 連結清單
  • 如果目前 throttle group 配額還很充足,那麼會向上檢查其 parent throttle group 的配額,如果 parent throttle group 的配額用盡,導緻 bio 被限流在 parent throttle group 時,會使用 @qnode_on_parent[rw] 這一套 qnode,此時被限流的 bio 緩存到 @qnode_on_parent[rw]->bios 連結清單,同時 @qnode_on_parent[rw] 被添加到其 parent throttle group 的 @queued[rw] 連結清單
struct throtl_grp {
    /*
     * qnode_on_self is used when bios are directly queued to this
     * throtl_grp so that local bios compete fairly with bios
     * dispatched from children.  qnode_on_parent is used when bios are
     * dispatched from this throtl_grp into its parent and will compete
     * with the sibling qnode_on_parents and the parent's
     * qnode_on_self.
     */
    struct throtl_qnode qnode_on_self[2];
    struct throtl_qnode qnode_on_parent[2];
    ...
}           
submit_bio
    submit_bio_noacct
        submit_bio_checks
            blk_throtl_bio
                tg_may_dispatch // check
                
                # if need to be throttled
                throtl_add_bio_tg
                    throtl_qnode_add_bio // add bio to qnode's @bios list
                                         // add qnode to throttle group's @queued[rw] list
                    sq->nr_queued[rw]++           
pending throttle group rbtree

此外所有包含有 pending bio 的 throttle group 會組織成一棵 rbtree,儲存在 throttle data 對應的 service queue 的 @pending_tree rbtree 中

需要注意的是,隻有存在 pending bio 需要處理的 throttle group 才會進入 @pending_tree rbtree 中

struct throtl_service_queue {
    /*
     * RB tree of active children throtl_grp's, which are sorted by
     * their ->disptime.
     */
    struct rb_root        pending_tree; /* RB tree of active tgs */
    ...
};           

這棵 rbtree 的 value 是 throttle group,而 key 是對應 throttle group 的 dispatch time,dispatch time 描述該 throttle group 中最近一個可以執行 dispatch 操作的 pending bio 的時間

dispatch time of bio

何謂 dispatch time?之前介紹到,throttle policy 是按照時間片 (time slice) 為機關對 IO 進行限流的,每個 throttle group 都記錄了目前一個時間片内,截止到目前為止已經使用了的配額。IO 下發過程中,如果檢查到 throttle group 的配額已經用盡,導緻目前下發的 bio 需要被限流時,那麼根據該 throttle group 配置的 limit 上限、目前時間片内截止到目前為止已經使用了的配額、以及目前被限流的 bio 的大小,就可以計算出将來的某一時刻,throttle group 的配額可以恢複,進而使得該 bio 被重新下發,這一時刻就稱為這個 pending bio 的 dispatch time

dispatch time of throttle group

每個 throttle group 都會維護一個 @disptime 字段,描述該 throttle group 包含的所有 pending bio 中,離目前時刻最近的一個 dispatch time,實際上也就是該 throttle group 下一次被排程的時刻

struct throtl_grp {
    /*
     * Dispatch time in jiffies. This is the estimated time when group
     * will unthrottle and is ready to dispatch more bio. It is used as
     * key to sort active groups in service tree.
     */
    unsigned long disptime;
    ...
}           

每當有一個 bio 被限流進而加入到 throttle group 中時,都會更新 throttle group 的 @disptime 字段

submit_bio
    submit_bio_noacct
        submit_bio_checks
            blk_throtl_bio
                tg_may_dispatch // check
                
                # if need to be throttled, add bio to qnode's @bios list
                throtl_add_bio_tg

                tg_update_disptime
                    # read_wait = time to wait for latest read IO
                    # write_wait = time to wait for latest write IO
                    # min_wait = min(read_wait, write_wait);
                    # tg->disptime = jiffies + min_wait;           
throttle group rbtree

之後就會将該 throttle group 添加到 throttle data 對應的 service queue 的 @pending_tree rbtree 中

@pending_tree rbtree 中的所有 throttle group 按照 @tg->disptime 排序,即 rbtree 中最左邊的 throttle group 的 dispatch time 距離目前時刻最近,也就是下一個将被排程的 throttle group

submit_bio
    submit_bio_noacct
        submit_bio_checks
            blk_throtl_bio
                tg_may_dispatch // check
                
                # if need to be throttled, add bio to qnode's @bios list
                throtl_add_bio_tg
                
                tg_update_disptime
                    # update tg->disptime
                    
                    throtl_enqueue_tg
                        tg_service_queue_add // add throttle group to throttle_data->service_queue's @pending_tree rbtree           
schedule dispatch timer

之後就會排程 dispatch timer 來處理緩存在 throttle group 中的 pending bio

這裡需要注意的是,需要等待一段時間,等待 throttle group 的配額恢複之後,才能排程 dispatch timer;@pending_tree rbtree 最左邊的 throttle group 對應的 @disptime 字段描述了最近一個 throttle group 将被排程的時刻,因而也就是等待這一時刻到來之後,再排程 dispatch timer

submit_bio
    submit_bio_noacct
        submit_bio_checks
            blk_throtl_bio
                tg_may_dispatch // check
                
                # if need to be throttled, add bio to qnode's @bios list
                throtl_add_bio_tg
                
                tg_update_disptime
                    # update tg->disptime
                    
                    throtl_enqueue_tg
                        tg_service_queue_add // add throttle group to throttle_data->service_queue's @pending_tree rbtree
                
                throtl_schedule_next_dispatch
                    # first_pending_disptime = disptime of the latest expiring throttle group
                    throtl_schedule_pending_timer(..., first_pending_disptime)           

dispatch

dispatch timer

dispatch timer 被排程的時候,就會處理 @pending_tree rbtree 中的 throttle group

iterate throttle groups

dispatch timer 會優先處理 @pending_tree rbtree 最左邊的 throttle group,即 dispatch time 最近的一個 throttle group,但是每個 throttle group 單次最多隻能分發 THROTL_GRP_QUANTUM 個 IO (包括 READ IO 和 WRITE IO),之後根據該 throttle group 中剩下的還未分發的 pending bio 更新該 throttle group 的 @disptime 字段,并根據更新後的 @disptime 字段,調整該 throttle group 在 @pending_tree rbtree 中的位置 (此時該 throttle group 往往不再位于 @pending_tree rbtree 的最左邊)

之後 dispatch timer 會在一個循環中重複上述過程,即從 @pending_tree rbtree 的最左邊取出一個 throttle group,分發其中的 pending bio,但同樣最多隻能分發 THROTL_GRP_QUANTUM 個 IO,...

重複以上過程,直到 @pending_tree rbtree 最左邊取出的 throttle group 的 @disptime 在目前時刻之後,才會結束

dispatch one throttle group

上述循環中,在對目前輪到的 throttle group 作 dispatch 操作的過程中實際上是将 @queued[rw] 連結清單中各個 qnode 的 @bios 連結清單中緩存的 pending bio 轉移到目前 throttle group 的 @qnode_on_parent[rw] 中,之後将 @qnode_on_parent[rw] 添加到 throttle data 的 service queue 的 @queued[rw] 連結清單中

也就是說此時尚未真正下發 pending bio,而隻是将這些 pending bio 轉移到 throttle data 的 service queue 的 @queued[rw] 連結清單中,之後會排程 dispatch worker 對這些 pending bio 作真正的下發操作

# throtl_pending_timer
throtl_pending_timer_fn // input @throtl_service_queue is from throtl_data
    throtl_select_dispatch
        # get the leftmost throttle group (@tg) in @pending_tree rbtree
        
        throtl_dispatch_tg(tg)
            # dispatch (75% * THROTL_GRP_QUANTUM) READ IO
                tg_dispatch_one_bio(tg, READ)
                    # get latest expiring qnode from @tg's @queued[READ] list
                    # get latest expiring bio from qnode
                    # add bio to current throttle group's qnode_on_parent[rw] list
                    # current throttle group's qnode_on_parent[rw] to throtl_data's throtl_service_queue's @queued[rw] list
                
            # dispatch (25% * THROTL_GRP_QUANTUM) WRITE IO
            ...
    
    queue_work(kthrotld_workqueue, &td->dispatch_work) // schedule @kthrotld_workqueue worker           

每個 throttle group 能夠處理的 pending bio 的數量存在一個上限,即 THROTL_GRP_QUANTUM,其中會優先處理 READ IO,但是單次能夠處理的 READ IO 也隻能占目前能夠處理的 THROTL_GRP_QUANTUM 的 75%

這裡需要注意的是,在處理單個 throttle group 的過程中,每次都是從 @queued[rw] 連結清單的頭部取出一個 qnode,再從該 qnode 的 @bios 連結清單的頭部取出一個 pending bio 進行處理,之後就會将該 qnode 轉移到 @queued[rw] 連結清單的尾部;之後再從 @queued[rw] 連結清單的頭部取出下一個 qnode,循環往複

這一行為正是當初引入 qnode 的意義所在,即所有 qnode (即所有 child/grandchild throttle group) 公平地配置設定目前 throttle group 的配額,防止其中的某個 child/grandchild throttle group 存在餓死的風險

dispatch worker

上述介紹到,dispatch timer 隻是将 pending bio 轉移到 throttle data 的 service queue 的 @queued[rw] 連結清單中,尚未進行真正的下發,之後排程的 dispatch worker 會對這些 pending bio 作真正的下發操作

每個 block device 維護一個 @dispatch_work,當該 block device 下存在 pending bio 需要下發時,就會排程 worker thread 進行處理

worker thread 隻是依次将緩存在 throttle data 的 service queue 的 @queued[rw] 連結清單中的 pending bio,下發給 block layer 進行處理

# @kthrotld_workqueue worker
blk_throtl_dispatch_work_fn
    # for each qnode on throtl_data's throtl_service_queue's @queued[rw] list
        # for each bio in the qnode
            submit_bio_noacct(bio)           

slice management

總的來說,slice 是一個動态移動的過程

  • bio 下發過程中做 limit 檢查的時候,@slice_end[rw] 會向後移,即 extend slice 操作
  • 檢查通過 bio 成功下發,即 dispatch 階段,@slice_start[rw] 會向後移,即 trim slice 操作
@slice_start[rw]                @slice_end[rw]
--------+-------------------------------+--------           
start new slice

向 throttle group 發送第一個 bio,或者這個 throttle group 在發生限流、之後發送完所有積壓的 bio 之後再重新發送一個 bio 時,此時這個 throttle group 是空的,即目前沒有 bio 在該 throttle group 中等待,同時目前的 slice 也已經過時了,那麼此時就會新開一個 slice

@slice_start[rw]                @slice_end[rw]
--------+-------------------------------+--------
        ^
current jiffies           
submit_bio
    submit_bio_noacct
        submit_bio_checks
            blk_throtl_bio
                tg_may_dispatch
                    # if throttle group is empty, and current slice used up, start a new slice
                    throtl_start_new_slice
                            @bytes_disp[rw] = 0;
                            @io_disp[rw] = 0;
                            @slice_start[rw] = jiffies;
                            @slice_end[rw] = jiffies + @td->throtl_slice;           
extend

如果目前的 slice 還沒有過時,但是目前 slice 中剩餘的時間 (即 (@slice_end[rw] - jiffies)) 還不足 @throtl_slice,由于 block throttle 中很多計算都是以 @throtl_slice 為機關的,因而此時就需要擴充目前的 slice,進而確定剩餘時間向上取整為 @throtl_slice 的倍數

@slice_start[rw]                @slice_end[rw]
--------+-------------------------------+--------
                      ^
                current jiffies

@slice_start[rw]                                @slice_end[rw]
--------+-------------------------------*-------------+-----------
                      ^
                current jiffies           

另外如果目前的 slice 已經過時,但是 throttle group 不為空,即目前 throttle group 中還存在等待的 bio,由于這些還在等待的 bio 的 dispatch 操作必須依賴目前 slice 的相關資料,因而此時也還不能新開 slice,因而此時也需要擴充目前的 slice,進而確定剩餘時間向上取整為 @throtl_slice 的倍數

@slice_start[rw]        @slice_end[rw]
--------+--------------------+--------
                                    ^
                            current jiffies

@slice_start[rw]                                        @slice_end[rw]
--------+--------------------*--------------------------------+--------
                                    ^
                            current jiffies           
submit_bio
    submit_bio_noacct
        submit_bio_checks
            blk_throtl_bio
                tg_may_dispatch
                    # if throttle group is not empty, or current slice not used up
                        # if remained time in current slice smaller than @throtl_slice
                        throtl_extend_slice
                            @slice_end[rw] = jiffies + @td->throtl_slice;           
trim slice

之前介紹到,在做 throttle limit 檢查之前會作 extend slice 操作,現在檢查通過即目前 bio 可以直接下發、不需要等待,那麼此時需要将之前擴充的 slice 重新縮減回去

這是因為在 throttle limit 檢查之前做了 extend slice 操作,現在如果不做 trim slice 操作,那麼之後如果 throttle group 重新設定了一個相對很小的 limit,而此時目前這個 slice 的相關資料,主要是 @io_disp[rw]/@bytes_disp[rw],都還是過去 limit 很大時的統計資料,這就會造成修改 limit 之後,新下發的 bio 需要 throttle 等待很長時間才能夠下發

@slice_start[rw]                @slice_end[rw]
--------+-------------------------------+--------
                                ^
                         current jiffies

                @slice_start[rw]                 @slice_end[rw]
--------*-----------------+-------------*-------------+-----------
                                ^
                         current jiffies           

trim slice 操作會将整個 slice 往後移,其中 @slice_start[rw] 會移動到目前的 jiffies 附近,同時也會按照 @slice_start[rw] 變化的幅度,等比例地減小 @io_disp[rw]/@bytes_disp[rw]

submit_bio
    submit_bio_noacct
        submit_bio_checks
            blk_throtl_bio
                tg_may_dispatch // pass
                
                # if pass the thottle check
                throtl_charge_bio
                
                throtl_trim_slice
                    @slice_end[rw] = jiffies + @td->throtl_slice;
                    # move @slice_start[rw] around current jiffies
                    # modify @io_disp[rw]/@bytes_disp[rw] proportionally           

dispatch 的過程中會從 throttle group 中取出等待的 bio,此時會再次調用 tg_may_dispatch() 檢查這個 bio 是否能夠下發,如果在 limit 之内就可以下發,進入 dispatch 階段,否則必須繼續等待

此時類似地在 tg_may_dispatch() 中,需要檢查目前 slice 中剩餘的時間,如果剩餘時間不足 @throtl_slice,就需要擴充目前的 slice,進而確定剩餘時間向上取整為 @throtl_slice 的倍數

# throtl_pending_timer
throtl_pending_timer_fn
    throtl_select_dispatch
        # get the leftmost throttle group (@tg) in @pending_tree rbtree
        
        throtl_dispatch_tg(tg)
            bio = throtl_peek_queued()
            tg_may_dispatch(bio, ...)
                # if remained time in current slice smaller than @throtl_slice
                throtl_extend_slice
                    @slice_end[rw] = jiffies + @td->throtl_slice;                       
trim

dispatch 的過程中如果檢查通過,基于以上類似的原因,在下發之後,需要将之前擴充的 slice 重新縮減回去

# throtl_pending_timer
throtl_pending_timer_fn
    throtl_select_dispatch
        # get the leftmost throttle group (@tg) in @pending_tree rbtree
        
        throtl_dispatch_tg(tg)
            bio = throtl_peek_queued()
            tg_may_dispatch(bio, ...) // pass
            
            tg_dispatch_one_bio
                throtl_charge_bio
                throtl_trim_slice           

example

example 1
Block Throttle

如果目前 throttle group 就被限流,那麼目前下發的 bio 緩存在目前 throttle group 的 @qnode_on_self,同時該 qnode 緩存在目前 throttle group 中

之後 dispatch 階段排程到該 throttle group 的時候,從該 throttle group 的 @queued 連結清單的第一個 qnode 取出一個 bio,将該 bio 轉移到目前 throttle group 的 @qnode_on_parent 中,之後将該 qnode (即 @qnode_on_parent) 轉移到 throttle data 的 @queued 連結清單中

之後排程的 dispatch worker 就會對 throttle data 的 @queued 連結清單中緩存的 pending bio 進行下發

example 2
Block Throttle

如果目前 throttle group 配額充足,那麼就會一層層往上,如果在某一層 parent throttle group 被限流,那麼目前下發的 bio 緩存在其下一層 child throttle group 的 @qnode_on_parent,同時該 qnode 緩存在該 parent throttle group 中 (說明該 qnode 中緩存的 bio 來自目前 throttle group 的下一層 throttle group,而非直接來自目前的 throttle group)

之後 dispatch 階段排程到該 throttle group (緩存有該 bio 的 parent throttle group) 的時候,類似地,從該 throttle group 的 @queued 連結清單的第一個 qnode 取出一個 bio,将該 bio 轉移到目前 throttle group 的 @qnode_on_parent 中,之後将該 qnode (即 @qnode_on_parent) 轉移到 throttle data 的 @queued 連結清單中

example 3
Block Throttle

類似地,如果目前 throttle group 配額充足,那麼就會一層層往上,如果在某一層 parent throttle group 被限流,那麼目前下發的 bio 緩存在其下一層 child throttle group 的 @qnode_on_parent,同時該 qnode 緩存在該 parent throttle group 中

之後 dispatch 階段也是類似地,當排程到該 throttle group 時,從該 throttle group 的 @queued 連結清單的第一個 qnode 取出一個 bio,将該 bio 轉移到目前 throttle group 的 @qnode_on_parent 中

此時如果該 @qnode_on_parent 已經存在于某個 @queued 連結清單 (例如其上一層 parent throttle group 的 @queued 連結清單,或者 throttle data 的 @queued 連結清單) 中,那麼此時不會再移動該 @qnode_on_parent

此時如果該 @qnode_on_parent 存在于其上一層 parent throttle group 的 @queued 連結清單,那麼之後在排程到該 parent throttle group 的時候,類似地,會将該 bio 轉移到目前 throttle group 的 @qnode_on_parent 中,之後将該 qnode (即 @qnode_on_parent) 轉移到 throttle data 的 @queued 連結清單中

Tunable

block group 是一組采用相同 block IO control policy 的程序的集合,在 cgroup filesystem 中,每個 block group 都對應一個目錄,該目錄下包含的配置檔案可以對該 block group 的 block IO control policy 的參數進行配置

采用 Throttling Limit Policy 的 block group 的目錄下,包含以下配置檔案

blkio.throttle.read_bps_device

該配置檔案描述該 block group 對該 block device 的讀操作的速度上限,機關為 bytes/second

echo "<major>:<minor>  <rate_bytes_per_second>" > /cgrp/blkio.throttle.read_bps_device           

例如下例中,該 block group 讀取 major/minor number 8:16 的 block device 時,速度上限為 1MB/s

echo "8:16  1048576" > /sys/fs/cgroup/blkio/blkio.throttle.read_bps_device           

blkio.throttle.write_bps_device

該配置檔案描述該 block group 對該 block device 的寫操作的速度上限,機關為 bytes/second

echo "<major>:<minor>  <rate_bytes_per_second>" > /cgrp/blkio.throttle.write_bps_device           

blkio.throttle.read_iops_device

該配置檔案描述該 block group 對該 block device 的讀操作的速度上限,機關為 bios/second

當同時對某個 block device 的 read_bps_device 與 read_iops_device 進行限制時,該 block device 需要同時受到兩者的限制

echo "<major>:<minor>  <rate_io_per_second>" > /cgrp/blkio.throttle.read_iops_device           

blkio.throttle.write_iops_device

該配置檔案描述該 block group 對該 block device 的寫操作的速度上限,機關為 bios/second

當同時對某個 block device 的 write_bps_device 與 write_iops_device 進行限制時,該 block device 需要同時受到兩者的限制

echo "<major>:<minor>  <rate_io_per_second>" > /cgrp/blkio.throttle.write_iops_device           

Statistics

io_serviced/io_service_bytes

blkio.throttle.io_serviced

throttle.io_service_bytes

描述該 block group 對應的各個 throttle group 已經下發的資料量,其中前者描述下發的 IO 數量,後者描述下發的資料量 (位元組為機關)

一個 block group 可能對多個 blkdev 進行限流配置,此時每個配置的 blkdev 都對應一個 throttle group,因而一個 block group 可以對應多個 throttle group

# cat /sys/fs/cgroup/blkio/blkio.throttle.io_serviced
253:16 Read 380
253:16 Write 158342
253:16 Sync 158590
253:16 Async 132
253:16 Discard 0
253:16 Total 158722
253:0 Read 15390
253:0 Write 60458
253:0 Sync 38000
253:0 Async 37848
253:0 Discard 0
253:0 Total 75848
Total 234570           

讀取 blkio.throttle.io_serviced 和 throttle.io_service_bytes 的時候,就會周遊目前 block cgroup 對應的所有 throttle group,依次輸出各個 throttle group 下發的資料量

每個 throttle group 都會統計自己的資料

struct throtl_grp {
    struct blkg_rwstat stat_bytes;
    struct blkg_rwstat stat_ios;
    ...
}           
struct blkg_rwstat {
    struct percpu_counter    cpu_cnt[BLKG_RWSTAT_NR];
    ...
};           

其中分為兩個次元進行統計

  • 一個是按照 IO 類型統計,分别為 READ/WRITE/DISCARD
  • 一個是按照操作類型統計,分别為 SYNC/ASYNC
BLKG_RWSTAT_READ,
    BLKG_RWSTAT_WRITE,
    BLKG_RWSTAT_SYNC,
    BLKG_RWSTAT_ASYNC,
    BLKG_RWSTAT_DISCARD           

以上兩個次元是完全正交的,即

TOTAL = READ + WRITE + DISCARD = SYNC + ASYNC           

charge when split

對于 bio 發生 split 的場景,目前的邏輯隻會對 split 之前的 original bio 作 charge 操作,因而目前限的都是 split 之前的 iops/bps

submit_bio
    submit_bio_noacct
        submit_bio_checks
            blk_throtl_bio
                if bio_flagged(bio, BIO_THROTTLED): return
                
                # if pass the throttle check
                throtl_charge_bio
                        @bytes_disp[rw] += bio_size;
                        @io_disp[rw]++;
                
                bio_set_flag(bio, BIO_THROTTLED)           
blk_queue_split
    blk_bio_segment_split
        bio_split
            bio_clone_fast
                __bio_clone_fast
                    if (bio_flagged(bio_src, BIO_THROTTLED)
                        bio_set_flag(bio, BIO_THROTTLED);           

通過 throttle 檢查的 original bio 都會打上 BIO_THROTTLED 标記,之後 original bio 發生 split 時,所有 split bio 也都會複制上 BIO_THROTTLED 标記,之後對這些 split bio 作 throttle 檢查的時候,檢查到 BIO_THROTTLED 标記就會直接通過,不會作任何限制

statistics when split

但是在 bio 發生 split 的場景下,io_serviced 和 io_service_bytes 的統計邏輯則存在差異

4.19 版本中,io_serviced 和 io_service_bytes 的統計邏輯為

submit_bio
    generic_make_request
        generic_make_request_checks
            blkcg_bio_issue_check
                blk_throtl_bio
        
                # if blk_throtl_bio() returns false, i.e., not throttled
                if (!bio_flagged(bio, BIO_QUEUE_ENTERED))
                         blkg_rwstat_add(@stat_bytes, bio->bi_iter.bi_size);
                     blkg_rwstat_add(&blkg->stat_ios, 1);           

當發生 split 的時候,split 出來的 bio 會标記為 BIO_QUEUE_ENTERED

original struct bio
+-------------------------------+
|                               |
+-------------------------------+

cloned struct bio      original struct bio
+-------+           +-----------------------+
| split |           |       remain          |
+-------+           +-----------------------+           
blk_queue_split
    bio_set_flag(*bio, BIO_QUEUE_ENTERED)           

也就是說,io_serviced 會重複統計 split bio,而 io_service_bytes 則不會

以 io_serviced 統計為例,

最開始對 original bio 調用 submit_bio() 時,會增加 io_serviced 計數

original struct bio
+-------------------------------+
|                               |
+-------------------------------+           
submit_bio (original bio)
    generic_make_request
        generic_make_request_checks
            blkcg_bio_issue_check
                blk_throtl_bio
                blkg_rwstat_add(&blkg->stat_ios, 1); // count for original bio           

之後 original bio 發生 split 時,會對 split 之後的 remain bio 遞歸調用 generic_make_request(),此時會再次增加 io_serviced 計數

original struct bio
+-------------------------------+
|                               |
+-------------------------------+

cloned struct bio      original struct bio
+-------+           +-----------------------+
| split |           |       remain          |
+-------+           +-----------------------+           
submit_bio (original bio)
    generic_make_request
        q->make_request_fn(), e.g., blk_mq_make_request()
            blk_queue_split
                split = blk_bio_segment_split() // split
                bio_set_flag(*bio, BIO_QUEUE_ENTERED) // split bio is flagged with BIO_QUEUE_ENTERED
                
                generic_make_request(remain)
                    generic_make_request_checks
                        blkcg_bio_issue_check
                            blk_throtl_bio
                            blkg_rwstat_add(&blkg->stat_ios, 1); // count for remain bio
                            # buffer the remain bio in bio_list temporarily
                
                # go on handling split bio           

v5.5 引入的 commit f73316482977ac401ac37245c9df48079d4e11f3 ("blk-cgroup: reimplement basic IO stats using cgroup rstat") 重構了這些統計的實作,附帶改變了 io_serviced 的統計邏輯,此時 io_serviced 不再重複統計 split bio

submit_bio
    submit_bio_noacct
        submit_bio_checks
            blk_throtl_bio
                if (bio_flagged(bio, BIO_THROTTLED)): return
                
                blkg_rwstat_add(@stat_bytes, bio->bi_iter.bi_size);
                blkg_rwstat_add(@stat_ios, 1);                    

statistic observation

block throttle 配置的 IOPS/BPS 的語義,從字面上了解就是限制 block cgroup 每秒鐘下發的 IO 數量,至于如何觀測這一限制的效果,其中會存在一些微妙的問題

從語義上來說,配置的 IOPS/BPS 就是限制 block cgroup 每秒鐘下發的 IO 數量,但是 block throttle 的實作決定了,拉長一段時間平均來看,是能夠達到這一限制的,但是如果采用 iostat 這類工具檢視秒級的資料,可以發現 iostat 輸出的秒級資料可能小于、也有可能大于配置的 IOPS/BPS

以下以 WRITE BPS=1024 KB/s 為例

iostat 輸出的 BPS 可能超過配置的 BPS

例如以下 iostat 輸出的資料,其中有一秒的 WITE BPS 為 1404 KB/s

Device:         rrqm/s   wrqm/s     r/s     w/s    rkB/s    wkB/s avgrq-sz avgqu-sz   await r_await w_await  svctm  %util
sdb               0.00     0.00    4.00    4.00  1024.00  1024.00   512.00     0.00    1.00    0.50    1.50   0.25   0.20

Device:         rrqm/s   wrqm/s     r/s     w/s    rkB/s    wkB/s avgrq-sz avgqu-sz   await r_await w_await  svctm  %util
sdb               0.00     0.00    4.00    7.00  1024.00  1404.00   441.45     0.01    1.55    0.50    2.14   0.64   0.70

Device:         rrqm/s   wrqm/s     r/s     w/s    rkB/s    wkB/s avgrq-sz avgqu-sz   await r_await w_await  svctm  %util
sdb               0.00     0.00    4.00    4.00  1024.00  1024.00   512.00     0.01    1.25    0.75    1.75   0.38   0.30           

以下是 ftrace 抓取的 WRITE IO 下發的時序

[1]    kworker/56:1-559   [056] ....  7033.714639: block_bio_queue: 8,16 W 23071232 + 2048 [kworker/56:1]

[2]    kworker/56:1-559   [056] ....  7034.714647: block_bio_queue: 8,16 W 27265536 + 2048 [kworker/56:1]
[3]    kworker/56:1-559   [056] ....  7034.827649: block_bio_queue: 8,16 W 4196864 + 232 [kworker/56:1]
[4]    kworker/56:1-559   [056] ....  7035.085652: block_bio_queue: 8,16 W 4197096 + 528 [kworker/56:1]
    
[5]    kworker/56:1-559   [056] ....  7036.085659: block_bio_queue: 8,16 W 6294016 + 2048 [kworker/56:1]           

block throttle 算法的原則是,等到有足夠的配額之後,再下發整個 IO,例如下發一個 1024KB 的 IO 時,需要等到時刻 [1] 才能下發這個 IO,其他的 IO 以此類推;因而在這條時間的長河上,block throttle 會嚴格地按照配置的 IOPS/BPS 限制,發送 IO,隻不過在其中任意截取的一段 (1s) 時間區間内,實際下發的 IO 資料量可能不足或超過配置的 IOPS/BPS 限制

[1]                  [2] [3]     [4]                 [5]
...+--------------------+--------------------+---+------+--------------------+...
                                        <-------------------->
                                                    1s           

例如上述時序中,如果 iostat 觀察的是 7034.5~7035.5 時間段,就會發現這 1s 時間内 WRITE IO 下發了 2808 (2048+232+528) 個 sector 即 1404 KB/s

iostat 輸出的 BPS 可能小于配置的 BPS

例如以下 iostat 輸出的資料,其中有一秒的 WITE BPS 為 904 KB/s

Device:         rrqm/s   wrqm/s     r/s     w/s    rkB/s    wkB/s avgrq-sz avgqu-sz   await r_await w_await  svctm  %util
sdb               0.00     0.00   ......   9.00  1024.00   904.00    32.00     0.01    0.22    0.38    0.04   0.10   1.20           
kworker/22:1-532   [022] ....  2020.029000: block_bio_queue: 8,16 W 27264512 + 256 [kworker/22:1]

    kworker/22:1-532   [022] ....  2020.154001: block_bio_queue: 8,16 W 16778752 + 256 [kworker/22:1]
    kworker/22:1-532   [022] ....  2020.279007: block_bio_queue: 8,16 W 23070208 + 256 [kworker/22:1]
    kworker/22:1-532   [022] ....  2020.279010: block_bio_queue: 8,16 W 2098072 + 8 [kworker/22:1]
    kworker/22:1-532   [022] ....  2020.279011: block_bio_queue: 8,16 W 2098080 + 8 [kworker/22:1]
    kworker/22:1-532   [022] ....  2020.411003: block_bio_queue: 8,16 W 14681600 + 256 [kworker/22:1]
    kworker/22:1-532   [022] ....  2020.536005: block_bio_queue: 8,16 W 4195840 + 256 [kworker/22:1]
    kworker/22:1-532   [022] ....  2020.661005: block_bio_queue: 8,16 W 6293248 + 256 [kworker/22:1]
    kworker/22:1-532   [022] ....  2020.786006: block_bio_queue: 8,16 W 8390400 + 256 [kworker/22:1]
    kworker/22:1-532   [022] ....  2020.911008: block_bio_queue: 8,16 W 31460352 + 256 [kworker/22:1]
    
    kworker/22:1-532   [022] ....  2021.036009: block_bio_queue: 8,16 W 25167616 + 256 [kworker/22:1]           

如果 iostat 觀察的是 2020.030~2021.030 時間段,就會發現這 1s 時間内 WRITE IO 下發了 1808 (256*7+16) 個 sector 即 904 KB/s

繼續閱讀