common_stat/
cgroups.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15#![allow(dead_code)]
16
17use std::fs::read_to_string;
18use std::path::Path;
19
20#[cfg(target_os = "linux")]
21use nix::sys::{statfs, statfs::statfs};
22use prometheus::core::{Collector, Desc};
23use prometheus::proto::MetricFamily;
24use prometheus::{IntGauge, Opts};
25
26const CGROUP_UNIFIED_MOUNTPOINT: &str = "/sys/fs/cgroup";
27
28const MEMORY_MAX_FILE_CGROUP_V2: &str = "memory.max";
29const MEMORY_MAX_FILE_CGROUP_V1: &str = "memory.limit_in_bytes";
30const MEMORY_USAGE_FILE_CGROUP_V2: &str = "memory.current";
31const CPU_MAX_FILE_CGROUP_V2: &str = "cpu.max";
32const CPU_QUOTA_FILE_CGROUP_V1: &str = "cpu.cfs_quota_us";
33const CPU_PERIOD_FILE_CGROUP_V1: &str = "cpu.cfs_period_us";
34const CPU_USAGE_FILE_CGROUP_V2: &str = "cpu.stat";
35
36// `MAX_VALUE_CGROUP_V2` string in `/sys/fs/cgroup/cpu.max` and `/sys/fs/cgroup/memory.max` to indicate that the resource is unlimited.
37const MAX_VALUE_CGROUP_V2: &str = "max";
38
39// For cgroup v1, if the memory is unlimited, it will return a very large value(different from platform) that close to 2^63.
40// For easier comparison, if the memory limit is larger than 1PB we consider it as unlimited.
41const MAX_MEMORY_IN_BYTES: i64 = 1125899906842624; // 1PB
42
43/// Get the limit of memory in bytes from cgroups filesystem.
44///
45/// - If the cgroup total memory is unset, return `None`.
46/// - Return `None` if it fails to read the memory limit or not on linux.
47pub fn get_memory_limit_from_cgroups() -> Option<i64> {
48    #[cfg(target_os = "linux")]
49    {
50        let memory_max_file = if is_cgroup_v2()? {
51            // Read `/sys/fs/cgroup/memory.max` to get the memory limit.
52            MEMORY_MAX_FILE_CGROUP_V2
53        } else {
54            // Read `/sys/fs/cgroup/memory.limit_in_bytes` to get the memory limit.
55            MEMORY_MAX_FILE_CGROUP_V1
56        };
57
58        // For cgroup v1, it will return a very large value(different from platform) if the memory is unset.
59        let memory_limit =
60            read_value_from_file(Path::new(CGROUP_UNIFIED_MOUNTPOINT).join(memory_max_file))?;
61
62        // If memory limit exceeds 1PB(cgroup v1), consider it as unset.
63        if memory_limit > MAX_MEMORY_IN_BYTES {
64            return None;
65        }
66        Some(memory_limit)
67    }
68
69    #[cfg(not(target_os = "linux"))]
70    None
71}
72
73/// Get the usage of memory in bytes from cgroups filesystem.
74///
75/// - Return `None` if it fails to read the memory usage or not on linux or cgroup is v1.
76pub fn get_memory_usage_from_cgroups() -> Option<i64> {
77    #[cfg(target_os = "linux")]
78    {
79        if is_cgroup_v2()? {
80            let usage = read_value_from_file(
81                Path::new(CGROUP_UNIFIED_MOUNTPOINT).join(MEMORY_USAGE_FILE_CGROUP_V2),
82            )?;
83            Some(usage)
84        } else {
85            None
86        }
87    }
88
89    #[cfg(not(target_os = "linux"))]
90    None
91}
92
93/// Get the limit of cpu in millicores from cgroups filesystem.
94///
95/// - If the cpu limit is unset, return `None`.
96/// - Return `None` if it fails to read the cpu limit or not on linux.
97pub fn get_cpu_limit_from_cgroups() -> Option<i64> {
98    #[cfg(target_os = "linux")]
99    if is_cgroup_v2()? {
100        // Read `/sys/fs/cgroup/cpu.max` to get the cpu limit.
101        get_cgroup_v2_cpu_limit(Path::new(CGROUP_UNIFIED_MOUNTPOINT).join(CPU_MAX_FILE_CGROUP_V2))
102    } else {
103        // Read `/sys/fs/cgroup/cpu.cfs_quota_us` and `/sys/fs/cgroup/cpu.cfs_period_us` to get the cpu limit.
104        let quota = read_value_from_file(
105            Path::new(CGROUP_UNIFIED_MOUNTPOINT).join(CPU_QUOTA_FILE_CGROUP_V1),
106        )?;
107
108        let period = read_value_from_file(
109            Path::new(CGROUP_UNIFIED_MOUNTPOINT).join(CPU_PERIOD_FILE_CGROUP_V1),
110        )?;
111
112        // Return the cpu limit in millicores.
113        Some(quota * 1000 / period)
114    }
115
116    #[cfg(not(target_os = "linux"))]
117    None
118}
119
120/// Get the usage of cpu in millicores from cgroups filesystem.
121///
122/// - Return `None` if it's not in the cgroups v2 environment or fails to read the cpu usage.
123pub fn get_cpu_usage_from_cgroups() -> Option<i64> {
124    // In certain bare-metal environments, the `/sys/fs/cgroup/cpu.stat` file may be present and reflect system-wide CPU usage rather than container-specific metrics.
125    // To ensure accurate collection of container-level CPU usage, verify the existence of the `/sys/fs/cgroup/memory.current` file.
126    // The presence of this file typically indicates execution within a containerized environment, thereby validating the relevance of the collected CPU usage data.
127    if !Path::new(CGROUP_UNIFIED_MOUNTPOINT)
128        .join(MEMORY_USAGE_FILE_CGROUP_V2)
129        .exists()
130    {
131        return None;
132    }
133
134    // Read `/sys/fs/cgroup/cpu.stat` to get `usage_usec`.
135    let content =
136        read_to_string(Path::new(CGROUP_UNIFIED_MOUNTPOINT).join(CPU_USAGE_FILE_CGROUP_V2)).ok()?;
137
138    // Read the first line of the content. It will be like this: `usage_usec 447926`.
139    let first_line = content.lines().next()?;
140    let fields = first_line.split(' ').collect::<Vec<&str>>();
141    if fields.len() != 2 {
142        return None;
143    }
144
145    fields[1].trim().parse::<i64>().ok()
146}
147
148// Calculate the cpu usage in millicores from cgroups filesystem.
149//
150// - Return `0` if the current cpu usage is equal to the last cpu usage or the interval is 0.
151pub(crate) fn calculate_cpu_usage(
152    current_cpu_usage_usecs: i64,
153    last_cpu_usage_usecs: i64,
154    interval_milliseconds: i64,
155) -> i64 {
156    let diff = current_cpu_usage_usecs - last_cpu_usage_usecs;
157    if diff > 0 && interval_milliseconds > 0 {
158        ((diff as f64 / interval_milliseconds as f64).round() as i64).max(1)
159    } else {
160        0
161    }
162}
163
164// Check whether the cgroup is v2.
165// - Return `true` if the cgroup is v2, otherwise return `false`.
166// - Return `None` if the detection fails or not on linux.
167fn is_cgroup_v2() -> Option<bool> {
168    #[cfg(target_os = "linux")]
169    {
170        let path = Path::new(CGROUP_UNIFIED_MOUNTPOINT);
171        let fs_stat = statfs(path).ok()?;
172        Some(fs_stat.filesystem_type() == statfs::CGROUP2_SUPER_MAGIC)
173    }
174
175    #[cfg(not(target_os = "linux"))]
176    None
177}
178
179fn read_value_from_file<P: AsRef<Path>>(path: P) -> Option<i64> {
180    let content = read_to_string(&path).ok()?;
181
182    // If the content starts with "max", return `None`.
183    if content.starts_with(MAX_VALUE_CGROUP_V2) {
184        return None;
185    }
186
187    content.trim().parse::<i64>().ok()
188}
189
190fn get_cgroup_v2_cpu_limit<P: AsRef<Path>>(path: P) -> Option<i64> {
191    let content = read_to_string(&path).ok()?;
192
193    let fields = content.trim().split(' ').collect::<Vec<&str>>();
194    if fields.len() != 2 {
195        return None;
196    }
197
198    // If the cgroup cpu limit is unset, return `None`.
199    let quota = fields[0].trim();
200    if quota == MAX_VALUE_CGROUP_V2 {
201        return None;
202    }
203
204    let quota = quota.parse::<i64>().ok()?;
205
206    let period = fields[1].trim();
207    let period = period.parse::<i64>().ok()?;
208
209    // Return the cpu limit in millicores.
210    Some(quota * 1000 / period)
211}
212
213/// A collector that collects cgroups metrics.
214#[derive(Debug)]
215pub struct CgroupsMetricsCollector {
216    descs: Vec<Desc>,
217    memory_usage: IntGauge,
218    cpu_usage: IntGauge,
219}
220
221impl Default for CgroupsMetricsCollector {
222    fn default() -> Self {
223        let mut descs = vec![];
224        let cpu_usage = IntGauge::with_opts(Opts::new(
225            "greptime_cgroups_cpu_usage_microseconds",
226            "the current cpu usage in microseconds that collected from cgroups filesystem",
227        ))
228        .unwrap();
229        descs.extend(cpu_usage.desc().into_iter().cloned());
230
231        let memory_usage = IntGauge::with_opts(Opts::new(
232            "greptime_cgroups_memory_usage_bytes",
233            "the current memory usage that collected from cgroups filesystem",
234        ))
235        .unwrap();
236        descs.extend(memory_usage.desc().into_iter().cloned());
237
238        Self {
239            descs,
240            memory_usage,
241            cpu_usage,
242        }
243    }
244}
245
246impl Collector for CgroupsMetricsCollector {
247    fn desc(&self) -> Vec<&Desc> {
248        self.descs.iter().collect()
249    }
250
251    fn collect(&self) -> Vec<MetricFamily> {
252        if let Some(cpu_usage) = get_cpu_usage_from_cgroups() {
253            self.cpu_usage.set(cpu_usage);
254        }
255
256        if let Some(memory_usage) = get_memory_usage_from_cgroups() {
257            self.memory_usage.set(memory_usage);
258        }
259
260        let mut mfs = Vec::with_capacity(self.descs.len());
261        mfs.extend(self.cpu_usage.collect());
262        mfs.extend(self.memory_usage.collect());
263        mfs
264    }
265}
266
267#[cfg(test)]
268mod tests {
269    use super::*;
270
271    #[test]
272    fn test_read_value_from_file() {
273        assert_eq!(
274            read_value_from_file(Path::new("testdata").join("memory.max")).unwrap(),
275            100000
276        );
277        assert_eq!(
278            read_value_from_file(Path::new("testdata").join("memory.max.unlimited")),
279            None
280        );
281        assert_eq!(read_value_from_file(Path::new("non_existent_file")), None);
282    }
283
284    #[test]
285    fn test_get_cgroup_v2_cpu_limit() {
286        assert_eq!(
287            get_cgroup_v2_cpu_limit(Path::new("testdata").join("cpu.max")).unwrap(),
288            1500
289        );
290        assert_eq!(
291            get_cgroup_v2_cpu_limit(Path::new("testdata").join("cpu.max.unlimited")),
292            None
293        );
294        assert_eq!(
295            get_cgroup_v2_cpu_limit(Path::new("non_existent_file")),
296            None
297        );
298    }
299}