]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
Apply known iocost solutions to block devices
authorGustavo Noronha Silva <gustavo.noronha@collabora.com>
Mon, 2 May 2022 17:02:23 +0000 (14:02 -0300)
committerLennart Poettering <lennart@poettering.net>
Thu, 20 Apr 2023 14:45:57 +0000 (16:45 +0200)
Meta's resource control demo project[0] includes a benchmark tool that can
be used to calculate the best iocost solutions for a given SSD.

  [0]: https://github.com/facebookexperimental/resctl-demo

A project[1] has now been started to create a publicly available database
of results that can be used to apply them automatically.

  [1]: https://github.com/iocost-benchmark/iocost-benchmarks

This change adds a new tool that gets triggered by a udev rule for any
block device and queries the hwdb for known solutions. The format for
the hwdb file that is currently generated by the github action looks like
this:

  # This file was auto-generated on Tue, 23 Aug 2022 13:03:57 +0000.
  # From the following commit:
  # https://github.com/iocost-benchmark/iocost-benchmarks/commit/ca82acfe93c40f21d3b513c055779f43f1126f88
  #
  # Match key format:
  # block:<devpath>:name:<model name>:

  # 12 points, MOF=[1.346,1.346], aMOF=[1.249,1.249]
  block:*:name:HFS256GD9TNG-62A0A:fwver:*:
    IOCOST_SOLUTIONS=isolation isolated-bandwidth bandwidth naive
    IOCOST_MODEL_ISOLATION=rbps=1091439492 rseqiops=52286 rrandiops=63784 wbps=192329466 wseqiops=12309 wrandiops=16119
    IOCOST_QOS_ISOLATION=rpct=0.00 rlat=8807 wpct=0.00 wlat=59023 min=100.00 max=100.00
    IOCOST_MODEL_ISOLATED_BANDWIDTH=rbps=1091439492 rseqiops=52286 rrandiops=63784 wbps=192329466 wseqiops=12309 wrandiops=16119
    IOCOST_QOS_ISOLATED_BANDWIDTH=rpct=0.00 rlat=8807 wpct=0.00 wlat=59023 min=100.00 max=100.00
    IOCOST_MODEL_BANDWIDTH=rbps=1091439492 rseqiops=52286 rrandiops=63784 wbps=192329466 wseqiops=12309 wrandiops=16119
    IOCOST_QOS_BANDWIDTH=rpct=0.00 rlat=8807 wpct=0.00 wlat=59023 min=100.00 max=100.00
    IOCOST_MODEL_NAIVE=rbps=1091439492 rseqiops=52286 rrandiops=63784 wbps=192329466 wseqiops=12309 wrandiops=16119
    IOCOST_QOS_NAIVE=rpct=99.00 rlat=8807 wpct=99.00 wlat=59023 min=75.00 max=100.00

The IOCOST_SOLUTIONS key lists the solutions available for that device
in the preferred order for higher isolation, which is a reasonable
default for most client systems. This can be overriden to choose better
defaults for custom use cases, like the various data center workloads.

The tool can also be used to query the known solutions for a specific
device or to apply a non-default solution (say, isolation or bandwidth).

Co-authored-by: Santosh Mahto <santosh.mahto@collabora.com>
man/iocost.conf.xml [new file with mode: 0644]
man/rules/meson.build
rules.d/90-iocost.rules [new file with mode: 0644]
rules.d/meson.build
src/udev/iocost/iocost.c [new file with mode: 0644]
src/udev/iocost/iocost.conf [new file with mode: 0644]
src/udev/meson.build

diff --git a/man/iocost.conf.xml b/man/iocost.conf.xml
new file mode 100644 (file)
index 0000000..be74244
--- /dev/null
@@ -0,0 +1,76 @@
+<?xml version='1.0'?>
+<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.5//EN"
+  "http://www.oasis-open.org/docbook/xml/4.2/docbookx.dtd">
+<!-- SPDX-License-Identifier: LGPL-2.1-or-later -->
+
+<refentry id="iocost.conf" xmlns:xi="http://www.w3.org/2001/XInclude">
+  <refentryinfo>
+    <title>iocost.conf</title>
+    <productname>systemd</productname>
+  </refentryinfo>
+
+  <refmeta>
+    <refentrytitle>iocost.conf</refentrytitle>
+    <manvolnum>5</manvolnum>
+  </refmeta>
+
+  <refnamediv>
+    <refname>iocost.conf</refname>
+    <refpurpose>Configuration files for the iocost solution manager</refpurpose>
+  </refnamediv>
+
+  <refsynopsisdiv>
+    <para>
+      <filename>/etc/systemd/iocost.conf</filename>
+      <filename>/etc/systemd/iocost.conf.d/*.conf</filename>
+    </para>
+  </refsynopsisdiv>
+
+  <refsect1>
+    <title>Description</title>
+
+    <para>This file configures the behavior of <literal>iocost</literal>, a tool mostly used by
+    <citerefentry><refentrytitle>systemd-udevd</refentrytitle><manvolnum>8</manvolnum></citerefentry> rules
+    to automatically apply I/O cost solutions to <filename>/sys/fs/cgroup/io.cost.*</filename>.</para>
+
+    <para>The qos and model values are calculated based on benchmarks collected on the
+    <ulink url="https://github.com/iocost-benchmark/iocost-benchmarks">iocost-benchmark</ulink>
+    project and turned into a set of solutions that go from most to least isolated.
+    Isolation allows the system to remain responsive in face of high I/O load.
+    Which solutions are available for a device can be queried from the udev metadata attached to it. By
+    default the naive solution is used, which provides the most bandwidth.</para>
+  </refsect1>
+
+  <xi:include href="standard-conf.xml" xpointer="main-conf" />
+
+  <refsect1>
+    <title>Options</title>
+
+    <para>All options are configured in the [IOCost] section:</para>
+
+    <variablelist class='config-directives'>
+
+      <varlistentry>
+        <term><varname>TargetSolution=</varname></term>
+
+        <listitem><para>Chooses which I/O cost solution (identified by named string) should be used
+        for the devices in this system. The known solutions can be queried from the udev metadata
+        attached to the devices. If a device does not have the specified solution, the first one
+        listed in <varname>IOCOST_SOLUTIONS</varname> is used instead.</para>
+
+        <para>E.g. <literal>TargetSolution=isolated-bandwidth</literal>.</para></listitem>
+      </varlistentry>
+    </variablelist>
+  </refsect1>
+
+  <refsect1>
+    <title>See Also</title>
+    <para>
+      <citerefentry><refentrytitle>udevadm</refentrytitle><manvolnum>8</manvolnum></citerefentry>,
+      <ulink url="https://github.com/iocost-benchmark/iocost-benchmarks">The iocost-benchmarks github project</ulink>,
+      <ulink url="https://github.com/facebookexperimental/resctl-demo/tree/main/resctl-bench/doc">The resctl-bench
+      documentation details how the values are obtained</ulink>
+    </para>
+  </refsect1>
+
+</refentry>
index cdf98eaaf04f0f37d56ecf5ca79982d3859c3aa8..ca3b471281fbacb571c5407587ede89ca8e4ec12 100644 (file)
@@ -24,6 +24,7 @@ manpages = [
  ['hostnamectl', '1', [], 'ENABLE_HOSTNAMED'],
  ['hwdb', '7', [], 'ENABLE_HWDB'],
  ['integritytab', '5', [], 'HAVE_LIBCRYPTSETUP'],
+ ['iocost.conf', '5', [], ''],
  ['journal-remote.conf', '5', ['journal-remote.conf.d'], 'HAVE_MICROHTTPD'],
  ['journal-upload.conf', '5', ['journal-upload.conf.d'], 'HAVE_MICROHTTPD'],
  ['journalctl', '1', [], ''],
diff --git a/rules.d/90-iocost.rules b/rules.d/90-iocost.rules
new file mode 100644 (file)
index 0000000..50f778a
--- /dev/null
@@ -0,0 +1,20 @@
+#  SPDX-License-Identifier: LGPL-2.1-or-later
+#
+#  This file is part of systemd.
+#
+#  systemd is free software; you can redistribute it and/or modify it
+#  under the terms of the GNU Lesser General Public License as published by
+#  the Free Software Foundation; either version 2.1 of the License, or
+#  (at your option) any later version.
+
+SUBSYSTEM!="block", GOTO="iocost_end"
+
+ENV{DEVTYPE}=="partition", GOTO="iocost_end"
+
+ACTION=="remove", GOTO="iocost_end"
+
+ENV{ID_MODEL}!="", IMPORT{builtin}="hwdb 'block::name:$env{ID_MODEL}:fwrev:$env{ID_REVISION}:'"
+
+ENV{IOCOST_SOLUTIONS}!="", RUN+="iocost apply $env{DEVNAME}"
+
+LABEL="iocost_end"
index 09edd58da2a765fb0594fe6944719cb490aba253..7280f5b995d6d9b395843c775299990e50fc0d38 100644 (file)
@@ -28,6 +28,7 @@ rules = [
                '78-sound-card.rules',
                '80-net-setup-link.rules',
                '81-net-dhcp.rules',
+               '90-iocost.rules',
               )],
 
         [files('80-drivers.rules'),
diff --git a/src/udev/iocost/iocost.c b/src/udev/iocost/iocost.c
new file mode 100644 (file)
index 0000000..54b50b4
--- /dev/null
@@ -0,0 +1,334 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <errno.h>
+#include <getopt.h>
+#include <stdio.h>
+#include <unistd.h>
+
+#include "sd-device.h"
+
+#include "alloc-util.h"
+#include "build.h"
+#include "cgroup-util.h"
+#include "conf-parser.h"
+#include "devnum-util.h"
+#include "device-util.h"
+#include "main-func.h"
+#include "path-util.h"
+#include "pretty-print.h"
+#include "verbs.h"
+
+static char *arg_target_solution = NULL;
+STATIC_DESTRUCTOR_REGISTER(arg_target_solution, freep);
+
+static int parse_config(void) {
+        static const ConfigTableItem items[] = {
+                { "IOCost", "TargetSolution", config_parse_string, 0, &arg_target_solution },
+        };
+        return config_parse(
+                        NULL,
+                        "/etc/udev/iocost.conf",
+                        NULL,
+                        "IOCost\0",
+                        config_item_table_lookup,
+                        items,
+                        CONFIG_PARSE_WARN,
+                        NULL,
+                        NULL);
+}
+
+static int help(void) {
+        printf("%s [OPTIONS...]\n\n"
+               "Set up iocost model and qos solutions for block devices\n"
+               "\nCommands:\n"
+               "  apply <path> [SOLUTION]    Apply solution for the device if\n"
+               "                             found, do nothing otherwise\n"
+               "  query <path>               Query the known solution for\n"
+               "                             the device\n"
+               "\nOptions:\n"
+               "  -h --help                  Show this help\n"
+               "     --version               Show package version\n",
+               program_invocation_short_name);
+
+        return 0;
+}
+
+static int parse_argv(int argc, char *argv[]) {
+        enum {
+                ARG_VERSION = 0x100,
+        };
+
+        static const struct option options[] = {
+                { "help",      no_argument,       NULL, 'h'           },
+                { "version",   no_argument,       NULL, ARG_VERSION   },
+                {}
+        };
+
+        int c;
+
+        assert(argc >= 1);
+        assert(argv);
+
+        while ((c = getopt_long(argc, argv, "h", options, NULL)) >= 0)
+                switch (c) {
+
+                case 'h':
+                        return help();
+
+                case ARG_VERSION:
+                        return version();
+
+                case '?':
+                        return -EINVAL;
+
+                default:
+                        assert_not_reached();
+                }
+
+        return 1;
+}
+
+static int get_known_solutions(sd_device *device, char ***ret_solutions) {
+        _cleanup_free_ char **s = NULL;
+        const char *value;
+        int r;
+
+        assert(ret_solutions);
+
+        r = sd_device_get_property_value(device, "IOCOST_SOLUTIONS", &value);
+        if (r < 0)
+                return r;
+
+        s = strv_split(value, WHITESPACE);
+        if (!s)
+                return -ENOMEM;
+
+        *ret_solutions = TAKE_PTR(s);
+
+        return 0;
+}
+
+static int choose_solution(char **solutions, const char **ret_name) {
+        assert(ret_name);
+
+        if (strv_isempty(solutions))
+                return log_error_errno(
+                                SYNTHETIC_ERRNO(EINVAL), "IOCOST_SOLUTIONS exists in hwdb but is empty.");
+
+        if (arg_target_solution && strv_find(solutions, arg_target_solution)) {
+                *ret_name = arg_target_solution;
+                log_debug("Selected solution based on target solution: %s", *ret_name);
+        } else {
+                *ret_name = solutions[0];
+                log_debug("Selected first available solution: %s", *ret_name);
+        }
+
+        return 0;
+}
+
+static int query_named_solution(
+                sd_device *device,
+                const char *name,
+                const char **ret_model,
+                const char **ret_qos) {
+
+        _cleanup_strv_free_ char **solutions = NULL;
+        _cleanup_free_ char *upper_name = NULL, *qos_key = NULL, *model_key = NULL;
+        const char *qos = NULL, *model = NULL;
+        int r;
+
+        assert(ret_qos);
+        assert(ret_model);
+
+        /* If NULL is passed we query the default solution, which is the first one listed
+         * in the IOCOST_SOLUTIONS key or the one specified by the TargetSolution setting.
+         */
+        if (!name) {
+                r = get_known_solutions(device, &solutions);
+                if (r == -ENOENT)
+                        return log_device_debug_errno(device, r, "No entry found for device, skipping iocost logic.");
+                if (r < 0)
+                        return log_device_error_errno(device, r, "Failed to query solutions from device: %m");
+
+                r = choose_solution(solutions, &name);
+                if (r < 0)
+                        return r;
+        }
+
+        upper_name = strdup(name);
+        if (!upper_name)
+                return log_oom();
+
+        ascii_strupper(upper_name);
+        string_replace_char(upper_name, '-', '_');
+
+        qos_key = strjoin("IOCOST_QOS_", upper_name);
+        if (!qos_key)
+                return log_oom();
+
+        model_key = strjoin("IOCOST_MODEL_", upper_name);
+        if (!model_key)
+                return log_oom();
+
+        r = sd_device_get_property_value(device, qos_key, &qos);
+        if (r == -ENOENT)
+                return log_device_debug_errno(device, r, "No value found for key %s, skipping iocost logic.", qos_key);
+        if (r < 0)
+                return log_device_error_errno(device, r, "Failed to obtain model for iocost solution from device: %m");
+
+        r = sd_device_get_property_value(device, model_key, &model);
+        if (r == -ENOENT)
+                return log_device_debug_errno(device, r, "No value found for key %s, skipping iocost logic.", model_key);
+        if (r < 0)
+                return log_device_error_errno(device, r, "Failed to obtain model for iocost solution from device: %m");
+
+        *ret_qos = qos;
+        *ret_model = model;
+
+        return 0;
+}
+
+static int apply_solution_for_path(const char *path, const char *name) {
+        _cleanup_(sd_device_unrefp) sd_device *device = NULL;
+        _cleanup_free_ char *qos = NULL, *model = NULL;
+        const char *qos_params = NULL, *model_params = NULL;
+        dev_t devnum;
+        int r;
+
+        r = sd_device_new_from_path(&device, path);
+        if (r < 0)
+                return log_error_errno(r, "Error looking up device: %m");
+
+        r = query_named_solution(device, name, &model_params, &qos_params);
+        if (r == -ENOENT)
+                return 0;
+        if (r < 0)
+                return r;
+
+        r = sd_device_get_devnum(device, &devnum);
+        if (r < 0)
+                return log_device_error_errno(device, r, "Error getting devnum: %m");
+
+        if (asprintf(&qos, DEVNUM_FORMAT_STR " enable=1 ctrl=user %s", DEVNUM_FORMAT_VAL(devnum), qos_params) < 0)
+                return log_oom();
+
+        if (asprintf(&model, DEVNUM_FORMAT_STR " model=linear ctrl=user %s", DEVNUM_FORMAT_VAL(devnum), model_params) < 0)
+                return log_oom();
+
+        log_debug("Applying iocost parameters to %s using solution '%s'\n"
+                        "\tio.cost.qos: %s\n"
+                        "\tio.cost.model: %s\n", path, name ?: "default", qos, model);
+
+        r = cg_set_attribute("io", NULL, "io.cost.qos", qos);
+        if (r < 0) {
+                log_device_full_errno(device, r == -ENOENT ? LOG_DEBUG : LOG_ERR, r, "Failed to set io.cost.qos: %m");
+                return r == -ENOENT ? 0 : r;
+        }
+
+        r = cg_set_attribute("io", NULL, "io.cost.model", model);
+        if (r < 0) {
+                log_device_full_errno(device, r == -ENOENT ? LOG_DEBUG : LOG_ERR, r, "Failed to set io.cost.model: %m");
+                return r == -ENOENT ? 0 : r;
+        }
+
+        return 0;
+}
+
+static int query_solutions_for_path(const char *path) {
+        _cleanup_(sd_device_unrefp) sd_device *device = NULL;
+        _cleanup_strv_free_ char **solutions = NULL;
+        const char *default_solution = NULL;
+        const char *model_name = NULL;
+        int r;
+
+        r = sd_device_new_from_path(&device, path);
+        if (r < 0)
+                return log_error_errno(r, "Error looking up device: %m");
+
+        r = sd_device_get_property_value(device, "ID_MODEL_FROM_DATABASE", &model_name);
+        if (r == -ENOENT) {
+                log_device_debug(device, "Missing ID_MODEL_FROM_DATABASE property, trying ID_MODEL");
+                r = sd_device_get_property_value(device, "ID_MODEL", &model_name);
+                if (r == -ENOENT) {
+                        log_device_info(device, "Device model not found");
+                        return 0;
+                }
+        }
+        if (r < 0)
+                return log_device_error_errno(device, r, "Model name for device %s is unknown", path);
+
+        r = get_known_solutions(device, &solutions);
+        if (r == -ENOENT) {
+                log_device_info(device, "Attribute IOCOST_SOLUTIONS missing, model not found in hwdb.");
+                return 0;
+        }
+        if (r < 0)
+                return log_device_error_errno(device, r, "Couldn't access IOCOST_SOLUTIONS for device %s, model name %s on hwdb: %m\n", path, model_name);
+
+        r = choose_solution(solutions, &default_solution);
+        if (r < 0)
+                return r;
+
+        log_info("Known solutions for %s model name: \"%s\"\n"
+                 "Preferred solution: %s\n"
+                 "Solution that would be applied: %s",
+                 path, model_name,
+                 arg_target_solution, default_solution);
+
+        STRV_FOREACH(s, solutions) {
+                const char *model = NULL, *qos = NULL;
+
+                r = query_named_solution(device, *s, &model, &qos);
+                if (r < 0 || !model || !qos)
+                        continue;
+
+                log_info("%s: io.cost.qos: %s\n"
+                         "%s: io.cost.model: %s", *s, qos, *s, model);
+        }
+
+        return 0;
+}
+
+static int verb_query(int argc, char *argv[], void *userdata) {
+        return query_solutions_for_path(ASSERT_PTR(argv[1]));
+}
+
+static int verb_apply(int argc, char *argv[], void *userdata) {
+        return apply_solution_for_path(
+                        ASSERT_PTR(argv[1]),
+                        argc > 2 ? ASSERT_PTR(argv[2]) : NULL);
+}
+
+static int iocost_main(int argc, char *argv[]) {
+        static const Verb verbs[] = {
+                { "query", 2, 2, 0, verb_query },
+                { "apply", 2, 3, 0, verb_apply },
+                {},
+        };
+
+        return dispatch_verb(argc, argv, verbs, NULL);
+}
+
+static int run(int argc, char *argv[]) {
+        int r;
+
+        log_setup();
+
+        r = parse_argv(argc, argv);
+        if (r <= 0)
+                return r;
+
+        (void) parse_config();
+
+        if (!arg_target_solution) {
+                arg_target_solution = strdup("naive");
+                if (!arg_target_solution)
+                        return log_oom();
+        }
+
+        log_debug("Target solution: %s.", arg_target_solution);
+
+        return iocost_main(argc, argv);
+}
+
+DEFINE_MAIN_FUNCTION(run);
diff --git a/src/udev/iocost/iocost.conf b/src/udev/iocost/iocost.conf
new file mode 100644 (file)
index 0000000..394ea34
--- /dev/null
@@ -0,0 +1,17 @@
+#  This file is part of systemd.
+#
+#  systemd is free software; you can redistribute it and/or modify it under the
+#  terms of the GNU Lesser General Public License as published by the Free
+#  Software Foundation; either version 2.1 of the License, or (at your option)
+#  any later version.
+#
+# Entries in this file show the compile time defaults. Local configuration
+# should be created by either modifying this file. Defaults can be restored by
+# simply deleting this file.
+#
+# Use 'systemd-analyze cat-config udev/iocost.conf' to display the full config.
+#
+# See iocost.conf(5) for details.
+
+[IOCost]
+#TargetSolution=naive
index 5b44dd8d7d67ad766c7dbed96d2efa080bbe470a..081948d223f59f92148c856c66a69ea4d2831a40 100644 (file)
@@ -116,6 +116,7 @@ udev_progs = [['ata_id/ata_id.c'],
               ['cdrom_id/cdrom_id.c'],
               ['fido_id/fido_id.c',
                'fido_id/fido_id_desc.c'],
+              ['iocost/iocost.c'],
               ['scsi_id/scsi_id.c',
                'scsi_id/scsi_serial.c'],
               ['v4l_id/v4l_id.c'],
@@ -149,6 +150,8 @@ endforeach
 if install_sysconfdir_samples
         install_data('udev.conf',
                      install_dir : sysconfdir / 'udev')
+        install_data('iocost/iocost.conf',
+                     install_dir : sysconfdir / 'udev')
 endif
 
 custom_target(