-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit a16b21b
Showing
4 changed files
with
309 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
# Generated by Cargo | ||
# will have compiled files and executables | ||
/target/ | ||
|
||
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries | ||
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html | ||
Cargo.lock | ||
|
||
# These are backup files generated by rustfmt | ||
**/*.rs.bk |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
[package] | ||
name = "container-stats" | ||
version = "0.1.0" | ||
authors = ["MCOfficer <[email protected]>"] | ||
description = "A small tool to analyze RAM usage of large amounts of docker containers." | ||
edition = "2018" | ||
|
||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | ||
|
||
[dependencies] | ||
proc-maps = "0.1.6" | ||
dockworker = {version="0.0.15", default_features=false, features=[]} | ||
itertools = "0.9.0" | ||
bytesize = "1.0.0" | ||
tabled = "0.1.0" | ||
structopt = "0.3" | ||
fancy-regex = "0.3.3" | ||
procfs = "0.7.8" | ||
|
||
[profile.release] | ||
opt-level = "z" | ||
lto = true |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
# container-stats | ||
|
||
A small tool to analyze RAM usage of large amounts of docker containers. | ||
|
||
``` | ||
container-stats 0.1.0 | ||
MCOfficer <[email protected]> | ||
A small tool to analyze RAM usage of large amounts of docker containers. | ||
USAGE: | ||
container-stats [FLAGS] [OPTIONS] | ||
FLAGS: | ||
--group-by-prefix Group containers by prefix | ||
--group-by-suffix Group containers by suffix | ||
-h, --help Prints help information | ||
-s, --sort Sorts containers by memory used | ||
--top Use docker top. Not supported on windows & significantly slower, but correctly detects | ||
multiple processes per container | ||
-t, --total Prints total memory used by containers | ||
-V, --version Prints version information | ||
OPTIONS: | ||
-d, --delimiter <delimiter> Delimiter for grouping [default: -] | ||
-m, --memory-backend <memory-backend> The way the used memory is calculated. Options are: "procmaps" (cross- | ||
platform), "rss" and "vsz" (both linux) [default: procmaps] | ||
-r, --regex <regex> Filters container names by a regular expression [default: .*] | ||
``` | ||
|
||
## Building | ||
|
||
``` | ||
$ cargo build [--release] | ||
``` | ||
|
||
## Samples | ||
|
||
List all containers: | ||
``` | ||
$ sudo ./container-stats | ||
+----------+--------+----------------------------------------------------------+------------------------------------------------------------------+ | ||
| memory | pid | name | id | | ||
+----------+--------+----------------------------------------------------------+------------------------------------------------------------------+ | ||
| 4.4 GB | 149498 | /sample-container-1 | 2b13f54b6493cb396ae5122a343053a84a2af5aab1c79eb2f5a58b6a9bb234ca | | ||
+----------+--------+----------------------------------------------------------+------------------------------------------------------------------+ | ||
| 4.8 GB | 60271 | /some-slightly-longer-sample-container | 3dd1f8261c053f9ee5908033bd8798a60120e8e0423c74d83bd8dceda22bd157 | | ||
+----------+--------+----------------------------------------------------------+------------------------------------------------------------------+ | ||
| 3.5 GB | 28350 | /sample-container-2 | 245c06ad91274c40a7f4e0090d5f74112b74818a4d237b5485962f142e74ac1c | | ||
+----------+--------+----------------------------------------------------------+------------------------------------------------------------------+ | ||
| 3.9 GB | 73631 | /some-pretty-damn-long-sample-container-name | 61f2cb9dc16d047f42152dde98c00e5a9f9e2a8ba305adfbf38252af3586e07a | | ||
+----------+--------+----------------------------------------------------------+------------------------------------------------------------------+ | ||
| 17.2 GB | 195327 | /sample-container-three | 1b05272735bf083e1b148169130f620505dbc2fcff3c4fc6f8f46e49a4efa676 | | ||
+----------+--------+----------------------------------------------------------+------------------------------------------------------------------+ | ||
``` | ||
|
||
Show the RSS usage of all containers, combined | ||
``` | ||
$ sudo ./container-stats -m rss -t | ||
Total: 114.3 GB (114328526848 B) | ||
``` | ||
(To learn about the difference between RSS and VSZ, see https://stackoverflow.com/questions/7880784) | ||
|
||
|
||
The total memory usage of all containers starting with `/sample-`: | ||
``` | ||
$ sudo ./container-stats -t -r "^/sample-" | ||
Total: 106.4 GB (106439839744 B) | ||
``` | ||
|
||
All containers, grouped by their prefixes, sorted by memory usage: | ||
``` | ||
$ sudo ./container-stats --group-by-prefix -s | ||
+----------+------------+-------------+ | ||
| memory | containers | fix | | ||
+----------+------------+-------------+ | ||
| 239.3 GB | 18 | /fatfix | | ||
+----------+------------+-------------+ | ||
| 133.1 GB | 10 | /prefix1 | | ||
+----------+------------+-------------+ | ||
| 106.4 GB | 24 | /prefix2 | | ||
+----------+------------+-------------+ | ||
| 88.3 GB | 7 | /fix | | ||
+----------+------------+-------------+ | ||
| 35.8 GB | 10 | /boring | | ||
+----------+------------+-------------+ | ||
| 26.1 GB | 6 | /foofix | | ||
+----------+------------+-------------+ | ||
| 14.7 GB | 4 | /barfix | | ||
+----------+------------+-------------+ | ||
| 11.3 GB | 3 | /prefix4 | | ||
+----------+------------+-------------+ | ||
| 127.9 MB | 1 | /tiny | | ||
+----------+------------+-------------+ | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,183 @@ | ||
use bytesize::ByteSize; | ||
use dockworker::container::{Container, ContainerFilters}; | ||
use dockworker::Docker; | ||
use itertools::{fold, join}; | ||
use proc_maps::{get_process_maps, Pid}; | ||
use procfs::process::Process; | ||
use fancy_regex::Regex; | ||
use structopt::StructOpt; | ||
use tabled::{table, Tabled}; | ||
|
||
#[derive(Tabled)] | ||
struct ContainerStats { | ||
memory: ByteSize, | ||
name: String, | ||
id: String, | ||
} | ||
|
||
#[derive(Tabled)] | ||
struct ContainerGroup { | ||
memory: ByteSize, | ||
containers: i32, | ||
fix: String, | ||
} | ||
|
||
#[derive(StructOpt, Debug)] | ||
#[structopt(name = "container-stats", author, about)] | ||
struct Opt { | ||
/// Prints total memory used by containers | ||
#[structopt(short, long)] | ||
total: bool, | ||
|
||
/// Sorts containers by memory used | ||
#[structopt(short, long)] | ||
sort: bool, | ||
|
||
/// Group containers by prefix | ||
#[structopt(long)] | ||
group_by_prefix: bool, | ||
|
||
/// Group containers by suffix | ||
#[structopt(long)] | ||
group_by_suffix: bool, | ||
|
||
/// Delimiter for grouping | ||
#[structopt(long, short, default_value = "-")] | ||
delimiter: char, | ||
|
||
/// Use docker top. Not supported on windows & significantly slower, but correctly detects multiple processes per container | ||
#[structopt(long)] | ||
top: bool, | ||
|
||
/// Filters container names by a regular expression | ||
#[structopt(long, short, default_value = ".*")] | ||
regex: String, | ||
|
||
/// The way the used memory is calculated. Options are: "procmaps" (cross-platform), "rss" and "vsz" (both linux). | ||
#[structopt(long, short, default_value = "procmaps")] | ||
memory_backend: String, | ||
} | ||
|
||
fn main() { | ||
let opt = Opt::from_args(); | ||
if !["procmaps".to_owned(), "rss".to_owned(), "vsz".to_owned()].contains(&opt.memory_backend) { | ||
println!("Error: unsupported memory backend"); | ||
return; | ||
} | ||
|
||
let docker = Docker::connect_with_defaults().unwrap(); | ||
match docker.list_containers(None, None, None, ContainerFilters::new()) { | ||
Ok(result) => handle_containers(&opt, docker, result), | ||
Err(e) => println!("Error connecting to docker daemon: {}", e), | ||
}; | ||
|
||
fn handle_containers(opt: &Opt, docker: Docker, containers: Vec<Container>) { | ||
let mut all_stats = gather_stats(opt, docker, containers); | ||
|
||
if !opt.regex.eq(".*") { | ||
all_stats = filter(all_stats, &opt.regex); | ||
} | ||
|
||
if opt.total { | ||
let total = fold(&all_stats, 0, |i, stats| i + stats.memory.as_u64()); | ||
println!("Total: {} ({} B)", ByteSize::b(total), total); | ||
return; | ||
} | ||
|
||
if opt.group_by_prefix || opt.group_by_suffix { | ||
let mut grouped_stats = Vec::<ContainerGroup>::new(); | ||
for stat in &all_stats { | ||
let mut split_name = stat.name.split(opt.delimiter); | ||
let fix = match opt.group_by_prefix { | ||
true => split_name.nth(0).unwrap_or(&stat.name).to_string(), | ||
false => split_name.last().unwrap_or(&stat.name).to_string(), | ||
}; | ||
|
||
let mut found = false; | ||
for group in &mut grouped_stats { | ||
if group.fix.eq(&fix) { | ||
group.memory = group.memory + stat.memory; | ||
group.containers += 1; | ||
found = true; | ||
} | ||
} | ||
|
||
if !found { | ||
grouped_stats.push(ContainerGroup { | ||
memory: stat.memory, | ||
containers: 1, | ||
fix: String::from(&fix), | ||
}); | ||
} | ||
} | ||
|
||
if opt.sort { | ||
grouped_stats.sort_by(|a, b| b.memory.cmp(&a.memory)); | ||
} | ||
println!("{}", table(&grouped_stats)); | ||
return; | ||
} | ||
|
||
if opt.sort { | ||
all_stats.sort_by(|a, b| b.memory.cmp(&a.memory)); | ||
} | ||
println!("{}", table(&all_stats)); | ||
} | ||
|
||
fn gather_stats(opt: &Opt, docker: Docker, containers: Vec<Container>) -> Vec<ContainerStats> { | ||
let mut all_stats = Vec::new(); | ||
for container in containers { | ||
let mut memory = 0; | ||
let mut pids = Vec::<i64>::new(); | ||
|
||
if opt.top { | ||
let processes = docker.processes(&container.Id).unwrap(); | ||
for p in processes { | ||
pids.push(p.pid.parse::<i64>().unwrap()); | ||
} | ||
} | ||
else { | ||
pids.push(docker.container_info(&container.Id).unwrap().State.Pid); | ||
} | ||
|
||
for pid in pids { | ||
memory += get_process_memory_bytes(opt, pid) as u64; | ||
} | ||
|
||
all_stats.push(ContainerStats { | ||
id: container.Id, | ||
name: join(container.Names, ", "), | ||
memory: ByteSize::b(memory), | ||
}) | ||
} | ||
all_stats | ||
} | ||
|
||
fn get_process_memory_bytes(opt: &Opt, pid: i64) -> usize { | ||
let mut memory = 0; | ||
|
||
if opt.memory_backend.eq("procmaps") { | ||
let maps = get_process_maps(pid as Pid).unwrap(); | ||
for map in maps { | ||
memory += map.size(); | ||
} | ||
} else { | ||
let p = Process::new(pid as i32).expect("Failed to access process"); | ||
let stat = p.stat().expect("Failed to get process stat"); | ||
memory = match opt.memory_backend.as_ref() { | ||
"rss" => stat.rss_bytes() as usize, | ||
"vsz" => stat.vsize as usize, | ||
&_ => panic!("Got unknown memory backend"), | ||
} | ||
} | ||
memory | ||
} | ||
|
||
fn filter(stats: Vec<ContainerStats>, pattern: &str) -> Vec<ContainerStats> { | ||
let re = Regex::new(pattern).expect("Failed to create Regex pattern!"); | ||
stats | ||
.into_iter() | ||
.filter(|s| re.is_match(&s.name).unwrap()) | ||
.collect() | ||
} | ||
} |