budgeted agents throttle at 90% of the cap; alert on repeated oom kills
This commit is contained in:
parent
c014b48c22
commit
11393bc538
2 changed files with 201 additions and 27 deletions
|
|
@ -156,6 +156,7 @@ pub(crate) fn write_resource_limits(
|
|||
cpu_quota: &str,
|
||||
cpu_weight: Option<u32>,
|
||||
io_weight: Option<u32>,
|
||||
memory_high: Option<u64>,
|
||||
) -> Result<(String, String)> {
|
||||
let logical = validate_container_system_name(container)?;
|
||||
validate_memory_max(memory_max)?;
|
||||
|
|
@ -176,6 +177,7 @@ pub(crate) fn write_resource_limits(
|
|||
cpu_quota,
|
||||
cpu_weight,
|
||||
io_weight,
|
||||
memory_high,
|
||||
);
|
||||
// Published whole: a `daemon-reload` between truncate and write would
|
||||
// otherwise load the unit with its limits and start guard missing.
|
||||
|
|
@ -226,6 +228,10 @@ const RESTART_SEC: u32 = 5;
|
|||
/// entirely, so a request from a hive-c0re built before the weights
|
||||
/// existed — or one whose nix option is `null` — renders no weight lines.
|
||||
///
|
||||
/// `memory_high` is the byte count of `MemoryHigh=`: the throttle point below
|
||||
/// the cap, where the kernel slows the container and reclaims from it. `None`
|
||||
/// omits the line.
|
||||
///
|
||||
/// A finite `MemoryMax=` comes with `MemorySwapMax=0`: at the budget the
|
||||
/// kernel kills inside the container instead of paging it out to host swap.
|
||||
/// `MemoryMax=infinity` is no budget and leaves swap alone.
|
||||
|
|
@ -236,12 +242,14 @@ fn limits_dropin_body(
|
|||
cpu_quota: &str,
|
||||
cpu_weight: Option<u32>,
|
||||
io_weight: Option<u32>,
|
||||
memory_high: Option<u64>,
|
||||
) -> String {
|
||||
// Built as two possibly-empty lines rather than pushed onto the
|
||||
// string: `format!` appended to a `String` trips clippy::pedantic's
|
||||
// `format_push_string`, and a `write!` would need an unwrap.
|
||||
let cpu_weight_line = cpu_weight.map_or_else(String::new, |w| format!("CPUWeight={w}\n"));
|
||||
let io_weight_line = io_weight.map_or_else(String::new, |w| format!("IOWeight={w}\n"));
|
||||
let high_line = memory_high.map_or_else(String::new, |b| format!("MemoryHigh={b}\n"));
|
||||
let swap_line = if memory_max == "infinity" {
|
||||
""
|
||||
} else {
|
||||
|
|
@ -257,6 +265,7 @@ fn limits_dropin_body(
|
|||
ExecStartPre={rootfs_check}\n\
|
||||
RestartSec={RESTART_SEC}\n\
|
||||
MemoryMax={memory_max}\n\
|
||||
{high_line}\
|
||||
{swap_line}\
|
||||
CPUQuota={cpu_quota}\n\
|
||||
{cpu_weight_line}{io_weight_line}"
|
||||
|
|
@ -892,6 +901,7 @@ mod tests {
|
|||
"4G",
|
||||
"200%",
|
||||
None,
|
||||
None,
|
||||
None
|
||||
),
|
||||
"[Unit]\n\
|
||||
|
|
@ -919,7 +929,7 @@ mod tests {
|
|||
("512M", "50%", Some(10), Some(10)),
|
||||
("infinity", "infinity", Some(10_000), None),
|
||||
] {
|
||||
let body = limits_dropin_body("/rt/x", CHECK, mem, cpu, cw, iw);
|
||||
let body = limits_dropin_body("/rt/x", CHECK, mem, cpu, cw, iw, None);
|
||||
let unit = body
|
||||
.split("[Service]")
|
||||
.next()
|
||||
|
|
@ -940,38 +950,59 @@ mod tests {
|
|||
#[test]
|
||||
fn swap_is_off_exactly_when_the_memory_is_budgeted() {
|
||||
for mem in ["4G", "512M", "50%", "8589934592"] {
|
||||
let body = limits_dropin_body("/rt/x", CHECK, mem, "200%", None, None);
|
||||
let body = limits_dropin_body("/rt/x", CHECK, mem, "200%", None, None, None);
|
||||
assert!(
|
||||
body.contains(&format!("\nMemoryMax={mem}\nMemorySwapMax=0\n")),
|
||||
"no swap line under a budget of {mem}: {body}"
|
||||
);
|
||||
assert_eq!(body.matches("MemorySwapMax=").count(), 1, "{body}");
|
||||
}
|
||||
let unbudgeted = limits_dropin_body("/rt/x", CHECK, "infinity", "200%", None, None);
|
||||
let unbudgeted = limits_dropin_body("/rt/x", CHECK, "infinity", "200%", None, None, None);
|
||||
assert!(
|
||||
unbudgeted.contains("\nMemoryMax=infinity\n"),
|
||||
"{unbudgeted}"
|
||||
);
|
||||
assert!(!unbudgeted.contains("MemorySwapMax"), "{unbudgeted}");
|
||||
}
|
||||
|
||||
/// `MemoryHigh=` is the byte count the caller sends, between `MemoryMax=`
|
||||
/// and the swap line, and absent when none is sent.
|
||||
#[test]
|
||||
fn memory_high_is_rendered_only_when_sent() {
|
||||
let with = limits_dropin_body(
|
||||
"/rt/x",
|
||||
CHECK,
|
||||
"4G",
|
||||
"200%",
|
||||
None,
|
||||
None,
|
||||
Some(3_865_470_566),
|
||||
);
|
||||
assert!(
|
||||
with.contains("\nMemoryMax=4G\nMemoryHigh=3865470566\nMemorySwapMax=0\n"),
|
||||
"{with}"
|
||||
);
|
||||
let without = limits_dropin_body("/rt/x", CHECK, "4G", "200%", None, None, None);
|
||||
assert!(!without.contains("MemoryHigh"), "{without}");
|
||||
}
|
||||
/// Weights are appended to the `[Service]` section, each omitted
|
||||
/// independently when `None`.
|
||||
#[test]
|
||||
fn weights_are_emitted_only_when_set() {
|
||||
let both = limits_dropin_body("/rt/x", CHECK, "4G", "200%", Some(80), Some(80));
|
||||
let both = limits_dropin_body("/rt/x", CHECK, "4G", "200%", Some(80), Some(80), None);
|
||||
assert!(
|
||||
both.ends_with("CPUQuota=200%\nCPUWeight=80\nIOWeight=80\n"),
|
||||
"{both}"
|
||||
);
|
||||
|
||||
let cpu_only = limits_dropin_body("/rt/x", CHECK, "4G", "200%", Some(80), None);
|
||||
let cpu_only = limits_dropin_body("/rt/x", CHECK, "4G", "200%", Some(80), None, None);
|
||||
assert!(
|
||||
cpu_only.ends_with("CPUQuota=200%\nCPUWeight=80\n"),
|
||||
"{cpu_only}"
|
||||
);
|
||||
assert!(!cpu_only.contains("IOWeight"), "{cpu_only}");
|
||||
|
||||
let io_only = limits_dropin_body("/rt/x", CHECK, "4G", "200%", None, Some(80));
|
||||
let io_only = limits_dropin_body("/rt/x", CHECK, "4G", "200%", None, Some(80), None);
|
||||
assert!(
|
||||
io_only.ends_with("CPUQuota=200%\nIOWeight=80\n"),
|
||||
"{io_only}"
|
||||
|
|
@ -984,7 +1015,7 @@ mod tests {
|
|||
#[test]
|
||||
fn the_rootfs_check_leads_the_service_section_whatever_the_caller_passes() {
|
||||
for (cw, iw) in [(None, None), (Some(80), Some(80))] {
|
||||
let body = limits_dropin_body("/rt/x", CHECK, "4G", "200%", cw, iw);
|
||||
let body = limits_dropin_body("/rt/x", CHECK, "4G", "200%", cw, iw, None);
|
||||
let service = body
|
||||
.split("[Service]\n")
|
||||
.nth(1)
|
||||
|
|
@ -1405,7 +1436,7 @@ mod tests {
|
|||
let errs = [
|
||||
handle_write_nspawn_flags(name, &[], &isolation, &[], &facts, None, None)
|
||||
.unwrap_err(),
|
||||
write_resource_limits(name, "4G", "200%", None, None).unwrap_err(),
|
||||
write_resource_limits(name, "4G", "200%", None, None, None).unwrap_err(),
|
||||
remove_service_dropin(name).unwrap_err(),
|
||||
];
|
||||
for e in errs {
|
||||
|
|
@ -1471,7 +1502,7 @@ mod tests {
|
|||
"$(id)",
|
||||
"%t",
|
||||
] {
|
||||
let err = write_resource_limits(PROBE, bad, "200%", None, None).unwrap_err();
|
||||
let err = write_resource_limits(PROBE, bad, "200%", None, None, None).unwrap_err();
|
||||
assert!(
|
||||
err.to_string().contains("invalid memory_max"),
|
||||
"{bad:?}: {err:#}"
|
||||
|
|
@ -1491,7 +1522,7 @@ mod tests {
|
|||
"4G",
|
||||
"%t",
|
||||
] {
|
||||
let err = write_resource_limits(PROBE, "4G", bad, None, None).unwrap_err();
|
||||
let err = write_resource_limits(PROBE, "4G", bad, None, None, None).unwrap_err();
|
||||
assert!(
|
||||
err.to_string().contains("invalid cpu_quota"),
|
||||
"{bad:?}: {err:#}"
|
||||
|
|
|
|||
|
|
@ -257,8 +257,14 @@ async fn exec(req: PrivRequest, writer: &mut OwnedWriteHalf) -> Result<(String,
|
|||
ref cpu_quota,
|
||||
cpu_weight,
|
||||
io_weight,
|
||||
memory_high,
|
||||
} => container_config::write_resource_limits(
|
||||
container, memory_max, cpu_quota, cpu_weight, io_weight,
|
||||
container,
|
||||
memory_max,
|
||||
cpu_quota,
|
||||
cpu_weight,
|
||||
io_weight,
|
||||
memory_high,
|
||||
),
|
||||
|
||||
PrivRequest::RemoveServiceDropin { ref container } => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue