Skip to content

Commit 2ec7d89

Browse files
authored
feat: respect CPU and cgroup limits in server-ng and connectors stats (#3647)
The server-ng and connectors runtime stats endpoints reported host-wide sysinfo numbers: a cpuset-confined or memory-capped process showed its neighbors' CPU load and a memory total it can never allocate, leaking host sizing on multi-tenant machines. The legacy server already scoped these (#3615) but kept the logic inline. Extract that logic into a shared system_stats crate: SystemProbe scopes total CPU usage to the allowed core set (snapshotted at startup, before shard threads pin themselves) and memory totals to the effective cgroup cap, falling back to host-wide values for unconfined processes. All three binaries now probe through it. On confined hosts the reported total_cpu_usage, total_memory and available_memory now reflect the confinement, so dashboards and alerts keyed to the old host-wide values will see lower numbers. The connectors runtime also keeps its sysinfo System alive across captures, so CPU usage reflects real deltas instead of the zero first sample of a freshly created System on every request.
1 parent a860cef commit 2ec7d89

16 files changed

Lines changed: 462 additions & 213 deletions

File tree

Cargo.lock

Lines changed: 12 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ members = [
6464
"core/shard",
6565
"core/shard_allocator",
6666
"core/simulator",
67+
"core/system_stats",
6768
"core/tools",
6869
"examples/rust",
6970
]
@@ -305,6 +306,7 @@ strum = { version = "0.28.0", features = ["derive"] }
305306
strum_macros = "0.28.0"
306307
syn = { version = "2", features = ["full", "extra-traits"] }
307308
sysinfo = "0.39.5"
309+
system_stats = { path = "core/system_stats" }
308310
tempfile = "3.27.0"
309311
terminal_size = { version = "0.4.4" }
310312
test-case = "3.3.1"

core/connectors/runtime/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ serde_with = { workspace = true }
7070
serde_yaml_ng = { workspace = true }
7171
strum = { workspace = true }
7272
sysinfo = { workspace = true }
73+
system_stats = { workspace = true }
7374
thiserror = { workspace = true }
7475
tokio = { workspace = true }
7576
toml = { workspace = true }

core/connectors/runtime/src/main.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ use std::{
3939
env,
4040
sync::{Arc, atomic::AtomicU32},
4141
};
42+
use system_stats::capture_allowed_cpus;
4243
use tracing::{error, info};
4344

4445
mod api;
@@ -114,6 +115,7 @@ fn print_ascii_art(text: &str) {
114115

115116
#[tokio::main]
116117
async fn main() -> Result<(), RuntimeError> {
118+
capture_allowed_cpus();
117119
Args::parse();
118120
print_ascii_art("Iggy Connectors");
119121

core/connectors/runtime/src/stats.rs

Lines changed: 19 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -20,31 +20,17 @@ use crate::metrics::ConnectorType;
2020
use iggy_common::{IggyTimestamp, SemanticVersion};
2121
use iggy_connector_sdk::api::{ConnectorRuntimeStats, ConnectorStats};
2222
use std::str::FromStr;
23-
use std::sync::Arc;
23+
use std::sync::{Arc, Mutex, OnceLock, PoisonError};
2424
use sysinfo::System;
25+
use system_stats::SystemProbe;
2526

2627
const VERSION: &str = env!("CARGO_PKG_VERSION");
2728
const SEMANTIC_VERSION: SemanticVersion = SemanticVersion::parse_const(VERSION);
2829

29-
pub async fn get_runtime_stats(context: &Arc<RuntimeContext>) -> ConnectorRuntimeStats {
30-
let pid = std::process::id();
31-
32-
let mut system = System::new_all();
33-
system.refresh_cpu_all();
34-
system.refresh_memory();
35-
system.refresh_processes(
36-
sysinfo::ProcessesToUpdate::Some(&[sysinfo::Pid::from_u32(pid)]),
37-
true,
38-
);
39-
40-
let total_cpu_usage = system.global_cpu_usage();
41-
let total_memory = system.total_memory();
42-
let available_memory = system.available_memory();
30+
static SYSINFO: OnceLock<Mutex<System>> = OnceLock::new();
4331

44-
let (cpu_usage, memory_usage) = system
45-
.process(sysinfo::Pid::from_u32(pid))
46-
.map(|p| (p.cpu_usage(), p.memory()))
47-
.unwrap_or((0.0, 0));
32+
pub async fn get_runtime_stats(context: &Arc<RuntimeContext>) -> ConnectorRuntimeStats {
33+
let system = probe_system();
4834

4935
let sources = context.sources.get_all().await;
5036
let sinks = context.sinks.get_all().await;
@@ -113,12 +99,12 @@ pub async fn get_runtime_stats(context: &Arc<RuntimeContext>) -> ConnectorRuntim
11399
ConnectorRuntimeStats {
114100
connectors_runtime_version: VERSION.to_owned(),
115101
connectors_runtime_version_semver: SEMANTIC_VERSION.get_numeric_version().ok(),
116-
process_id: pid,
117-
cpu_usage,
118-
total_cpu_usage,
119-
memory_usage,
120-
total_memory,
121-
available_memory,
102+
process_id: system.process_id,
103+
cpu_usage: system.cpu_usage,
104+
total_cpu_usage: system.total_cpu_usage,
105+
memory_usage: system.memory_usage,
106+
total_memory: system.total_memory,
107+
available_memory: system.available_memory,
122108
run_time,
123109
start_time: start,
124110
sources_total,
@@ -128,3 +114,11 @@ pub async fn get_runtime_stats(context: &Arc<RuntimeContext>) -> ConnectorRuntim
128114
connectors,
129115
}
130116
}
117+
118+
fn probe_system() -> SystemProbe {
119+
let mut system = SYSINFO
120+
.get_or_init(|| Mutex::new(System::new()))
121+
.lock()
122+
.unwrap_or_else(PoisonError::into_inner);
123+
SystemProbe::capture(&mut system)
124+
}

core/connectors/sdk/src/api.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,21 +73,39 @@ impl ConnectorError {
7373
/// Runtime statistics response from `/stats` endpoint.
7474
#[derive(Debug, Serialize, Deserialize)]
7575
pub struct ConnectorRuntimeStats {
76+
/// The version of the connectors runtime.
7677
pub connectors_runtime_version: String,
78+
/// The semantic version of the connectors runtime in the numeric format,
79+
/// e.g. 1.2.3 -> 1002003 (major followed by zero-padded three-digit minor and patch).
7780
#[serde(skip_serializing_if = "Option::is_none")]
7881
pub connectors_runtime_version_semver: Option<u32>,
82+
/// The unique identifier of the runtime process.
7983
pub process_id: u32,
84+
/// The CPU usage of the runtime process.
8085
pub cpu_usage: f32,
86+
/// The total CPU usage of the system, scoped to the cores this process may run on
87+
/// when confined by an affinity/cpuset mask.
8188
pub total_cpu_usage: f32,
89+
/// The memory usage of the runtime process in bytes.
8290
pub memory_usage: u64,
91+
/// The total memory of the system in bytes, or the effective cgroup memory limit when
92+
/// the runtime runs inside a memory-capped cgroup (container, systemd slice).
8393
pub total_memory: u64,
94+
/// The available memory of the system in bytes, scoped to the cgroup limit when one applies.
8495
pub available_memory: u64,
96+
/// The elapsed time since the runtime started, in microseconds.
8597
pub run_time: u64,
98+
/// The time the runtime started, in microseconds since the UNIX epoch.
8699
pub start_time: u64,
100+
/// The number of configured source connectors, including disabled and failed ones.
87101
pub sources_total: u32,
102+
/// The number of currently running source connectors.
88103
pub sources_running: u32,
104+
/// The number of configured sink connectors, including disabled and failed ones.
89105
pub sinks_total: u32,
106+
/// The number of currently running sink connectors.
90107
pub sinks_running: u32,
108+
/// Per-connector statistics for every configured source and sink.
91109
pub connectors: Vec<ConnectorStats>,
92110
}
93111

core/server-ng/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ slab = { workspace = true }
154154
socket2 = { workspace = true }
155155
strum = { workspace = true }
156156
sysinfo = { workspace = true }
157+
system_stats = { workspace = true }
157158
tempfile = { workspace = true }
158159
thiserror = { workspace = true }
159160
tokio = { workspace = true }

core/server-ng/src/main.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,13 @@ use args::Args;
2323
use clap::Parser;
2424
use server_ng::bootstrap::{bootstrap, load_config};
2525
use server_ng::server_error::ServerNgError;
26+
use system_stats::capture_allowed_cpus;
2627
use tracing::{error, info};
2728

2829
fn main() -> Result<(), ServerNgError> {
30+
// Before shard threads pin themselves: a pinned capture sees one core.
31+
capture_allowed_cpus();
32+
2933
let bootstrap_runtime = match server_common::create_shard_executor() {
3034
Ok(rt) => rt,
3135
Err(e) => {

core/server-ng/src/responses.rs

Lines changed: 26 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,8 @@ use shard::ConnectedClientInfo;
8787
use std::cell::RefCell;
8888
use std::rc::Rc;
8989
use std::sync::{Arc, OnceLock};
90-
use sysinfo::{Pid, ProcessesToUpdate, System as SysinfoSystem};
90+
use sysinfo::System as SysinfoSystem;
91+
use system_stats::SystemProbe;
9192

9293
/// Build the `get_me` reply for the requesting connection. Identity
9394
/// (`user_id`, transport kind, peer address) comes from the per-shard
@@ -727,50 +728,32 @@ impl HostIdentity {
727728
static HOST_IDENTITY: OnceLock<HostIdentity> = OnceLock::new();
728729

729730
fn probe_system_stats() -> SystemStats {
730-
let process_id = std::process::id();
731731
let host = HOST_IDENTITY.get_or_init(HostIdentity::probe);
732-
SYSINFO.with_borrow_mut(|slot| {
733-
let sys = slot.get_or_insert_with(SysinfoSystem::new_all);
734-
sys.refresh_cpu_all();
735-
sys.refresh_memory();
736-
sys.refresh_processes(ProcessesToUpdate::Some(&[Pid::from_u32(process_id)]), true);
737-
738-
let mut stats = SystemStats {
739-
process_id,
740-
cpu_usage: 0.0,
741-
total_cpu_usage: sys.global_cpu_usage(),
742-
memory_usage: 0,
743-
total_memory: sys.total_memory(),
744-
available_memory: sys.available_memory(),
745-
run_time: 0,
746-
start_time: 0,
747-
read_bytes: 0,
748-
written_bytes: 0,
749-
threads_count: 0,
750-
hostname: host.hostname.clone(),
751-
os_name: host.os_name.clone(),
752-
os_version: host.os_version.clone(),
753-
kernel_version: host.kernel_version.clone(),
754-
};
755-
756-
if let Some(process) = sys.process(Pid::from_u32(process_id)) {
757-
stats.cpu_usage = process.cpu_usage();
758-
stats.memory_usage = process.memory();
759-
// sysinfo reports whole seconds; the wire fields are micros (the
760-
// SDK decodes them via `IggyDuration` / `IggyTimestamp::from`, both
761-
// micro-based).
762-
stats.run_time = process.run_time().saturating_mul(1_000_000);
763-
stats.start_time = process.start_time().saturating_mul(1_000_000);
764-
let disk_usage = process.disk_usage();
765-
stats.read_bytes = disk_usage.total_read_bytes;
766-
stats.written_bytes = disk_usage.total_written_bytes;
767-
stats.threads_count = process
768-
.tasks()
769-
.map_or(0, |tasks| u32::try_from(tasks.len()).unwrap_or(u32::MAX));
770-
}
732+
let probe = SYSINFO.with_borrow_mut(|slot| {
733+
let sys = slot.get_or_insert_with(SysinfoSystem::new);
734+
SystemProbe::capture(sys)
735+
});
771736

772-
stats
773-
})
737+
SystemStats {
738+
process_id: probe.process_id,
739+
cpu_usage: probe.cpu_usage,
740+
total_cpu_usage: probe.total_cpu_usage,
741+
memory_usage: probe.memory_usage,
742+
total_memory: probe.total_memory,
743+
available_memory: probe.available_memory,
744+
// sysinfo reports whole seconds; the wire fields are micros (the
745+
// SDK decodes them via `IggyDuration` / `IggyTimestamp::from`, both
746+
// micro-based).
747+
run_time: probe.run_time_secs.saturating_mul(1_000_000),
748+
start_time: probe.start_time_secs.saturating_mul(1_000_000),
749+
read_bytes: probe.read_bytes,
750+
written_bytes: probe.written_bytes,
751+
threads_count: probe.threads_count,
752+
hostname: host.hostname.clone(),
753+
os_name: host.os_name.clone(),
754+
os_version: host.os_version.clone(),
755+
kernel_version: host.kernel_version.clone(),
756+
}
774757
}
775758

776759
fn build_get_stream_response(

core/server/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ chrono = { workspace = true }
4848
clap = { workspace = true }
4949
compio = { workspace = true }
5050
configs = { workspace = true }
51-
cpu_allocation = { workspace = true }
5251
ctrlc = { workspace = true }
5352
cyper = { workspace = true }
5453
cyper-axum = { workspace = true }
@@ -87,6 +86,7 @@ slab = { workspace = true }
8786
socket2 = { workspace = true }
8887
strum = { workspace = true }
8988
sysinfo = { workspace = true }
89+
system_stats = { workspace = true }
9090
tempfile = { workspace = true }
9191
thiserror = { workspace = true }
9292
tokio = { workspace = true }

0 commit comments

Comments
 (0)