From: Luca Boccassi Date: Wed, 17 Nov 2021 10:00:12 +0000 (+0000) Subject: core: add Condition[Memory/CPU/IO]Pressure X-Git-Tag: v250-rc1~102 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=81513b382b24a7f3602987f71042d075ca27d1a5;p=thirdparty%2Fsystemd.git core: add Condition[Memory/CPU/IO]Pressure By default checks PSI on /proc/pressure, and causes a unit to be skipped if the threshold is above the given configuration for the avg300 measurement. Also allow to pass a custom timespan, and a particular slice unit to check under. Fixes #20139 --- diff --git a/man/systemd.unit.xml b/man/systemd.unit.xml index dbf56c26a55..2a44b8cfd8e 100644 --- a/man/systemd.unit.xml +++ b/man/systemd.unit.xml @@ -1658,6 +1658,29 @@ + + ConditionMemoryPressure= + ConditionCPUPressure= + ConditionIOPressure= + + Verify that the overall system (memory, CPU or IO) pressure is below or equal to a threshold. + This setting takes a threshold value as argument. It can be specified as a simple percentage value, + suffixed with %, in which case the pressure will be measured as an average over the last + five minutes before the attempt to start the unit is performed. + Alternatively, the average timespan can also be specified using / as a separator, for + example: 10%/1min. The supported timespans match what the kernel provides, and are + limited to 10sec, 1min and 5min. The + full PSI will be checked first, and if not found some will be + checked. For more details, see the documentation on PSI (Pressure Stall Information) + . + + Optionally, the threshold value can be prefixed with the slice unit under which the pressure will be checked, + followed by a :. If the slice unit is not specified, the overall system pressure will be measured, + instead of a particular cgroup's. + + + AssertArchitecture= AssertVirtualization= @@ -1686,6 +1709,9 @@ AssertMemory= AssertCPUs= AssertOSRelease= + AssertMemoryPressure= + AssertCPUPressure= + AssertIOPressure= Similar to the ConditionArchitecture=, ConditionVirtualization=, …, condition settings described above, these settings diff --git a/src/core/load-fragment-gperf.gperf.in b/src/core/load-fragment-gperf.gperf.in index 67cb2d70a24..6baa3b3b742 100644 --- a/src/core/load-fragment-gperf.gperf.in +++ b/src/core/load-fragment-gperf.gperf.in @@ -341,6 +341,9 @@ Unit.ConditionUser, config_parse_unit_condition_string, Unit.ConditionGroup, config_parse_unit_condition_string, CONDITION_GROUP, offsetof(Unit, conditions) Unit.ConditionControlGroupController, config_parse_unit_condition_string, CONDITION_CONTROL_GROUP_CONTROLLER, offsetof(Unit, conditions) Unit.ConditionOSRelease, config_parse_unit_condition_string, CONDITION_OS_RELEASE, offsetof(Unit, conditions) +Unit.ConditionMemoryPressure, config_parse_unit_condition_string, CONDITION_MEMORY_PRESSURE, offsetof(Unit, conditions) +Unit.ConditionCPUPressure, config_parse_unit_condition_string, CONDITION_CPU_PRESSURE, offsetof(Unit, conditions) +Unit.ConditionIOPressure, config_parse_unit_condition_string, CONDITION_IO_PRESSURE, offsetof(Unit, conditions) Unit.AssertPathExists, config_parse_unit_condition_path, CONDITION_PATH_EXISTS, offsetof(Unit, asserts) Unit.AssertPathExistsGlob, config_parse_unit_condition_path, CONDITION_PATH_EXISTS_GLOB, offsetof(Unit, asserts) Unit.AssertPathIsDirectory, config_parse_unit_condition_path, CONDITION_PATH_IS_DIRECTORY, offsetof(Unit, asserts) @@ -368,6 +371,9 @@ Unit.AssertUser, config_parse_unit_condition_string, Unit.AssertGroup, config_parse_unit_condition_string, CONDITION_GROUP, offsetof(Unit, asserts) Unit.AssertControlGroupController, config_parse_unit_condition_string, CONDITION_CONTROL_GROUP_CONTROLLER, offsetof(Unit, asserts) Unit.AssertOSRelease, config_parse_unit_condition_string, CONDITION_OS_RELEASE, offsetof(Unit, asserts) +Unit.AssertMemoryPressure, config_parse_unit_condition_string, CONDITION_MEMORY_PRESSURE, offsetof(Unit, asserts) +Unit.AssertCPUPressure, config_parse_unit_condition_string, CONDITION_CPU_PRESSURE, offsetof(Unit, asserts) +Unit.AssertIOPressure, config_parse_unit_condition_string, CONDITION_IO_PRESSURE, offsetof(Unit, asserts) Unit.CollectMode, config_parse_collect_mode, 0, offsetof(Unit, collect_mode) Service.PIDFile, config_parse_pid_file, 0, offsetof(Service, pid_file) Service.ExecCondition, config_parse_exec, SERVICE_EXEC_CONDITION, offsetof(Service, exec_command) diff --git a/src/shared/condition.c b/src/shared/condition.c index dae75a5bf5b..63bdc38a490 100644 --- a/src/shared/condition.c +++ b/src/shared/condition.c @@ -39,8 +39,10 @@ #include "os-util.h" #include "parse-util.h" #include "path-util.h" +#include "percent-util.h" #include "proc-cmdline.h" #include "process-util.h" +#include "psi-util.h" #include "selinux-util.h" #include "smack-util.h" #include "stat-util.h" @@ -962,6 +964,131 @@ static int condition_test_file_is_executable(Condition *c, char **env) { (st.st_mode & 0111)); } +static int condition_test_psi(Condition *c, char **env) { + _cleanup_free_ char *first = NULL, *second = NULL, *third = NULL, *fourth = NULL, *pressure_path = NULL; + const char *p, *value, *pressure_type; + loadavg_t *current, limit; + ResourcePressure pressure; + int r; + + assert(c); + assert(c->parameter); + assert(IN_SET(c->type, CONDITION_MEMORY_PRESSURE, CONDITION_CPU_PRESSURE, CONDITION_IO_PRESSURE)); + + if (!is_pressure_supported()) { + log_debug("Pressure Stall Information (PSI) is not supported, skipping."); + return 1; + } + + pressure_type = c->type == CONDITION_MEMORY_PRESSURE ? "memory" : + c->type == CONDITION_CPU_PRESSURE ? "cpu" : + "io"; + + p = c->parameter; + r = extract_many_words(&p, ":", 0, &first, &second, NULL); + if (r <= 0) + return log_debug_errno(r < 0 ? r : SYNTHETIC_ERRNO(EINVAL), "Failed to parse condition parameter %s: %m", c->parameter); + /* If only one parameter is passed, then we look at the global system pressure rather than a specific cgroup. */ + if (r == 1) { + pressure_path = path_join("/proc/pressure", pressure_type); + if (!pressure_path) + return log_oom(); + + value = first; + } else { + const char *controller = strjoina(pressure_type, ".pressure"); + _cleanup_free_ char *slice_path = NULL; + CGroupMask mask, required_mask; + char *slice; + + required_mask = c->type == CONDITION_MEMORY_PRESSURE ? CGROUP_MASK_MEMORY : + c->type == CONDITION_CPU_PRESSURE ? CGROUP_MASK_CPU : + CGROUP_MASK_IO; + + slice = strstrip(first); + if (!slice) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to parse condition parameter %s: %m", c->parameter); + + r = cg_all_unified(); + if (r < 0) + return log_debug_errno(r, "Failed to determine whether the unified cgroups hierarchy is used: %m"); + if (r == 0) { + log_debug("PSI condition check requires the unified cgroups hierarchy, skipping."); + return 1; + } + + r = cg_mask_supported(&mask); + if (r < 0) + return log_debug_errno(r, "Failed to get supported cgroup controllers: %m"); + + if (!FLAGS_SET(mask, required_mask)) { + log_debug("Cgroup %s controller not available, skipping PSI condition check.", pressure_type); + return 1; + } + + r = cg_slice_to_path(slice, &slice_path); + if (r < 0) + return log_debug_errno(r, "Cannot determine slice \"%s\" cgroup path: %m", slice); + + r = cg_get_path(SYSTEMD_CGROUP_CONTROLLER, slice_path, controller, &pressure_path); + if (r < 0) + return log_debug_errno(r, "Error getting cgroup pressure path from %s: %m", slice_path); + + value = second; + } + + /* If a value including a specific timespan (in the intervals allowed by the kernel), + * parse it, otherwise we assume just a plain percentage that will be checked if it is + * smaller or equal to the current pressure average over 5 minutes. */ + r = extract_many_words(&value, "/", 0, &third, &fourth, NULL); + if (r <= 0) + return log_debug_errno(r < 0 ? r : SYNTHETIC_ERRNO(EINVAL), "Failed to parse condition parameter %s: %m", c->parameter); + if (r == 1) + current = &pressure.avg300; + else { + const char *timespan; + + timespan = skip_leading_chars(fourth, NULL); + if (!timespan) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to parse condition parameter %s: %m", c->parameter); + + if (startswith(timespan, "10sec")) + current = &pressure.avg10; + else if (startswith(timespan, "1min")) + current = &pressure.avg60; + else if (startswith(timespan, "5min")) + current = &pressure.avg300; + else + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to parse condition parameter %s: %m", c->parameter); + } + + value = strstrip(third); + if (!value) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to parse condition parameter %s: %m", c->parameter); + + r = parse_permyriad(value); + if (r < 0) + return log_debug_errno(r, "Failed to parse permyriad: %s", c->parameter); + + r = store_loadavg_fixed_point(r / 100LU, r % 100LU, &limit); + if (r < 0) + return log_debug_errno(r, "Failed to parse loadavg: %s", c->parameter); + + r = read_resource_pressure(pressure_path, PRESSURE_TYPE_FULL, &pressure); + if (r == -ENODATA) /* cpu.pressure 'full' was added recently, fall back to 'some'. */ + r = read_resource_pressure(pressure_path, PRESSURE_TYPE_SOME, &pressure); + if (r == -ENOENT) { + /* We already checked that /proc/pressure exists, so this means we were given a cgroup + * that doesn't exist or doesn't exist any longer. */ + log_debug("\"%s\" not found, skipping PSI check.", pressure_path); + return 1; + } + if (r < 0) + return log_debug_errno(r, "Error parsing pressure from %s: %m", pressure_path); + + return *current <= limit; +} + int condition_test(Condition *c, char **env) { static int (*const condition_tests[_CONDITION_TYPE_MAX])(Condition *c, char **env) = { @@ -994,6 +1121,9 @@ int condition_test(Condition *c, char **env) { [CONDITION_ENVIRONMENT] = condition_test_environment, [CONDITION_CPU_FEATURE] = condition_test_cpufeature, [CONDITION_OS_RELEASE] = condition_test_osrelease, + [CONDITION_MEMORY_PRESSURE] = condition_test_psi, + [CONDITION_CPU_PRESSURE] = condition_test_psi, + [CONDITION_IO_PRESSURE] = condition_test_psi, }; int r, b; @@ -1119,6 +1249,9 @@ static const char* const condition_type_table[_CONDITION_TYPE_MAX] = { [CONDITION_ENVIRONMENT] = "ConditionEnvironment", [CONDITION_CPU_FEATURE] = "ConditionCPUFeature", [CONDITION_OS_RELEASE] = "ConditionOSRelease", + [CONDITION_MEMORY_PRESSURE] = "ConditionMemoryPressure", + [CONDITION_CPU_PRESSURE] = "ConditionCPUPressure", + [CONDITION_IO_PRESSURE] = "ConditionIOPressure", }; DEFINE_STRING_TABLE_LOOKUP(condition_type, ConditionType); @@ -1153,6 +1286,9 @@ static const char* const assert_type_table[_CONDITION_TYPE_MAX] = { [CONDITION_ENVIRONMENT] = "AssertEnvironment", [CONDITION_CPU_FEATURE] = "AssertCPUFeature", [CONDITION_OS_RELEASE] = "AssertOSRelease", + [CONDITION_MEMORY_PRESSURE] = "AssertMemoryPressure", + [CONDITION_CPU_PRESSURE] = "AssertCPUPressure", + [CONDITION_IO_PRESSURE] = "AssertIOPressure", }; DEFINE_STRING_TABLE_LOOKUP(assert_type, ConditionType); diff --git a/src/shared/condition.h b/src/shared/condition.h index 3a5420c402c..2bbb7fa7f46 100644 --- a/src/shared/condition.h +++ b/src/shared/condition.h @@ -22,6 +22,9 @@ typedef enum ConditionType { CONDITION_ENVIRONMENT, CONDITION_CPU_FEATURE, CONDITION_OS_RELEASE, + CONDITION_MEMORY_PRESSURE, + CONDITION_CPU_PRESSURE, + CONDITION_IO_PRESSURE, CONDITION_NEEDS_UPDATE, CONDITION_FIRST_BOOT, diff --git a/src/test/test-condition.c b/src/test/test-condition.c index fff697d5a4e..4b22784d178 100644 --- a/src/test/test-condition.c +++ b/src/test/test-condition.c @@ -25,6 +25,7 @@ #include "nulstr-util.h" #include "os-util.h" #include "process-util.h" +#include "psi-util.h" #include "selinux-util.h" #include "set.h" #include "smack-util.h" @@ -1031,4 +1032,158 @@ TEST(condition_test_os_release) { condition_free(condition); } +TEST(condition_test_psi) { + Condition *condition; + CGroupMask mask; + int r; + + if (!is_pressure_supported()) + return (void) log_notice("Pressure Stall Information (PSI) is not supported, skipping %s", __func__); + + condition = condition_new(CONDITION_MEMORY_PRESSURE, "", false, false); + assert_se(condition); + assert_se(condition_test(condition, environ) < 0); + condition_free(condition); + + condition = condition_new(CONDITION_CPU_PRESSURE, "sbarabau", false, false); + assert_se(condition); + assert_se(condition_test(condition, environ) < 0); + condition_free(condition); + + condition = condition_new(CONDITION_MEMORY_PRESSURE, "10%sbarabau", false, false); + assert_se(condition); + assert_se(condition_test(condition, environ) < 0); + condition_free(condition); + + condition = condition_new(CONDITION_CPU_PRESSURE, "10% sbarabau", false, false); + assert_se(condition); + assert_se(condition_test(condition, environ) < 0); + condition_free(condition); + + condition = condition_new(CONDITION_CPU_PRESSURE, "-10", false, false); + assert_se(condition); + assert_se(condition_test(condition, environ) < 0); + condition_free(condition); + + condition = condition_new(CONDITION_CPU_PRESSURE, "10%/10min", false, false); + assert_se(condition); + assert_se(condition_test(condition, environ) < 0); + condition_free(condition); + + condition = condition_new(CONDITION_CPU_PRESSURE, "10min/10%", false, false); + assert_se(condition); + assert_se(condition_test(condition, environ) < 0); + condition_free(condition); + + condition = condition_new(CONDITION_CPU_PRESSURE, "10% 5min", false, false); + assert_se(condition); + assert_se(condition_test(condition, environ) < 0); + condition_free(condition); + + condition = condition_new(CONDITION_CPU_PRESSURE, "/5min", false, false); + assert_se(condition); + assert_se(condition_test(condition, environ) < 0); + condition_free(condition); + + condition = condition_new(CONDITION_IO_PRESSURE, "10s / ", false, false); + assert_se(condition); + assert_se(condition_test(condition, environ) < 0); + condition_free(condition); + + condition = condition_new(CONDITION_MEMORY_PRESSURE, "100%", false, false); + assert_se(condition); + assert_se(condition_test(condition, environ) >= 0); + condition_free(condition); + + condition = condition_new(CONDITION_MEMORY_PRESSURE, "0%", false, false); + assert_se(condition); + assert_se(condition_test(condition, environ) >= 0); + condition_free(condition); + + condition = condition_new(CONDITION_MEMORY_PRESSURE, "0.0%", false, false); + assert_se(condition); + assert_se(condition_test(condition, environ) >= 0); + condition_free(condition); + + condition = condition_new(CONDITION_CPU_PRESSURE, "100%", false, false); + assert_se(condition); + assert_se(condition_test(condition, environ) >= 0); + condition_free(condition); + + condition = condition_new(CONDITION_CPU_PRESSURE, "0%", false, false); + assert_se(condition); + assert_se(condition_test(condition, environ) >= 0); + condition_free(condition); + + condition = condition_new(CONDITION_CPU_PRESSURE, "0.0%", false, false); + assert_se(condition); + assert_se(condition_test(condition, environ) >= 0); + condition_free(condition); + + condition = condition_new(CONDITION_CPU_PRESSURE, "0.01%", false, false); + assert_se(condition); + assert_se(condition_test(condition, environ) >= 0); + condition_free(condition); + + condition = condition_new(CONDITION_CPU_PRESSURE, "0.0%/10sec", false, false); + assert_se(condition); + assert_se(condition_test(condition, environ) >= 0); + condition_free(condition); + + condition = condition_new(CONDITION_CPU_PRESSURE, "100.0% / 1min", false, false); + assert_se(condition); + assert_se(condition_test(condition, environ) >= 0); + condition_free(condition); + + condition = condition_new(CONDITION_IO_PRESSURE, "50.0% / 1min", false, false); + assert_se(condition); + assert_se(condition_test(condition, environ) >= 0); + condition_free(condition); + + r = cg_all_unified(); + if (r < 0) + return (void) log_notice("Failed to determine whether the unified cgroups hierarchy is used, skipping %s", __func__); + if (r == 0) + return (void) log_notice("Requires the unified cgroups hierarchy, skipping %s", __func__); + + if (cg_mask_supported(&mask) < 0) + return (void) log_notice("Failed to get supported cgroup controllers, skipping %s", __func__); + + if (!FLAGS_SET(mask, CGROUP_MASK_MEMORY)) + return (void) log_notice("Requires the cgroup memory controller, skipping %s", __func__); + + if (!FLAGS_SET(mask, CGROUP_MASK_CPU)) + return (void) log_notice("Requires the cgroup CPU controller, skipping %s", __func__); + + condition = condition_new(CONDITION_MEMORY_PRESSURE, " : / ", false, false); + assert_se(condition); + assert_se(condition_test(condition, environ) < 0); + condition_free(condition); + + condition = condition_new(CONDITION_CPU_PRESSURE, "hopefullythisisnotarealone.slice:100% / 10sec", false, false); + assert_se(condition); + assert_se(condition_test(condition, environ) > 0); + condition_free(condition); + + condition = condition_new(CONDITION_CPU_PRESSURE, "-.slice:100.0% / 1min", false, false); + assert_se(condition); + assert_se(condition_test(condition, environ) >= 0); + condition_free(condition); + + condition = condition_new(CONDITION_MEMORY_PRESSURE, "-.slice:0.0%/5min", false, false); + assert_se(condition); + assert_se(condition_test(condition, environ) >= 0); + condition_free(condition); + + condition = condition_new(CONDITION_MEMORY_PRESSURE, "-.slice:100.0%", false, false); + assert_se(condition); + assert_se(condition_test(condition, environ) >= 0); + condition_free(condition); + + condition = condition_new(CONDITION_IO_PRESSURE, "-.slice:0.0%", false, false); + assert_se(condition); + assert_se(condition_test(condition, environ) >= 0); + condition_free(condition); +} + DEFINE_TEST_MAIN(LOG_DEBUG); diff --git a/test/fuzz/fuzz-unit-file/directives-all.service b/test/fuzz/fuzz-unit-file/directives-all.service index c038ed87072..78ddaf5ec8a 100644 --- a/test/fuzz/fuzz-unit-file/directives-all.service +++ b/test/fuzz/fuzz-unit-file/directives-all.service @@ -12,14 +12,17 @@ AssertACPower= AssertArchitecture= AssertCapability= AssertControlGroupController= +AssertCPUPressure= AssertDirectoryNotEmpty= AssertFileIsExecutable= AssertFileNotEmpty= AssertFirstBoot= AssertGroup= AssertHost= +AssertIOPressure= AssertKernelCommandLine= AssertKernelVersion= +AssertMemoryPressure= AssertNeedsUpdate= AssertOSRelease= AssertPathExists= @@ -56,14 +59,17 @@ ConditionACPower= ConditionArchitecture= ConditionCapability= ConditionControlGroupController= +ConditionCPUPressure= ConditionDirectoryNotEmpty= ConditionFileIsExecutable= ConditionFileNotEmpty= ConditionFirstBoot= ConditionGroup= ConditionHost= +ConditionIOPressure= ConditionKernelCommandLine= ConditionKernelVersion= +ConditionMemoryPressure= ConditionNeedsUpdate= ConditionOSRelease= ConditionPathExists= diff --git a/test/fuzz/fuzz-unit-file/directives.service b/test/fuzz/fuzz-unit-file/directives.service index 176c825cab7..ca9959538b8 100644 --- a/test/fuzz/fuzz-unit-file/directives.service +++ b/test/fuzz/fuzz-unit-file/directives.service @@ -4,6 +4,7 @@ After= AllowIsolate= AssertACPower= AssertArchitecture= +AssertCPUPressure= AssertCPUs= AssertCapability= AssertControlGroupController= @@ -14,9 +15,11 @@ AssertFileNotEmpty= AssertFirstBoot= AssertGroup= AssertHost= +AssertIOPressure= AssertKernelCommandLine= AssertKernelVersion= AssertMemory= +AssertMemoryPressure= AssertNeedsUpdate= AssertOSRelease= AssertPathExists= @@ -37,6 +40,7 @@ BindsTo= CollectMode= ConditionACPower= ConditionArchitecture= +ConditionCPUPressure= ConditionCPUs= ConditionFirmware= ConditionCapability= @@ -48,9 +52,11 @@ ConditionFileNotEmpty= ConditionFirstBoot= ConditionGroup= ConditionHost= +ConditionIOPressure= ConditionKernelCommandLine= ConditionKernelVersion= ConditionMemory= +ConditionMemoryPressure= ConditionNeedsUpdate= ConditionOSRelease= ConditionPathExists=