Add ActivatingConcurrencyMax for slice startup pacing
Introduce Slice.ActivatingConcurrencyMax to limit how many units within a slice hierarchy may be in activating state concurrently. Expose the setting over D-Bus, support transient/property parsing, enforce it during unit start dispatch, and re-check queued starts when units leave activating state. Document the new slice option and add a PID1 concurrency test covering queued startup behavior.
This commit is contained in:
committed by
Yu Watanabe
parent
6044c31038
commit
5f0d98e728
@@ -11469,6 +11469,8 @@ node /org/freedesktop/systemd1/unit/system_2eslice {
|
||||
@org.freedesktop.DBus.Property.EmitsChangedSignal("false")
|
||||
readonly u ConcurrencySoftMax = ...;
|
||||
@org.freedesktop.DBus.Property.EmitsChangedSignal("false")
|
||||
readonly u ActivatingConcurrencyMax = ...;
|
||||
@org.freedesktop.DBus.Property.EmitsChangedSignal("false")
|
||||
readonly u NCurrentlyActive = ...;
|
||||
@org.freedesktop.DBus.Property.EmitsChangedSignal("false")
|
||||
readonly s Slice = '...';
|
||||
@@ -11666,6 +11668,8 @@ node /org/freedesktop/systemd1/unit/system_2eslice {
|
||||
|
||||
<!--property ConcurrencySoftMax is not documented!-->
|
||||
|
||||
<!--property ActivatingConcurrencyMax is not documented!-->
|
||||
|
||||
<!--property NCurrentlyActive is not documented!-->
|
||||
|
||||
<!--property Slice is not documented!-->
|
||||
@@ -11854,6 +11858,8 @@ node /org/freedesktop/systemd1/unit/system_2eslice {
|
||||
|
||||
<variablelist class="dbus-property" generated="True" extra-ref="ConcurrencySoftMax"/>
|
||||
|
||||
<variablelist class="dbus-property" generated="True" extra-ref="ActivatingConcurrencyMax"/>
|
||||
|
||||
<variablelist class="dbus-property" generated="True" extra-ref="NCurrentlyActive"/>
|
||||
|
||||
<variablelist class="dbus-property" generated="True" extra-ref="Slice"/>
|
||||
@@ -13265,6 +13271,7 @@ $ gdbus introspect --system --dest org.freedesktop.systemd1 \
|
||||
<varname>IOPressureWatch</varname>,
|
||||
<varname>CPUSetPartition</varname>, and
|
||||
<varname>OOMRules</varname> were added in version 261.</para>
|
||||
<para><varname>ActivatingConcurrencyMax</varname> was added in version 262.</para>
|
||||
</refsect2>
|
||||
<refsect2>
|
||||
<title>Scope Unit Objects</title>
|
||||
|
||||
@@ -139,6 +139,46 @@
|
||||
|
||||
<xi:include href="version-info.xml" xpointer="v258"/></listitem>
|
||||
</varlistentry>
|
||||
|
||||
<varlistentry>
|
||||
<term><varname>ActivatingConcurrencyMax=</varname></term>
|
||||
|
||||
<listitem><para>Configures a limit on the maximum number of units assigned to this
|
||||
slice (or any descendent slices) that may be in the <emphasis>activating</emphasis> state
|
||||
at the same time. Unlike <varname>ConcurrencySoftMax=</varname> which limits units in the
|
||||
<emphasis>active</emphasis> state, this option limits units while they are starting up.
|
||||
Once a unit leaves the <emphasis>activating</emphasis> state (whether to
|
||||
<emphasis>active</emphasis>, <emphasis>failed</emphasis>, or any other state), it no longer
|
||||
counts toward this limit, allowing the next queued unit to begin starting.</para>
|
||||
|
||||
<para>This is particularly useful for managing the "thundering herd" problem during system
|
||||
boot, where many long-running services (such as container workloads) attempt to start
|
||||
simultaneously. By setting <varname>ActivatingConcurrencyMax=</varname>, you can pace the
|
||||
startup process to limit CPU and I/O pressure, while still allowing all services to
|
||||
eventually reach the <emphasis>active</emphasis> state.</para>
|
||||
|
||||
<para>When the limit is reached, further activation requests are queued and will be
|
||||
dispatched automatically once running activations complete. No error is returned to the
|
||||
caller. Note that if a unit becomes stuck in the activating state (for example, due to
|
||||
a hung process or missing dependency), it will continue to occupy a slot until it
|
||||
leaves that state. Configure appropriate timeouts (e.g.,
|
||||
<varname>TimeoutStartSec=</varname>) on individual units to prevent indefinite blocking.</para>
|
||||
|
||||
<para>Setting <varname>ActivatingConcurrencyMax=0</varname> blocks all activation
|
||||
requests in the slice hierarchy indefinitely. Queued units will never start until
|
||||
the limit is raised. This can be used to intentionally freeze slice startup,
|
||||
matching the behavior of <varname>ConcurrencySoftMax=0</varname>.</para>
|
||||
|
||||
<para>If the special value <literal>infinity</literal> is specified, no concurrency limit
|
||||
is enforced. This is the default.</para>
|
||||
|
||||
<para>Note that this option has a hierarchical effect: a limit set for a slice unit will
|
||||
apply to both the units immediately within the slice and all units further down the slice
|
||||
tree. Note that slice units themselves never enter the activating state, so nested slices
|
||||
do not count toward the limit.</para>
|
||||
|
||||
<xi:include href="version-info.xml" xpointer="v262"/></listitem>
|
||||
</varlistentry>
|
||||
</variablelist>
|
||||
|
||||
</refsect1>
|
||||
|
||||
@@ -34,6 +34,7 @@ const sd_bus_vtable bus_slice_vtable[] = {
|
||||
* systemctl set-property), hence they aren't marked as constant */
|
||||
SD_BUS_PROPERTY("ConcurrencyHardMax", "u", bus_property_get_unsigned, offsetof(Slice, concurrency_hard_max), 0),
|
||||
SD_BUS_PROPERTY("ConcurrencySoftMax", "u", bus_property_get_unsigned, offsetof(Slice, concurrency_soft_max), 0),
|
||||
SD_BUS_PROPERTY("ActivatingConcurrencyMax", "u", bus_property_get_unsigned, offsetof(Slice, activating_concurrency_max), 0),
|
||||
SD_BUS_PROPERTY("NCurrentlyActive", "u", property_get_currently_active, 0, 0),
|
||||
SD_BUS_VTABLE_END
|
||||
};
|
||||
@@ -59,6 +60,9 @@ static int bus_slice_set_transient_property(
|
||||
if (streq(name, "ConcurrencySoftMax"))
|
||||
return bus_set_transient_unsigned(u, name, &s->concurrency_soft_max, message, flags, reterr_error);
|
||||
|
||||
if (streq(name, "ActivatingConcurrencyMax"))
|
||||
return bus_set_transient_unsigned(u, name, &s->activating_concurrency_max, message, flags, reterr_error);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -634,6 +634,7 @@ Path.TriggerLimitIntervalSec, config_parse_sec,
|
||||
Path.TriggerLimitBurst, config_parse_unsigned, 0, offsetof(Path, trigger_limit.burst)
|
||||
Slice.ConcurrencySoftMax, config_parse_concurrency_max, 0, offsetof(Slice, concurrency_soft_max)
|
||||
Slice.ConcurrencyHardMax, config_parse_concurrency_max, 0, offsetof(Slice, concurrency_hard_max)
|
||||
Slice.ActivatingConcurrencyMax, config_parse_concurrency_max, 0, offsetof(Slice, activating_concurrency_max)
|
||||
{{ CGROUP_CONTEXT_CONFIG_ITEMS('Slice') }}
|
||||
{{ CGROUP_CONTEXT_CONFIG_ITEMS('Scope') }}
|
||||
{{ KILL_CONTEXT_CONFIG_ITEMS('Scope') }}
|
||||
|
||||
@@ -28,6 +28,7 @@ static void slice_init(Unit *u) {
|
||||
u->ignore_on_isolate = true;
|
||||
s->concurrency_hard_max = UINT_MAX;
|
||||
s->concurrency_soft_max = UINT_MAX;
|
||||
s->activating_concurrency_max = UINT_MAX;
|
||||
}
|
||||
|
||||
static void slice_set_state(Slice *s, SliceState state) {
|
||||
@@ -410,6 +411,25 @@ unsigned slice_get_currently_active(Slice *slice, Unit *ignore, bool with_pendin
|
||||
return n;
|
||||
}
|
||||
|
||||
static unsigned slice_get_currently_activating(Slice *slice, Unit *ignore) {
|
||||
Unit *u = ASSERT_PTR(UNIT(slice));
|
||||
unsigned n = 0;
|
||||
Unit *member;
|
||||
|
||||
UNIT_FOREACH_DEPENDENCY(member, u, UNIT_ATOM_SLICE_OF) {
|
||||
if (member == ignore)
|
||||
continue;
|
||||
|
||||
if (unit_active_state(member) == UNIT_ACTIVATING)
|
||||
n++;
|
||||
|
||||
if (member->type == UNIT_SLICE)
|
||||
n += slice_get_currently_activating(SLICE(member), ignore);
|
||||
}
|
||||
|
||||
return n;
|
||||
}
|
||||
|
||||
bool slice_concurrency_soft_max_reached(Slice *slice, Unit *ignore) {
|
||||
assert(slice);
|
||||
|
||||
@@ -438,6 +458,20 @@ bool slice_concurrency_hard_max_reached(Slice *slice, Unit *ignore) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool slice_activating_concurrency_max_reached(Slice *slice, Unit *ignore) {
|
||||
assert(slice);
|
||||
|
||||
if (slice->activating_concurrency_max != UINT_MAX &&
|
||||
slice_get_currently_activating(slice, ignore) >= slice->activating_concurrency_max)
|
||||
return true;
|
||||
|
||||
Unit *parent = UNIT_GET_SLICE(UNIT(slice));
|
||||
if (parent)
|
||||
return slice_activating_concurrency_max_reached(SLICE(parent), ignore);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
const UnitVTable slice_vtable = {
|
||||
.object_size = sizeof(Slice),
|
||||
.cgroup_context_offset = offsetof(Slice, cgroup_context),
|
||||
|
||||
@@ -12,6 +12,7 @@ typedef struct Slice {
|
||||
|
||||
unsigned concurrency_soft_max;
|
||||
unsigned concurrency_hard_max;
|
||||
unsigned activating_concurrency_max;
|
||||
|
||||
CGroupContext cgroup_context;
|
||||
|
||||
@@ -26,3 +27,4 @@ unsigned slice_get_currently_active(Slice *slice, Unit *ignore, bool with_pendin
|
||||
|
||||
bool slice_concurrency_hard_max_reached(Slice *slice, Unit *ignore);
|
||||
bool slice_concurrency_soft_max_reached(Slice *slice, Unit *ignore);
|
||||
bool slice_activating_concurrency_max_reached(Slice *slice, Unit *ignore);
|
||||
|
||||
+22
-9
@@ -2033,6 +2033,10 @@ int unit_start(Unit *u, ActivationDetails *details) {
|
||||
* the queue */
|
||||
if (slice_concurrency_soft_max_reached(slice, u))
|
||||
return -EAGAIN; /* Try again, keep in queue */
|
||||
|
||||
/* Check activating concurrency limit to pace concurrent startups */
|
||||
if (slice_activating_concurrency_max_reached(slice, u))
|
||||
return -EAGAIN; /* Try again, keep in queue */
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2713,20 +2717,21 @@ static void unit_check_concurrency_limit(Unit *u) {
|
||||
if (!slice)
|
||||
return;
|
||||
|
||||
/* If a unit was stopped, maybe it has pending siblings (or children thereof) that can be started now */
|
||||
/* If a unit was stopped, maybe it has pending siblings (or children thereof) that can be started now.
|
||||
* Walk up the slice hierarchy and re-dispatch for each ancestor that has a limit configured. */
|
||||
|
||||
if (SLICE(slice)->concurrency_soft_max != UINT_MAX) {
|
||||
Unit *sibling;
|
||||
UNIT_FOREACH_DEPENDENCY(sibling, slice, UNIT_ATOM_SLICE_OF) {
|
||||
if (sibling == u)
|
||||
for (Unit *s = slice; s; s = UNIT_GET_SLICE(s)) {
|
||||
if (SLICE(s)->concurrency_soft_max != UINT_MAX ||
|
||||
SLICE(s)->activating_concurrency_max != UINT_MAX) {
|
||||
Unit *member;
|
||||
UNIT_FOREACH_DEPENDENCY(member, s, UNIT_ATOM_SLICE_OF) {
|
||||
if (member == u)
|
||||
continue;
|
||||
|
||||
unit_recursive_add_to_run_queue(sibling);
|
||||
unit_recursive_add_to_run_queue(member);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Also go up the tree. */
|
||||
unit_check_concurrency_limit(slice);
|
||||
}
|
||||
|
||||
void unit_notify(Unit *u, UnitActiveState os, UnitActiveState ns, bool reload_success) {
|
||||
@@ -2882,7 +2887,15 @@ void unit_notify(Unit *u, UnitActiveState os, UnitActiveState ns, bool reload_su
|
||||
* when something BindsTo= to a Type=oneshot unit, as these units go directly from starting to
|
||||
* inactive, without ever entering started.) */
|
||||
unit_submit_to_stop_when_bound_queue(u);
|
||||
|
||||
/* Maybe the activating concurrency limits now allow dispatching of another start job in this slice? */
|
||||
unit_check_concurrency_limit(u);
|
||||
}
|
||||
|
||||
/* When a unit leaves the activating state (to deactivating, inactive, or active), it frees up a slot for
|
||||
* ActivatingConcurrencyMax. Re-dispatch queued starts. */
|
||||
if (os == UNIT_ACTIVATING && ns != UNIT_ACTIVATING)
|
||||
unit_check_concurrency_limit(u);
|
||||
}
|
||||
|
||||
int unit_watch_pidref(Unit *u, const PidRef *pid, bool exclusive) {
|
||||
|
||||
@@ -273,6 +273,7 @@ typedef struct UnitStatusInfo {
|
||||
/* Slice */
|
||||
unsigned concurrency_hard_max;
|
||||
unsigned concurrency_soft_max;
|
||||
unsigned activating_concurrency_max;
|
||||
unsigned n_currently_active;
|
||||
|
||||
/* CGroup */
|
||||
@@ -758,16 +759,21 @@ static void print_status_info(
|
||||
if (endswith(i->id, ".slice")) {
|
||||
printf(" Act. Units: %u", i->n_currently_active);
|
||||
|
||||
if (i->concurrency_soft_max != UINT_MAX || i->concurrency_hard_max != UINT_MAX) {
|
||||
if (i->concurrency_soft_max != UINT_MAX || i->concurrency_hard_max != UINT_MAX || i->activating_concurrency_max != UINT_MAX) {
|
||||
fputs(" (", stdout);
|
||||
|
||||
if (i->concurrency_soft_max != UINT_MAX && i->concurrency_soft_max < i->concurrency_hard_max) {
|
||||
printf("soft limit: %u", i->concurrency_soft_max);
|
||||
if (i->concurrency_hard_max != UINT_MAX)
|
||||
if (i->concurrency_hard_max != UINT_MAX || i->activating_concurrency_max != UINT_MAX)
|
||||
fputs("; ", stdout);
|
||||
}
|
||||
if (i->concurrency_hard_max != UINT_MAX)
|
||||
if (i->concurrency_hard_max != UINT_MAX) {
|
||||
printf("hard limit: %u", i->concurrency_hard_max);
|
||||
if (i->activating_concurrency_max != UINT_MAX)
|
||||
fputs("; ", stdout);
|
||||
}
|
||||
if (i->activating_concurrency_max != UINT_MAX)
|
||||
printf("activating limit: %u", i->activating_concurrency_max);
|
||||
|
||||
putchar(')');
|
||||
}
|
||||
@@ -2260,6 +2266,7 @@ static int show_one(
|
||||
{ "What", "s", NULL, offsetof(UnitStatusInfo, what) },
|
||||
{ "ConcurrencyHardMax", "u", NULL, offsetof(UnitStatusInfo, concurrency_hard_max) },
|
||||
{ "ConcurrencySoftMax", "u", NULL, offsetof(UnitStatusInfo, concurrency_soft_max) },
|
||||
{ "ActivatingConcurrencyMax", "u", NULL, offsetof(UnitStatusInfo, activating_concurrency_max) },
|
||||
{ "NCurrentlyActive", "u", NULL, offsetof(UnitStatusInfo, n_currently_active) },
|
||||
{ "MemoryCurrent", "t", NULL, offsetof(UnitStatusInfo, memory_current) },
|
||||
{ "MemoryPeak", "t", NULL, offsetof(UnitStatusInfo, memory_peak) },
|
||||
@@ -2336,6 +2343,9 @@ static int show_one(
|
||||
.ip_egress_bytes = UINT64_MAX,
|
||||
.io_read_bytes = UINT64_MAX,
|
||||
.io_write_bytes = UINT64_MAX,
|
||||
.concurrency_soft_max = UINT_MAX,
|
||||
.concurrency_hard_max = UINT_MAX,
|
||||
.activating_concurrency_max = UINT_MAX,
|
||||
};
|
||||
bool collect_found_properties;
|
||||
int r;
|
||||
|
||||
@@ -117,6 +117,65 @@ systemctl start sleepforever3@a.service
|
||||
systemctl stop concurrency1.slice
|
||||
systemctl reset-failed
|
||||
|
||||
# Test ActivatingConcurrencyMax
|
||||
cat >/run/systemd/system/concurrency-activating.slice <<EOF
|
||||
[Slice]
|
||||
ActivatingConcurrencyMax=2
|
||||
EOF
|
||||
|
||||
cat >/run/systemd/system/slow-start@.service <<EOF
|
||||
[Service]
|
||||
Slice=concurrency-activating.slice
|
||||
# Simulate slow startup
|
||||
ExecStartPre=/usr/bin/sleep 2
|
||||
ExecStart=/usr/bin/sleep infinity
|
||||
EOF
|
||||
|
||||
systemctl daemon-reload
|
||||
|
||||
# Start 3 services - only 2 should activate concurrently
|
||||
systemctl --no-block start slow-start@a.service
|
||||
systemctl --no-block start slow-start@b.service
|
||||
systemctl --no-block start slow-start@c.service
|
||||
|
||||
# Wait for jobs to be dispatched and first two to enter activating
|
||||
for _ in {1..20}; do
|
||||
jobs=$(systemctl list-jobs | grep -c "slow-start@.*start" || true)
|
||||
if [[ "$jobs" -eq 3 ]]; then
|
||||
activating=$(systemctl list-units --state=activating 'slow-start@*' --no-legend | wc -l)
|
||||
if [[ "$activating" -eq 2 ]]; then
|
||||
break
|
||||
fi
|
||||
fi
|
||||
sleep 0.1
|
||||
done
|
||||
|
||||
# Check that exactly 2 are activating (c should be queued)
|
||||
test "$(systemctl list-jobs | grep -c "slow-start@.*start")" -eq 3
|
||||
test "$(systemctl show -p ActiveState slow-start@a.service --value)" = "activating"
|
||||
test "$(systemctl show -p ActiveState slow-start@b.service --value)" = "activating"
|
||||
test "$(systemctl show -p ActiveState slow-start@c.service --value)" = "inactive"
|
||||
|
||||
# Wait for a and b to finish starting, then c to start and finish
|
||||
# a,b take ~2s to activate, then c starts and takes ~2s more
|
||||
sleep 5
|
||||
|
||||
# Now all should be active (c started when a/b finished activating)
|
||||
systemctl is-active slow-start@a.service
|
||||
systemctl is-active slow-start@b.service
|
||||
systemctl is-active slow-start@c.service
|
||||
|
||||
# Cleanup
|
||||
systemctl stop concurrency-activating.slice
|
||||
systemctl reset-failed
|
||||
rm /run/systemd/system/concurrency-activating.slice
|
||||
rm /run/systemd/system/slow-start@.service
|
||||
|
||||
systemctl daemon-reload
|
||||
|
||||
# Final cleanup of original tests
|
||||
systemctl reset-failed
|
||||
|
||||
rm /run/systemd/system/concurrency1.slice
|
||||
rm /run/systemd/system/concurrency1-concurrency2.slice
|
||||
rm /run/systemd/system/concurrency1-concurrency3.slice
|
||||
|
||||
Reference in New Issue
Block a user