Merge pull request #1143 from LibreQoE/chore/warn_tsc

Warn when expensive clocksource is active
This commit is contained in:
Robert Chacón
2026-08-18 09:33:53 -06:00
committed by GitHub
3 changed files with 109 additions and 1 deletions
+17
View File
@@ -109,6 +109,23 @@ journalctl -u lqosd --since "10 minutes ago"
Si el log muestra `LibreQoS failed to attach the XDP/TC kernel` o `Unable to load the XDP/TC kernel`, trate el arranque de `lqosd` como fallido. La WebUI y el bus local no arrancan hasta que el programa del kernel se cargue y se adjunte correctamente. El error de carga incluye el valor de retorno bruto, el número errno y el código errno, por ejemplo `raw=-11, errno=11, code=EAGAIN`. Revise si hay un programa XDP existente, un hook TC ocupado, falta de soporte del driver o mapas BPF fijados obsoletos antes de reiniciar `lqosd`.
### Uso de CPU elevado en horas pico (clocksource TSC inestable)
Si la CPU se satura al 100% durante las horas pico y el throughput/latencia se degradan, el kernel de Linux puede haber marcado el TSC de la CPU como un clocksource inestable y haber vuelto a uno más lento (normalmente HPET). Los operadores de LibreQoS han observado este comportamiento en algunos hosts AMD Ryzen con los estados C de la CPU habilitados, a veces solo después de reiniciar.
LibreQoS advierte al arrancar cuando HPET o el temporizador PM de ACPI está activo. Si ve esta advertencia de lqosd, compruebe el clocksource directamente:
```bash
cat /sys/devices/system/clocksource/clocksource0/current_clocksource
```
Si la salida es `hpet` o `acpi_pm` en un host físico de LibreQoS, soluciónelo de una de estas formas:
- añadiendo `tsc=reliable` (o `tsc=nowatchdog`) a la línea de comandos del kernel en `/etc/default/grub` (`GRUB_CMDLINE_LINUX_DEFAULT`) y ejecutando `sudo update-grub`, y luego reiniciando, o
- deshabilitando los estados C de la CPU en el BIOS/UEFI.
En los hosts donde el watchdog de estabilidad degrada el TSC, añadir únicamente `clocksource=tsc` no evita esa degradación.
### Depuración avanzada de lqosd
```bash
+17
View File
@@ -141,6 +141,23 @@ sudo systemctl edit lqosd
Set `LQOSD_MEMORY_WATCHDOG_DISABLED=1` only when you are actively watching memory pressure through another tool.
### High CPU usage at peak traffic (unstable TSC clocksource)
If the CPU pegs at 100% during peak hours and throughput/latency suffer, the Linux kernel may have marked the CPU's TSC as an unstable clocksource and fallen back to a slower one (usually HPET). LibreQoS operators have observed this on some AMD Ryzen hosts with CPU C-states enabled, sometimes only after a reboot.
LibreQoS warns at startup when HPET or the ACPI PM timer is active. If you see this lqosd warning, check the clocksource directly:
```bash
cat /sys/devices/system/clocksource/clocksource0/current_clocksource
```
If the output is `hpet` or `acpi_pm` on a physical LibreQoS host, fix it by either:
- adding `tsc=reliable` (or `tsc=nowatchdog`) to the kernel command line in `/etc/default/grub` (`GRUB_CMDLINE_LINUX_DEFAULT`) and running `sudo update-grub`, then rebooting, or
- disabling CPU C-states in the BIOS/UEFI.
On hosts where the stability watchdog demotes the TSC, adding only `clocksource=tsc` does not prevent the demotion.
### Advanced lqosd debug
At the command-line, run:
+75 -1
View File
@@ -4,7 +4,7 @@ use crate::node_manager::{WarningLevel, add_global_warning};
use anyhow::Result;
use lqos_config::Config;
use lqos_sys::interface_name_to_index;
use tracing::{error, info, warn};
use tracing::{debug, error, info, warn};
fn check_queues(interface: &str) -> Result<()> {
let path = format!("/sys/class/net/{interface}/queues/");
@@ -174,8 +174,55 @@ fn check_bridge_status(config: &Config, interfaces: &[IpLinkInterface]) -> Resul
Ok(())
}
/// Determine whether the host clocksource is healthy for packet-level timing.
///
/// When the kernel selects a slower clocksource, per-packet reads in the XDP/TC
/// hot path and userspace timing reads can become far more expensive, driving
/// high CPU usage at peak traffic.
///
/// Returns a warning message when an expensive x86 fallback clocksource is
/// active. Returns `None` for the TSC and paravirtual clocksources.
fn clocksource_warning(current: &str) -> Option<String> {
let current = current.trim();
if !matches!(current, "hpet" | "acpi_pm") {
return None;
}
Some(format!(
"Active clocksource is '{current}' instead of 'tsc'. A slower clocksource can make packet-level and userspace timing expensive and cause high CPU at peak. See the Troubleshooting guide: 'High CPU usage at peak traffic (unstable TSC clocksource)'."
))
}
/// Reads the active clocksource from sysfs and, when it is an expensive x86
/// fallback, records a node_manager global warning and a log line.
///
/// Side effects: reads `/sys/devices/system/clocksource/clocksource0/`, and on
/// a problem emits a `warn!` log line and a global warning. This is
/// intentionally non-fatal: shaping startup continues either way.
fn check_clocksource() {
// The TSC is an x86 clocksource; other architectures name their stable
// counter differently and never expose a usable "tsc".
if !matches!(std::env::consts::ARCH, "x86" | "x86_64") {
return;
}
let base = Path::new("/sys/devices/system/clocksource/clocksource0");
let current_path = base.join("current_clocksource");
let Ok(current) = std::fs::read_to_string(&current_path) else {
debug!("Unable to read {current_path:?}; skipping clocksource check");
return;
};
let Some(warning) = clocksource_warning(&current) else {
return;
};
warn!("{warning}");
add_global_warning(WarningLevel::Warning, warning);
}
/// Runs a series of preflight checks to ensure that the configuration is sane
pub fn preflight_checks() -> Result<()> {
// Warn (but do not block) if the host clocksource is unhealthy.
check_clocksource();
// Are we able to load the configuration?
let config = lqos_config::load_config().map_err(|_| {
error!("Failed to load configuration file - /etc/lqos.conf");
@@ -226,3 +273,30 @@ pub fn preflight_checks() -> Result<()> {
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn healthy_tsc_clocksource_produces_no_warning() {
assert_eq!(clocksource_warning("tsc\n"), None);
}
#[test]
fn demoted_tsc_clocksource_produces_warning() {
let warning = clocksource_warning("hpet\n").unwrap();
assert!(warning.contains("'hpet'"));
assert!(warning.contains("tsc"));
}
#[test]
fn acpi_pm_clocksource_produces_warning() {
assert!(clocksource_warning("acpi_pm\n").is_some());
}
#[test]
fn paravirtual_clocksource_produces_no_warning() {
assert_eq!(clocksource_warning("kvm-clock\n"), None);
}
}