resource_accounting/lib.rs
1//! Building blocks for process-level resource accounting, including memory bounds enforcement and
2//! CPU usage tracking.
3//!
4//! # Overview
5//!
6//! This crate provides a four-pronged approach to process accounting:
7//!
8//! - memory bounds (components declare their _expected_ memory usage)
9//! - allocation tracking (tracking _actual_ memory usage)
10//! - memory limiting (enforcing _maximum_ memory usage)
11//! - CPU tracking (tracking per-component CPU time consumption)
12//!
13//! Through this approach, data planes can be vastly more resilient to memory exhaustion or
14//! exceeding externally applied memory limits, and gain visibility into per-component CPU usage.
15//!
16//! # Memory bounds
17//!
18//! One major problem with resource planning is predicting memory usage. For many applications,
19//! there are a number of factors that can influence memory usage, such as:
20//!
21//! - the workload itself (amount of data coming in)
22//! - application configuration (buffer sizes)
23//! - application changes (new features, bug fixes)
24//!
25//! This requires additional effort by operators, potentially on an ongoing basis, to empirically
26//! determine the right amount of memory to dedicate. What if instead, an application could
27//! determine a reasonable upper bound on its memory usage based on its configuration and report
28//! that to the operator? This is the goal of memory bounds.
29//!
30//! Memory bounds are a way for components to declare their expected memory usage, categorized into
31//! both a minimum required amount and a firm limit. The minimum required amount is the amount of
32//! memory that's required for the component to function correctly, which generally encompasses
33//! things like pre-allocated buffers. The firm limit is meant to indicate the maximum amount of
34//! memory that the component should use, regardless of the workload.
35//!
36//! Providing firm limits does require some additional thought and care, as a component needs to be
37//! able to actually limit itself in order to adhere to those limits. While determining the
38//! bounds themselves is out of scope for this crate, our other two prongs are meant to pick up the
39//! slack where memory bounds fall off.
40//!
41//! # Allocation tracking
42//!
43//! As memory bounds are inherently lossy, and not everything can be fully bounded, we need a way to
44//! track the actual memory used against the expected memory usage. This is where allocation
45//! tracking comes into play and offers a very precise view into per-component memory usage.
46//!
47//! A custom allocator is provided that tracks all memory allocations, and more specifically,
48//! attributes them to a set of registered components. Components register with the allocator and
49//! receive a "token" that can be used to scope allocations to that component.
50//!
51//! By tracking allocations in this way, we end up with the actual usage of each component, which
52//! can then be compared against the memory bounds to determine if a component is exceeding its
53//! bounds or not. In cases where a component is exceeding its bounds, or the application as a whole
54//! is exceeding its configured limit, we need a way to attempt to enforce those limits.
55//!
56//! # Memory limiting
57//!
58//! When the application is approaching its configured memory limit, or is exceeding the limit, a
59//! mechanism is needed to slow down the rate of memory growth. The global memory limiter is a
60//! mechanism for cooperatively applying backpressure in order to limit the rate of work, and
61//! thereby limit the rate of allocations. Components participate by utilizing the global memory
62//! limiter, which conditionally applies small delays in order to artificially generate backpressure.
63//!
64//! # CPU tracking
65//!
66//! CPU tracking provides per-component visibility into CPU time consumption. When running on supported
67//! operating systems, we can granularly track the amount of CPU time spent on a per-thread basis, which
68//! allows us to track CPU usage for resource groups in the same way we track allocations: through the
69//! [`Tracked`] future wrapper.
70//!
71//! This allows operators to understand which components are consuming the most CPU time, aiding in
72//! capacity planning and performance optimization.
73//!
74//! CPU usage tracking is only available on Linux.
75#![deny(warnings)]
76#![deny(missing_docs)]
77
78use std::collections::HashMap;
79
80use serde::Serialize;
81
82#[cfg(test)]
83pub mod test_util;
84
85mod allocator;
86pub use self::allocator::TrackingAllocator;
87
88mod api;
89pub use self::api::ResourceAPIHandler;
90
91mod registry;
92pub use self::registry::{ComponentRegistry, ComponentRegistryHandle, MemoryBoundsBuilder};
93
94mod grant;
95pub use self::grant::MemoryGrant;
96
97mod groups;
98pub use self::groups::{ResourceGroupRegistry, ResourceGroupToken, ResourceTrackingGuard, Track, Tracked};
99
100mod limiter;
101pub use self::limiter::MemoryLimiter;
102
103mod stats;
104pub use self::stats::{ResourceStats, ResourceStatsSnapshot};
105
106mod verifier;
107pub use self::verifier::{BoundsVerifier, VerifiedBounds, VerifierError};
108
109/// Memory bounds for a component.
110///
111/// Components will naturally allocate memory in many phases, from initialization to normal operation. In some cases,
112/// these allocations can be unbounded, leading to potential memory exhaustion.
113///
114/// When a component has a way to bound its memory usage, it can implement this trait to provide that accounting. A
115/// bounds builder exposes a simple interface for tallying up the memory usage of individual pieces of a component, such
116/// as buffers and buffer pools, containers, and more.
117pub trait MemoryBounds {
118 /// Specifies the minimum and firm memory bounds for this component and its subcomponents.
119 fn specify_bounds(&self, builder: &mut MemoryBoundsBuilder);
120}
121
122impl<T> MemoryBounds for &T
123where
124 T: MemoryBounds,
125{
126 fn specify_bounds(&self, builder: &mut MemoryBoundsBuilder) {
127 T::specify_bounds(self, builder);
128 }
129}
130
131impl<T> MemoryBounds for Box<T>
132where
133 T: MemoryBounds + ?Sized,
134{
135 fn specify_bounds(&self, builder: &mut MemoryBoundsBuilder) {
136 T::specify_bounds(self, builder);
137 }
138}
139
140/// Represents a memory usage expression for a component.
141#[derive(Clone, Debug, Serialize)]
142#[serde(tag = "type")]
143pub enum UsageExpr {
144 /// A config value
145 Config {
146 /// The name
147 name: String,
148 /// The value
149 value: usize,
150 },
151
152 /// A struct size
153 StructSize {
154 /// The value
155 name: String,
156 /// The value
157 value: usize,
158 },
159
160 /// A constant value
161 Constant {
162 /// The name
163 name: String,
164 /// The value
165 value: usize,
166 },
167
168 /// A product of subexpressions
169 Product {
170 /// Values to multiply
171 values: Vec<UsageExpr>,
172 },
173
174 /// A sum of subexpressions
175 Sum {
176 /// Values to add
177 values: Vec<UsageExpr>,
178 },
179}
180
181impl UsageExpr {
182 /// Creates a new usage expression that's a config value.
183 pub fn config(s: impl Into<String>, value: usize) -> Self {
184 Self::Config { name: s.into(), value }
185 }
186
187 /// Creates a new usage expression that's a constant value.
188 pub fn constant(s: impl Into<String>, value: usize) -> Self {
189 Self::Constant { name: s.into(), value }
190 }
191
192 /// Creates a new usage expression that's a struct size.
193 pub fn struct_size<T>(s: impl Into<String>) -> Self {
194 Self::StructSize {
195 name: s.into(),
196 value: std::mem::size_of::<T>(),
197 }
198 }
199
200 /// Creates a new usage expression that's the product of two subexpressions.
201 pub fn product(_s: impl Into<String>, lhs: UsageExpr, rhs: UsageExpr) -> Self {
202 Self::Product { values: vec![lhs, rhs] }
203 }
204
205 /// Creates a new usage expression that's the sum of two subexpressions.
206 pub fn sum(_s: impl Into<String>, lhs: UsageExpr, rhs: UsageExpr) -> Self {
207 Self::Sum { values: vec![lhs, rhs] }
208 }
209
210 fn evaluate(&self) -> usize {
211 match self {
212 Self::Config { value, .. } | Self::StructSize { value, .. } | Self::Constant { value, .. } => *value,
213 Self::Product { values } => values.iter().map(UsageExpr::evaluate).product(),
214 Self::Sum { values } => values.iter().map(UsageExpr::evaluate).sum(),
215 }
216 }
217}
218
219/// Memory bounds for a component.
220#[derive(Clone, Debug, Default)]
221pub struct ComponentBounds {
222 self_minimum_required_bytes: Vec<UsageExpr>,
223 self_firm_limit_bytes: Vec<UsageExpr>,
224 subcomponents: HashMap<String, ComponentBounds>,
225}
226
227impl ComponentBounds {
228 /// Gets the total minimum required bytes for this component and all subcomponents.
229 pub fn total_minimum_required_bytes(&self) -> usize {
230 self.self_minimum_required_bytes
231 .iter()
232 .map(UsageExpr::evaluate)
233 .sum::<usize>()
234 + self
235 .subcomponents
236 .values()
237 .map(|cb| cb.total_minimum_required_bytes())
238 .sum::<usize>()
239 }
240
241 /// Gets the total firm limit bytes for this component and all subcomponents.
242 ///
243 /// The firm limit includes the minimum required bytes.
244 pub fn total_firm_limit_bytes(&self) -> usize {
245 self.self_minimum_required_bytes
246 .iter()
247 .map(UsageExpr::evaluate)
248 .sum::<usize>()
249 + self
250 .self_firm_limit_bytes
251 .iter()
252 .map(UsageExpr::evaluate)
253 .sum::<usize>()
254 + self
255 .subcomponents
256 .values()
257 .map(|cb| cb.total_firm_limit_bytes())
258 .sum::<usize>()
259 }
260
261 /// Returns an iterator of all subcomponents within this component.
262 ///
263 /// Only iterates over direct subcomponents, not the subcomponents of those subcomponents, and so on.
264 pub fn subcomponents(&self) -> impl IntoIterator<Item = (&String, &ComponentBounds)> {
265 self.subcomponents.iter()
266 }
267
268 /// Returns a tree of all bound expressions for this component and its subcomponents as JSON.
269 pub fn to_exprs(&self) -> Vec<serde_json::Value> {
270 let path = vec!["root".to_string()];
271 let mut stack = vec![(path, self)];
272 let mut output = Vec::new();
273
274 while let Some((path, cb)) = stack.pop() {
275 for expr in &cb.self_minimum_required_bytes {
276 output.push(serde_json::json!({
277 "name": format!("{}.min", path.join(".")),
278 "expr": expr,
279 }));
280 }
281 for expr in &cb.self_firm_limit_bytes {
282 output.push(serde_json::json!({
283 "name": format!("{}.firm", path.join(".")),
284 "expr": expr,
285 }));
286 }
287
288 for (name, subcomponent) in cb.subcomponents() {
289 let mut path = path.clone();
290 path.push(name.clone());
291 stack.push((path, subcomponent));
292 }
293 }
294
295 output
296 }
297}