From: Tao Cui <[email protected]>

Why a pluggable model at all
----------------------------

When iocost landed in 2019, its commit message already promised that
"a later patch will also allow using bpf progs for cost models", and
the code has carried the split for it ever since: calc_vtime_cost()
is a dispatcher whose only implementation is calc_vtime_cost_builtin().
Seven years later the builtin linear model is still the only one.
This RFC fills that slot, following the bpf_tcp_ca and sched_ext
precedent: builtin algorithms stay as the default, new algorithms are
prototyped in BPF behind a struct_ops.

The measured problems
---------------------

The builtin model prices each IO with a binary sequential/random base
picked by a single per-cgroup cursor and a 16MB seek threshold, plus
a per-page cost.  On a virtio-blk device with the HDD autop profile,
a 4k IO costs ~24us when judged sequential and ~2.7ms when judged
random, a 112x spread, so a wrong judgement becomes a wrong price.
Three classes of mispricing, all measured:

 1. Heuristic rigidity.  Two legitimate sequential readers in one
    cgroup (a database with multiple tablespaces, a threaded backup)
    ping-pong the single cursor and are all priced random: measured
    89x overcharge, 12.9x throughput collapse under the same weight.
    Random IO within a hot window smaller than the 16MB threshold is
    priced sequential: measured 107x undercharge, an accounting
    escape for hotspot workloads.  No setting of the six builtin
    parameters seems able to fix this: telling the streams apart
    requires per-IO state tracking, which looks like logic rather
    than coefficients.

 2. Device nonlinearity.  SLC-cache phases, SMR band placement and
    shared controllers (multiple NVMe namespaces multiplexing one
    device) make the real cost of an identical IO vary by an order of
    magnitude over time or across namespaces.  A static
    6-parameter linear model has no way to express that.

 3. Unpriced operations.  Flush and zone append fall through to a
    cost of zero and bypass throttling entirely (fixed in a separate
    series already posted), but the same pattern extends to device
    quirks the builtin model was never taught.

Mispricing feeds directly into the control loop: vtime budgets,
surplus donation and the vrate feedback all consume the model's
output, so a wrong model can skew the whole controller.

How
---

A new "iocost_model_ops" struct_ops with a single callback:

    u64 calc_cost(op, nbytes, sector, cursor, iocg_id, flags)

scalar arguments only, no kernel pointers exposed; the return value
is vtime (2^37 per second of device time), clamped to 1s per IO; a
return value of 0 delegates the IO back to the builtin formula, so a
model which only handles some IO types cannot make the rest free.
iocg_id identifies the issuing cgroup so a model can keep
per-cgroup state; struct_ops signatures are frozen once merged, so
it is part of the initial interface.  Devices opt in per queue with
"echo $dev ctrl=bpf > io.cost.model"; everything else keeps the
builtin model unchanged.  Patch overview:

 1/8: add an iocost_ioc_tick tracepoint emitting the per-period
      controller state, so model quality can be evaluated without
      drgn (existing events are state-change driven and silent in
      steady state)
 2/8: define the iocost_model_ops interface (above); the model is
      called from the IO submission path under RCU and must not
      sleep
 3/8: register the struct_ops; at most one model system-wide (EBUSY),
      registration serialized with a mutex, base helper set allowed
      so models can use maps for per-cgroup state
 4/8: dispatch cost calculation to the registered model in
      calc_vtime_cost(), falling back to the builtin formula; the
      request-level sizing path keeps the builtin formula
 5/8: per-device opt-in via ctrl=bpf
 6/8: selftest with an example model (the builtin linear HDD formula
      at double cost, mirroring bpf_dctcp as the in-tree reference
      implementation) plus a runner verifying registration and
      ctrl=bpf readback, including the rejected-without-model case
 7/8: a second example model which replaces the single-cursor
      sequentiality heuristic with per-cgroup multi-stream detection
      keyed by iocg_id, the first consumer of that argument
 8/8: document ctrl=bpf in cgroup-v2.rst

Does it work
------------

Mechanism, verified functionally (QEMU, virtio-blk with the HDD
profile, same 8s sequential-read workload from a 1%-weight cgroup,
builtin vs the 2x example model):

 - per-IO charge: 2854us -> 5728us, a factor of 2.001-2.007x; the
   completed IO count halves (2634 -> 1332) and total cost.usage is
   conserved (7.51s vs 7.63s of device time), i.e. the model output
   drives both charging and budgeting
 - the same ratio holds across four hosts (local disk, enterprise
   NVMe, NVMe-backed root on an idle k8s master, and a real 7.3T HDD
   behind a loaded host) and across 4k/64k/1M block sizes: 2.00-2.03x
   on the flash-backed hosts, and within 2% of 2x on the real HDD,
   where the host load adds variance
 - edge cases: ctrl=bpf without a registered model is rejected; a
   second registration fails with EBUSY; unregistering the model or
   returning 0 (delegation) both fall back to the builtin price; a
   model using a map keyed by iocg_id records distinct ids per
   cgroup
 - iocost_ioc_tick fires every period in steady state and emits one
   final tick with running=0 before the controller goes idle

Payoff, demonstrated with the multi-stream example model (7/8) on
the same setup, 4k IOs at weight 1000, builtin vs the model:

 - two sequential readers in one cgroup: priced 2064us/op by builtin
   (both judged random by the single cursor) and 23us/op by the model
   (each stream keeps its own slot), a throughput recovery from 7.2
   to 511 MiB/s
 - random IO inside an 8M window: priced 22.9us/op by builtin
   (undercharge, an accounting escape) and 2643us/op by the model
 - single-stream sequential and whole-disk random pricing are
   unchanged, so the model fixes both directions of mispricing
   without introducing a new one

Non-interference, measured on enterprise NVMe (MEMBLAZE P6541,
passthrough to KVM guest, 8 proc x 128k sequential read, 7.5M
aggregate IOPS):

 - no measurable overhead when not in use: builtin vs BPF-patched kernel
   (no model registered) within noise (30180 vs 30314 ops/proc);
   the one indirect call per bio is not measurable at this rate
 - 12-hour stress (870 iterations, 3 cgroups at 1000/100/10 weights,
   read + write + flush concurrent, 87 iocost disable/enable cycles,
   870 model switches): zero errors, zero memory drift (0.03% over
   32GB)

Open questions for discussion:

 1. Interface: scalar args (as here) vs passing struct bio * for
    CO-RE access?  Is a cold-path model_refresh() callback needed so
    models can track vrate?
 2. Per-cgroup state lifetime: iocg_id is the css id and is recycled
    once the cgroup is removed, so models must treat it as a
    transient key.  Does the interface need a release(iocg_id)
    callback so models can clean up per-cgroup state instead of
    detecting reuse, or is per-cgroup bpf local storage, which
    carries its own lifetime, the better home for model state?
 3. Request-level asymmetry: the request-based sizing path
    (calc_size_vtime_cost, which feeds the latency QoS met/missed
    decision) still uses the builtin formula when a model is
    registered, while bio charging goes through the model.  Should
    the request path dispatch too, or is builtin sizing with a BPF
    charge model acceptable?
 4. One model system-wide vs named models selectable per device (as
    tcp-cc names are per-socket)?

Tao Cui (8):
  blk-iocost: add iocost_ioc_tick tracepoint for per-period device
    summary
  blk-iocost: define iocost_model_ops cost model interface
  blk-iocost: implement BPF struct_ops registration
  blk-iocost: dispatch cost calculation to registered BPF model
  blk-iocost: add ctrl=bpf per-device opt-in
  selftests/bpf: add iocost cost model test
  selftests/bpf: add multi-stream sequentiality example model
  docs: cgroup-v2: document io.cost ctrl=bpf option

 Documentation/admin-guide/cgroup-v2.rst       |  16 +-
 block/Kconfig                                 |   9 +
 block/Makefile                                |   1 +
 block/blk-iocost-bpf.c                        | 152 +++++++++++++++++
 block/blk-iocost.c                            |  46 +++++-
 include/linux/blk-iocost.h                    |  56 +++++++
 include/trace/events/iocost.h                 |  40 +++++
 .../selftests/bpf/prog_tests/iocost_model.c   | 155 ++++++++++++++++++
 .../selftests/bpf/progs/iocost_model.c        |  91 ++++++++++
 tools/testing/selftests/bpf/progs/iocost_ms.c | 122 ++++++++++++++
 10 files changed, 682 insertions(+), 6 deletions(-)
 create mode 100644 block/blk-iocost-bpf.c
 create mode 100644 include/linux/blk-iocost.h
 create mode 100644 tools/testing/selftests/bpf/prog_tests/iocost_model.c
 create mode 100644 tools/testing/selftests/bpf/progs/iocost_model.c
 create mode 100644 tools/testing/selftests/bpf/progs/iocost_ms.c

-- 
2.43.0


Reply via email to