|
6 | 6 | module Process |
7 | 7 | module Metrics |
8 | 8 | module Host |
9 | | - # Linux implementation of host memory metrics. |
10 | | - # Uses cgroups v2 (memory.max, memory.current) or cgroups v1 (memory.limit_in_bytes, memory.usage_in_bytes) when in a container; |
11 | | - # otherwise reads /proc/meminfo (MemTotal, MemAvailable/MemFree, SwapTotal/SwapFree). Parses meminfo once per capture and reuses it. |
12 | | - class Memory::Linux |
13 | | - # Threshold for distinguishing actual memory limits from "unlimited" sentinel values in cgroups v1. |
14 | | - # In cgroups v1, when memory.limit_in_bytes is set to unlimited (by writing -1), the kernel stores a very large sentinel near 2^63. |
15 | | - # Any value >= 2^60 (1 exabyte) is treated as unlimited and we fall back to /proc/meminfo. |
16 | | - # Reference: https://www.kernel.org/doc/Documentation/cgroup-v1/memory.txt |
17 | | - CGROUP_V1_UNLIMITED_THRESHOLD = 2**60 |
| 9 | + # Linux host memory: tries cgroup v2, then cgroup v1, then /proc/meminfo. |
| 10 | + module Memory::Linux |
18 | 11 | DEFAULT_CGROUP_ROOT = "/sys/fs/cgroup" |
19 | 12 |
|
20 | | - def initialize(cgroup_root: nil) |
21 | | - @cgroup_root = (cgroup_root || DEFAULT_CGROUP_ROOT).to_s.chomp("/") |
22 | | - @meminfo = false |
23 | | - end |
24 | | - |
25 | | - # Capture current host memory. Reads total and used (from cgroup or meminfo), computes free, parses swap and reclaimable from meminfo/cgroup. |
26 | | - # @returns [Host::Memory | Nil] |
27 | | - def capture |
28 | | - total = capture_total |
29 | | - return nil unless total && total.positive? |
30 | | - |
31 | | - used = capture_used(total) |
32 | | - used = 0 if used.nil? || used.negative? |
33 | | - used = [used, total].min |
34 | | - |
35 | | - swap_total, swap_used = capture_swap |
36 | | - reclaimable = capture_reclaimable |
37 | | - |
38 | | - return Host::Memory.new(total, used, swap_total, swap_used, reclaimable) |
39 | | - end |
40 | | - |
41 | | - private |
42 | | - |
43 | | - # Path for cgroups v2 (unified): e.g. /sys/fs/cgroup/memory.stat |
44 | | - def cgroup_v2_path(name) |
45 | | - "#{@cgroup_root}/#{name}" |
46 | | - end |
47 | | - |
48 | | - # Path for cgroups v1 (memory controller): e.g. /sys/fs/cgroup/memory/memory.stat |
49 | | - def cgroup_v1_path(name) |
50 | | - "#{@cgroup_root}/memory/#{name}" |
51 | | - end |
52 | | - |
53 | | - # Memoized /proc/meminfo contents. Used for total (MemTotal), used (via MemAvailable), and swap when not in a cgroup. |
54 | | - # @returns [String | Nil] |
55 | | - def meminfo |
56 | | - if @meminfo == false |
57 | | - @meminfo = File.read("/proc/meminfo") rescue nil |
58 | | - end |
59 | | - |
60 | | - return @meminfo |
61 | | - end |
62 | | - |
63 | | - # Total memory in bytes: cgroups v2 memory.max, cgroups v1 memory.limit_in_bytes (if < threshold), else MemTotal from meminfo. |
64 | | - # @returns [Integer | Nil] |
65 | | - def capture_total |
66 | | - if File.exist?(cgroup_v2_path("memory.max")) |
67 | | - limit = File.read(cgroup_v2_path("memory.max")).strip |
68 | | - return limit.to_i if limit != "max" |
69 | | - end |
70 | | - |
71 | | - if File.exist?(cgroup_v1_path("memory.limit_in_bytes")) |
72 | | - limit = File.read(cgroup_v1_path("memory.limit_in_bytes")).strip.to_i |
73 | | - return limit if limit > 0 && limit < CGROUP_V1_UNLIMITED_THRESHOLD |
74 | | - end |
75 | | - |
76 | | - unless meminfo_content = self.meminfo |
77 | | - return nil |
78 | | - end |
79 | | - |
80 | | - meminfo_content.each_line do |line| |
81 | | - if /MemTotal:\s*(?<total>\d+)\s*kB/ =~ line |
82 | | - return $~[:total].to_i * 1024 |
| 13 | + def self.capture(cgroup_root: DEFAULT_CGROUP_ROOT) |
| 14 | + if Memory::Linux::CgroupV2.supported?(cgroup_root) |
| 15 | + if capture = Memory::Linux::CgroupV2.new(cgroup_root: cgroup_root).capture |
| 16 | + return capture |
83 | 17 | end |
84 | 18 | end |
85 | 19 |
|
86 | | - return nil |
87 | | - end |
88 | | - |
89 | | - # Current memory usage in bytes: cgroups v2 memory.current, cgroups v1 memory.usage_in_bytes, or total - MemAvailable from meminfo. |
90 | | - # @parameter total [Integer] Total memory (used to compute used from MemAvailable when not in cgroup). |
91 | | - # @returns [Integer | Nil] |
92 | | - def capture_used(total) |
93 | | - if File.exist?(cgroup_v2_path("memory.current")) |
94 | | - current = File.read(cgroup_v2_path("memory.current")).strip.to_i |
95 | | - return current |
96 | | - end |
97 | | - |
98 | | - if File.exist?(cgroup_v1_path("memory.usage_in_bytes")) |
99 | | - limit = File.read(cgroup_v1_path("memory.limit_in_bytes")).strip.to_i |
100 | | - if limit > 0 && limit < CGROUP_V1_UNLIMITED_THRESHOLD |
101 | | - return File.read(cgroup_v1_path("memory.usage_in_bytes")).strip.to_i |
102 | | - end |
103 | | - end |
104 | | - |
105 | | - unless meminfo_content = self.meminfo |
106 | | - return nil |
107 | | - end |
108 | | - |
109 | | - available_kilobytes = meminfo_content[/MemAvailable:\s*(\d+)\s*kB/, 1]&.to_i |
110 | | - available_kilobytes ||= meminfo_content[/MemFree:\s*(\d+)\s*kB/, 1]&.to_i |
111 | | - return nil unless available_kilobytes |
112 | | - |
113 | | - return [total - (available_kilobytes * 1024), 0].max |
114 | | - end |
115 | | - |
116 | | - # Swap total and used in bytes from meminfo (SwapTotal, SwapFree). |
117 | | - # @returns [Array(Integer, Integer)] [swap_total_bytes, swap_used_bytes], or [nil, nil] if no swap. |
118 | | - def capture_swap |
119 | | - return [nil, nil] unless meminfo_content = self.meminfo |
120 | | - swap_total_kilobytes = meminfo_content[/SwapTotal:\s*(\d+)\s*kB/, 1]&.to_i |
121 | | - swap_free_kilobytes = meminfo_content[/SwapFree:\s*(\d+)\s*kB/, 1]&.to_i |
122 | | - |
123 | | - return [nil, nil] unless swap_total_kilobytes |
124 | | - |
125 | | - swap_total_bytes = swap_total_kilobytes * 1024 |
126 | | - swap_used_bytes = (swap_total_kilobytes - (swap_free_kilobytes || 0)) * 1024 |
127 | | - |
128 | | - return swap_total_bytes, swap_used_bytes |
129 | | - end |
130 | | - |
131 | | - # Reclaimable memory in bytes (page cache, buffers, reclaimable slab). Included in used_size. |
132 | | - # From cgroups v2 memory.stat "file", cgroups v1 memory.stat "cache", or meminfo Cached + Buffers + SReclaimable. |
133 | | - # @returns [Integer | Nil] |
134 | | - def capture_reclaimable |
135 | | - if File.exist?(cgroup_v2_path("memory.stat")) |
136 | | - # cgroups v2: "file" is file-backed (page cache), in bytes |
137 | | - content = File.read(cgroup_v2_path("memory.stat")) rescue nil |
138 | | - if content && (m = content.match(/^file\s+(\d+)/m)) |
139 | | - return m[1].to_i |
| 20 | + if Memory::Linux::CgroupV1.supported?(cgroup_root) |
| 21 | + if capture = Memory::Linux::CgroupV1.new(cgroup_root: cgroup_root).capture |
| 22 | + return capture |
140 | 23 | end |
141 | 24 | end |
142 | 25 |
|
143 | | - if File.exist?(cgroup_v1_path("memory.stat")) |
144 | | - # cgroups v1: "cache" is page cache, in bytes |
145 | | - content = File.read(cgroup_v1_path("memory.stat")) rescue nil |
146 | | - if content && (m = content.match(/^cache\s+(\d+)/m)) |
147 | | - return m[1].to_i |
148 | | - end |
149 | | - end |
150 | | - |
151 | | - # meminfo: Cached + Buffers + SReclaimable (kB) |
152 | | - unless meminfo_content = self.meminfo |
153 | | - return nil |
154 | | - end |
155 | | - cached_kb = meminfo_content[/Cached:\s*(\d+)\s*kB/, 1]&.to_i || 0 |
156 | | - buffers_kb = meminfo_content[/Buffers:\s*(\d+)\s*kB/, 1]&.to_i || 0 |
157 | | - sreclaimable_kb = meminfo_content[/SReclaimable:\s*(\d+)\s*kB/, 1]&.to_i || 0 |
158 | | - reclaimable_kb = cached_kb + buffers_kb + sreclaimable_kb |
159 | | - return reclaimable_kb * 1024 |
| 26 | + return Memory::Linux::Meminfo.new.capture if Memory::Linux::Meminfo.supported? |
160 | 27 | end |
161 | 28 | end |
162 | 29 | end |
163 | 30 | end |
164 | 31 | end |
165 | 32 |
|
| 33 | +require_relative "linux/cgroup_v2" |
| 34 | +require_relative "linux/cgroup_v1" |
| 35 | +require_relative "linux/meminfo" |
| 36 | + |
166 | 37 | # Wire Host::Memory to this implementation on Linux. |
167 | 38 | class << Process::Metrics::Host::Memory |
168 | 39 | def capture |
169 | | - Process::Metrics::Host::Memory::Linux.new.capture |
| 40 | + Process::Metrics::Host::Memory::Linux.capture |
170 | 41 | end |
171 | 42 |
|
172 | 43 | def supported? |
|
0 commit comments