core: add RestartRandomizedDelaySec= service option
We already support exponential backoff for automatic restarts via RestartSec=/RestartSteps=/RestartMaxDelaySec=, but there is no way to randomize the restart delay. When many instances of a service fail at the same time (e.g. because a shared resource briefly went away) they are all restarted in lockstep, creating a thundering herd problem. So this commit adds a simple `RestartRandomizedDelaySec=` service option which is similar to the timer `RandomizedDelaySec=` and adds a randomized restart delay.
This commit is contained in:
committed by
Lennart Poettering
parent
23b0bd230b
commit
046de3caea
@@ -218,4 +218,76 @@ assert_eq "$(systemctl show "$UNIT_NAME" -P NRestarts)" "1"
|
||||
|
||||
rm /run/systemd/system/"$UNIT_NAME"
|
||||
|
||||
# Test RestartRandomizedDelaySec=
|
||||
|
||||
export UNIT_NAME="TEST-03-JOBS-restart-randomized-delay.service"
|
||||
|
||||
cat >"/run/systemd/system/$UNIT_NAME" <<EOF
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=false
|
||||
Restart=on-failure
|
||||
RestartSec=1
|
||||
RestartRandomizedDelaySec=1
|
||||
StartLimitIntervalSec=0
|
||||
EOF
|
||||
|
||||
systemctl daemon-reload
|
||||
|
||||
# The option should be parsed and exposed on the bus in usec.
|
||||
assert_eq "$(systemctl show "$UNIT_NAME" -P RestartRandomizedDelayUSec)" "1s"
|
||||
|
||||
# The chosen delay is logged at debug level when the unit enters auto-restart, so we can read it without
|
||||
# waiting for the delay to elapse.
|
||||
PREV_LOG_LEVEL="$(systemctl log-level)"
|
||||
|
||||
restart_randomized_delay_cleanup() {
|
||||
set +e
|
||||
systemctl log-level "$PREV_LOG_LEVEL"
|
||||
systemctl stop "$UNIT_NAME"
|
||||
rm -f /run/systemd/system/"$UNIT_NAME"
|
||||
systemctl daemon-reload
|
||||
}
|
||||
trap restart_randomized_delay_cleanup EXIT
|
||||
|
||||
systemctl log-level debug
|
||||
|
||||
get_restart_interval() {
|
||||
# Enter auto-restart once, read the logged "<total>|<delay>", then stop again so it never has to elapse.
|
||||
systemctl start --no-block "$UNIT_NAME"
|
||||
timeout 10 bash -c 'while [[ "$(systemctl show "'"$UNIT_NAME"'" -P SubState)" != "auto-restart" ]]; do sleep .2; done'
|
||||
systemctl stop "$UNIT_NAME"
|
||||
journalctl --sync
|
||||
# needed because of -o pipefail
|
||||
{ journalctl -q --no-pager -o cat -b -u "$UNIT_NAME" --grep="Next restart interval calculated as" || true; } |
|
||||
sed -n 's/.*calculated as: \(.*\) (randomized delay: \(.*\))$/\1|\2/p' | tail -n1
|
||||
}
|
||||
|
||||
# Several samples + "not all equal": two draws could rarely render identically (~1e-6) and falsely fail.
|
||||
DELAYS=()
|
||||
TOTALS=()
|
||||
for _ in {1..4}; do
|
||||
IFS='|' read -r total delay <<<"$(get_restart_interval)"
|
||||
TOTALS+=("$total")
|
||||
DELAYS+=("$delay")
|
||||
done
|
||||
|
||||
systemctl log-level "$PREV_LOG_LEVEL"
|
||||
|
||||
: "Chosen randomized restart delays: ${DELAYS[*]} (totals: ${TOTALS[*]})"
|
||||
for delay in "${DELAYS[@]}"; do
|
||||
assert_neq "$delay" ""
|
||||
# Within bound: a value below 1s never renders a bare "<digit>s" token (only ms/us).
|
||||
if [[ "$delay" =~ [0-9]s ]]; then
|
||||
echo "FAIL: randomized restart delay '$delay' exceeds the configured 1s bound" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
# Total must vary, proving the jitter is folded into the armed timer (not merely logged).
|
||||
all_equal=1
|
||||
for total in "${TOTALS[@]}"; do
|
||||
[[ "$total" == "${TOTALS[0]}" ]] || all_equal=0
|
||||
done
|
||||
assert_eq "$all_equal" "0"
|
||||
|
||||
touch /testok
|
||||
|
||||
Reference in New Issue
Block a user