/root/doris/be/src/util/cgroup_util.cpp
Line | Count | Source |
1 | | // Licensed to the Apache Software Foundation (ASF) under one |
2 | | // or more contributor license agreements. See the NOTICE file |
3 | | // distributed with this work for additional information |
4 | | // regarding copyright ownership. The ASF licenses this file |
5 | | // to you under the Apache License, Version 2.0 (the |
6 | | // "License"); you may not use this file except in compliance |
7 | | // with the License. You may obtain a copy of the License at |
8 | | // |
9 | | // http://www.apache.org/licenses/LICENSE-2.0 |
10 | | // |
11 | | // Unless required by applicable law or agreed to in writing, |
12 | | // software distributed under the License is distributed on an |
13 | | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
14 | | // KIND, either express or implied. See the License for the |
15 | | // specific language governing permissions and limitations |
16 | | // under the License. |
17 | | |
18 | | #include "util/cgroup_util.h" |
19 | | |
20 | | #include <absl/strings/escaping.h> |
21 | | #include <absl/strings/str_split.h> |
22 | | |
23 | | #include <algorithm> |
24 | | #include <fstream> |
25 | | #include <utility> |
26 | | #include <vector> |
27 | | |
28 | | #include "io/fs/local_file_system.h" |
29 | | #include "util/error_util.h" |
30 | | #include "util/string_parser.hpp" |
31 | | |
32 | | using std::pair; |
33 | | |
34 | | namespace doris { |
35 | | |
36 | 14 | bool CGroupUtil::cgroupsv1_enable() { |
37 | 14 | bool exists = true; |
38 | 14 | Status st = io::global_local_filesystem()->exists("/proc/cgroups", &exists); |
39 | 14 | return st.ok() && exists; |
40 | 14 | } |
41 | | |
42 | 16 | bool CGroupUtil::cgroupsv2_enable() { |
43 | 16 | #if defined(OS_LINUX) |
44 | | // This file exists iff the host has cgroups v2 enabled. |
45 | 16 | auto controllers_file = default_cgroups_mount / "cgroup.controllers"; |
46 | 16 | bool exists = true; |
47 | 16 | Status st = io::global_local_filesystem()->exists(controllers_file, &exists); |
48 | 16 | return st.ok() && exists; |
49 | | #else |
50 | | return false; |
51 | | #endif |
52 | 16 | } |
53 | | |
54 | 11 | Status CGroupUtil::find_global_cgroupv1(const std::string& subsystem, std::string* path) { |
55 | 11 | std::ifstream proc_cgroups("/proc/self/cgroup", std::ios::in); |
56 | 11 | std::string line; |
57 | 27 | while (true) { |
58 | 27 | if (proc_cgroups.fail()) { |
59 | 0 | return Status::CgroupError("Error reading /proc/self/cgroup: {}", get_str_err_msg()); |
60 | 27 | } else if (proc_cgroups.peek() == std::ifstream::traits_type::eof()) { |
61 | 0 | return Status::CgroupError("Could not find subsystem {} in /proc/self/cgroup", |
62 | 0 | subsystem); |
63 | 0 | } |
64 | | // The line format looks like this: |
65 | | // 4:memory:/user.slice |
66 | | // 9:cpu,cpuacct:/user.slice |
67 | | // so field size will be 3 |
68 | 27 | getline(proc_cgroups, line); |
69 | 27 | if (!proc_cgroups.good()) { |
70 | 0 | continue; |
71 | 0 | } |
72 | 27 | std::vector<std::string> fields = absl::StrSplit(line, ":"); |
73 | | // ":" in the path does not appear to be escaped - bail in the unusual case that |
74 | | // we get too many tokens. |
75 | 27 | if (fields.size() != 3) { |
76 | 0 | return Status::InvalidArgument( |
77 | 0 | "Could not parse line from /proc/self/cgroup - had {} > 3 tokens: '{}'", |
78 | 0 | fields.size(), line); |
79 | 0 | } |
80 | 27 | std::vector<std::string> subsystems = absl::StrSplit(fields[1], ","); |
81 | 27 | auto it = std::find(subsystems.begin(), subsystems.end(), subsystem); |
82 | 27 | if (it != subsystems.end()) { |
83 | 11 | *path = std::move(fields[2]); |
84 | 11 | return Status::OK(); |
85 | 11 | } |
86 | 27 | } |
87 | 11 | } |
88 | | |
89 | 22 | static Status unescape_path(const std::string& escaped, std::string* unescaped) { |
90 | 22 | std::string err; |
91 | 22 | if (!absl::CUnescape(escaped, unescaped, &err)) { |
92 | 0 | return Status::InvalidArgument("Could not unescape path '{}': {}", escaped, err); |
93 | 0 | } |
94 | 22 | return Status::OK(); |
95 | 22 | } |
96 | | |
97 | | Status CGroupUtil::find_cgroupv1_mounts(const std::string& subsystem, |
98 | 11 | pair<std::string, std::string>* result) { |
99 | 11 | std::ifstream mountinfo("/proc/self/mountinfo", std::ios::in); |
100 | 11 | std::string line; |
101 | 182 | while (true) { |
102 | 182 | if (mountinfo.fail() || mountinfo.bad()) { |
103 | 0 | return Status::CgroupError("Error reading /proc/self/mountinfo: {}", get_str_err_msg()); |
104 | 182 | } else if (mountinfo.eof()) { |
105 | 0 | return Status::CgroupError("Could not find subsystem {} in /proc/self/mountinfo", |
106 | 0 | subsystem); |
107 | 0 | } |
108 | | // The relevant lines look like below (see proc manpage for full documentation). The |
109 | | // first example is running outside of a container, the second example is running |
110 | | // inside a docker container. Field 3 is the path relative to the root CGroup on |
111 | | // the host and Field 4 is the mount point from this process's point of view. |
112 | | // 34 29 0:28 / /sys/fs/cgroup/memory rw,nosuid,nodev,noexec,relatime shared:15 - |
113 | | // cgroup cgroup rw,memory |
114 | | // 275 271 0:28 /docker/f23eee6f88c2ba99fcce /sys/fs/cgroup/memory |
115 | | // ro,nosuid,nodev,noexec,relatime master:15 - cgroup cgroup rw,memory |
116 | 182 | getline(mountinfo, line); |
117 | 182 | if (!mountinfo.good()) { |
118 | 0 | continue; |
119 | 0 | } |
120 | 182 | std::vector<std::string> fields = absl::StrSplit(line, " ", absl::SkipWhitespace()); |
121 | 182 | if (fields.size() < 7) { |
122 | 0 | return Status::InvalidArgument( |
123 | 0 | "Could not parse line from /proc/self/mountinfo - had {} > 7 tokens: '{}'", |
124 | 0 | fields.size(), line); |
125 | 0 | } |
126 | 182 | if (fields[fields.size() - 3] != "cgroup") { |
127 | 66 | continue; |
128 | 66 | } |
129 | | // This is a cgroup mount. Check if it's the mount we're looking for. |
130 | 116 | std::vector<std::string> cgroup_opts = |
131 | 116 | absl::StrSplit(fields[fields.size() - 1], ",", absl::SkipWhitespace()); |
132 | 116 | auto it = std::find(cgroup_opts.begin(), cgroup_opts.end(), subsystem); |
133 | 116 | if (it == cgroup_opts.end()) { |
134 | 105 | continue; |
135 | 105 | } |
136 | | // This is the right mount. |
137 | 11 | std::string mount_path, system_path; |
138 | 11 | RETURN_IF_ERROR(unescape_path(fields[4], &mount_path)); |
139 | 11 | RETURN_IF_ERROR(unescape_path(fields[3], &system_path)); |
140 | | // Strip trailing "/" so that both returned paths match in whether they have a |
141 | | // trailing "/". |
142 | 11 | if (system_path[system_path.size() - 1] == '/') { |
143 | 0 | system_path.pop_back(); |
144 | 0 | } |
145 | 11 | *result = {mount_path, system_path}; |
146 | 11 | return Status::OK(); |
147 | 11 | } |
148 | 11 | } |
149 | | |
150 | 11 | Status CGroupUtil::find_abs_cgroupv1_path(const std::string& subsystem, std::string* path) { |
151 | 11 | if (!cgroupsv1_enable()) { |
152 | 0 | return Status::InvalidArgument("cgroup is not enabled!"); |
153 | 0 | } |
154 | 11 | RETURN_IF_ERROR(find_global_cgroupv1(subsystem, path)); |
155 | 11 | pair<std::string, std::string> paths; |
156 | 11 | RETURN_IF_ERROR(find_cgroupv1_mounts(subsystem, &paths)); |
157 | 11 | const std::string& mount_path = paths.first; |
158 | 11 | const std::string& system_path = paths.second; |
159 | 11 | if (path->compare(0, system_path.size(), system_path) != 0) { |
160 | 0 | return Status::InvalidArgument("Expected CGroup path '{}' to start with '{}'", *path, |
161 | 0 | system_path); |
162 | 0 | } |
163 | 11 | path->replace(0, system_path.size(), mount_path); |
164 | 11 | return Status::OK(); |
165 | 11 | } |
166 | | |
167 | 0 | std::string CGroupUtil::cgroupv2_of_process() { |
168 | 0 | #if defined(OS_LINUX) |
169 | 0 | if (!cgroupsv2_enable()) { |
170 | 0 | return ""; |
171 | 0 | } |
172 | | // All PIDs assigned to a cgroup are in /sys/fs/cgroups/{cgroup_name}/cgroup.procs |
173 | | // A simpler way to get the membership is: |
174 | 0 | std::ifstream cgroup_name_file("/proc/self/cgroup"); |
175 | 0 | if (!cgroup_name_file.is_open()) { |
176 | 0 | return ""; |
177 | 0 | } |
178 | | // With cgroups v2, there will be a *single* line with prefix "0::/" |
179 | | // (see https://docs.kernel.org/admin-guide/cgroup-v2.html) |
180 | | // such as 0::/user.slice/user-1005.slice/session-213906.scope this is the cgroup name |
181 | | // it should be combined with the default cgroup mount point to get the full path to the cgroup, e.g. |
182 | | // /sys/fs/cgroup/user.slice/user-1005.slice/session-213906.scope |
183 | 0 | std::string cgroup; |
184 | 0 | std::getline(cgroup_name_file, cgroup); |
185 | 0 | static const std::string v2_prefix = "0::/"; |
186 | 0 | if (!cgroup.starts_with(v2_prefix)) { |
187 | 0 | return ""; |
188 | 0 | } |
189 | 0 | cgroup = cgroup.substr(v2_prefix.length()); |
190 | 0 | return cgroup; |
191 | | #else |
192 | | return ""; |
193 | | #endif |
194 | 0 | } |
195 | | |
196 | 0 | std::optional<std::string> CGroupUtil::get_cgroupsv2_path(const std::string& subsystem) { |
197 | 0 | #if defined(OS_LINUX) |
198 | 0 | if (!CGroupUtil::cgroupsv2_enable()) { |
199 | 0 | return {}; |
200 | 0 | } |
201 | | |
202 | 0 | std::string cgroup = CGroupUtil::cgroupv2_of_process(); |
203 | | // /sys/fs/cgroup/user.slice/user-1005.slice/session-213906.scope |
204 | 0 | auto current_cgroup = cgroup.empty() ? default_cgroups_mount : (default_cgroups_mount / cgroup); |
205 | | |
206 | | // Return the bottom-most nested current memory file. If there is no such file at the current |
207 | | // level, try again at the parent level as memory settings are inherited. |
208 | 0 | while (current_cgroup != default_cgroups_mount.parent_path()) { |
209 | 0 | if (std::filesystem::exists(current_cgroup / subsystem)) { |
210 | 0 | return {current_cgroup}; |
211 | 0 | } |
212 | 0 | current_cgroup = current_cgroup.parent_path(); |
213 | 0 | } |
214 | 0 | return {}; |
215 | | #else |
216 | | return {}; |
217 | | #endif |
218 | 0 | } |
219 | | |
220 | | Status CGroupUtil::read_int_line_from_cgroup_file(const std::filesystem::path& file_path, |
221 | 5 | int64_t* val) { |
222 | 5 | std::ifstream file_stream(file_path, std::ios::in); |
223 | 5 | if (!file_stream.is_open()) { |
224 | 1 | return Status::CgroupError("Error open {}", file_path.string()); |
225 | 1 | } |
226 | | |
227 | 4 | std::string line; |
228 | 4 | getline(file_stream, line); |
229 | 4 | if (file_stream.fail() || file_stream.bad()) { |
230 | 0 | return Status::CgroupError("Error reading {}: {}", file_path.string(), get_str_err_msg()); |
231 | 0 | } |
232 | 4 | StringParser::ParseResult pr; |
233 | | // Parse into an int64_t If it overflows, returning the max value of int64_t is ok because that |
234 | | // is effectively unlimited. |
235 | 4 | *val = StringParser::string_to_int<int64_t>(line.c_str(), line.size(), &pr); |
236 | 4 | if ((pr != StringParser::PARSE_SUCCESS && pr != StringParser::PARSE_OVERFLOW)) { |
237 | 0 | return Status::InvalidArgument("Failed to parse {} as int64: '{}'", file_path.string(), |
238 | 0 | line); |
239 | 0 | } |
240 | 4 | return Status::OK(); |
241 | 4 | } |
242 | | |
243 | | void CGroupUtil::read_int_metric_from_cgroup_file( |
244 | | const std::filesystem::path& file_path, |
245 | 5 | std::unordered_map<std::string, int64_t>& metrics_map) { |
246 | 5 | std::ifstream cgroup_file(file_path, std::ios::in); |
247 | 5 | std::string line; |
248 | 144 | while (cgroup_file.good() && !cgroup_file.eof()) { |
249 | 139 | getline(cgroup_file, line); |
250 | 139 | std::vector<std::string> fields = absl::StrSplit(line, " ", absl::SkipWhitespace()); |
251 | 139 | if (fields.size() < 2) { |
252 | 4 | continue; |
253 | 4 | } |
254 | 135 | std::string key = fields[0].substr(0, fields[0].size()); |
255 | | |
256 | 135 | StringParser::ParseResult result; |
257 | 135 | auto value = |
258 | 135 | StringParser::string_to_int<int64_t>(fields[1].data(), fields[1].size(), &result); |
259 | | |
260 | 135 | if (result == StringParser::PARSE_SUCCESS) { |
261 | 135 | if (fields.size() == 2) { |
262 | 135 | metrics_map[key] = value; |
263 | 135 | } else if (fields[2] == "kB") { |
264 | 0 | metrics_map[key] = value * 1024L; |
265 | 0 | } else { |
266 | 0 | LOG(WARNING) << "Unknown unit in cgroup file " << file_path.string() |
267 | 0 | << ", line: " << line; |
268 | 0 | } |
269 | 135 | } |
270 | 135 | } |
271 | 5 | if (cgroup_file.is_open()) { |
272 | 4 | cgroup_file.close(); |
273 | 4 | } |
274 | 5 | } |
275 | | |
276 | | Status CGroupUtil::read_string_line_from_cgroup_file(const std::filesystem::path& file_path, |
277 | 6 | std::string* line_ptr) { |
278 | 6 | std::ifstream file_stream(file_path, std::ios::in); |
279 | 6 | if (!file_stream.is_open()) { |
280 | 0 | return Status::CgroupError("Error open {}", file_path.string()); |
281 | 0 | } |
282 | 6 | std::string line; |
283 | 6 | getline(file_stream, line); |
284 | 6 | if (file_stream.fail() || file_stream.bad()) { |
285 | 0 | return Status::CgroupError("Error reading {}: {}", file_path.string(), get_str_err_msg()); |
286 | 0 | } |
287 | 6 | *line_ptr = line; |
288 | 6 | return Status::OK(); |
289 | 6 | } |
290 | | |
291 | 8 | Status CGroupUtil::parse_cpuset_line(std::string cpuset_line, int* cpu_count_ptr) { |
292 | 8 | if (cpuset_line.empty()) { |
293 | 0 | return Status::CgroupError("cpuset line is empty"); |
294 | 0 | } |
295 | 8 | std::vector<std::string> ranges; |
296 | 8 | boost::split(ranges, cpuset_line, boost::is_any_of(",")); |
297 | 8 | int cpu_count = 0; |
298 | | |
299 | 15 | for (const std::string& range : ranges) { |
300 | 15 | std::vector<std::string> cpu_values; |
301 | 15 | boost::split(cpu_values, range, boost::is_any_of("-")); |
302 | | |
303 | 15 | if (cpu_values.size() == 2) { |
304 | 10 | int start = std::stoi(cpu_values[0]); |
305 | 10 | int end = std::stoi(cpu_values[1]); |
306 | 10 | cpu_count += (end - start) + 1; |
307 | 10 | } else { |
308 | 5 | cpu_count++; |
309 | 5 | } |
310 | 15 | } |
311 | 8 | *cpu_count_ptr = cpu_count; |
312 | 8 | return Status::OK(); |
313 | 8 | } |
314 | | |
315 | 2 | int CGroupUtil::get_cgroup_limited_cpu_number(int physical_cores) { |
316 | 2 | if (physical_cores <= 0) { |
317 | 0 | return physical_cores; |
318 | 0 | } |
319 | 2 | int ret = physical_cores; |
320 | 2 | #if defined(OS_LINUX) |
321 | | // For cgroup v2 |
322 | | // Child cgroup's cpu.max may bigger than parent group's cpu.max, |
323 | | // so it should look up from current cgroup to top group. |
324 | | // For cpuset, child cgroup's cpuset.cpus could not bigger thant parent's cpuset.cpus. |
325 | 2 | if (CGroupUtil::cgroupsv2_enable()) { |
326 | 0 | std::string cgroupv2_process_path = CGroupUtil::cgroupv2_of_process(); |
327 | 0 | if (cgroupv2_process_path.empty()) { |
328 | 0 | return ret; |
329 | 0 | } |
330 | 0 | std::filesystem::path current_cgroup_path = (default_cgroups_mount / cgroupv2_process_path); |
331 | 0 | ret = get_cgroup_v2_cpu_quota_number(current_cgroup_path, default_cgroups_mount, ret); |
332 | |
|
333 | 0 | current_cgroup_path = (default_cgroups_mount / cgroupv2_process_path); |
334 | 0 | ret = get_cgroup_v2_cpuset_number(current_cgroup_path, default_cgroups_mount, ret); |
335 | 2 | } else if (CGroupUtil::cgroupsv1_enable()) { |
336 | | // cpu quota, should find first not empty config from current path to top. |
337 | | // because if a process attach to current cgroup, its cpu quota may not be set. |
338 | 2 | std::string cpu_quota_path = ""; |
339 | 2 | Status cpu_quota_ret = CGroupUtil::find_abs_cgroupv1_path("cpu", &cpu_quota_path); |
340 | 2 | if (cpu_quota_ret.ok() && !cpu_quota_path.empty()) { |
341 | 2 | std::filesystem::path current_cgroup_path = cpu_quota_path; |
342 | 2 | ret = get_cgroup_v1_cpu_quota_number(current_cgroup_path, default_cgroups_mount, ret); |
343 | 2 | } |
344 | | |
345 | | //cpuset |
346 | | // just lookup current process cgroup path is enough |
347 | | // because if a process attach to current cgroup, its cpuset.cpus must be set. |
348 | 2 | std::string cpuset_path = ""; |
349 | 2 | Status cpuset_ret = CGroupUtil::find_abs_cgroupv1_path("cpuset", &cpuset_path); |
350 | 2 | if (cpuset_ret.ok() && !cpuset_path.empty()) { |
351 | 2 | std::filesystem::path current_path = cpuset_path; |
352 | 2 | ret = get_cgroup_v1_cpuset_number(current_path, ret); |
353 | 2 | } |
354 | 2 | } |
355 | 2 | #endif |
356 | 2 | return ret; |
357 | 2 | } |
358 | | |
359 | | int CGroupUtil::get_cgroup_v2_cpu_quota_number(std::filesystem::path& current_path, |
360 | | const std::filesystem::path& default_cg_mout_path, |
361 | 4 | int cpu_num) { |
362 | 4 | int ret = cpu_num; |
363 | 12 | while (current_path != default_cg_mout_path.parent_path()) { |
364 | 8 | std::ifstream cpu_max_file(current_path / "cpu.max"); |
365 | 8 | if (cpu_max_file.is_open()) { |
366 | 8 | std::string cpu_limit_str; |
367 | 8 | double cpu_period; |
368 | 8 | cpu_max_file >> cpu_limit_str >> cpu_period; |
369 | 8 | if (cpu_limit_str != "max" && cpu_period != 0) { |
370 | 5 | double cpu_limit = std::stod(cpu_limit_str); |
371 | 5 | ret = std::min(static_cast<int>(std::ceil(cpu_limit / cpu_period)), ret); |
372 | 5 | } |
373 | 8 | } |
374 | 8 | current_path = current_path.parent_path(); |
375 | 8 | } |
376 | 4 | return ret; |
377 | 4 | } |
378 | | |
379 | | int CGroupUtil::get_cgroup_v2_cpuset_number(std::filesystem::path& current_path, |
380 | | const std::filesystem::path& default_cg_mout_path, |
381 | 2 | int cpu_num) { |
382 | 2 | int ret = cpu_num; |
383 | 3 | while (current_path != default_cg_mout_path.parent_path()) { |
384 | 3 | std::ifstream cpuset_cpus_file(current_path / "cpuset.cpus.effective"); |
385 | 3 | current_path = current_path.parent_path(); |
386 | 3 | if (cpuset_cpus_file.is_open()) { |
387 | 3 | std::string cpuset_line; |
388 | 3 | cpuset_cpus_file >> cpuset_line; |
389 | 3 | if (cpuset_line.empty()) { |
390 | 1 | continue; |
391 | 1 | } |
392 | 2 | int cpus_count = 0; |
393 | 2 | static_cast<void>(CGroupUtil::parse_cpuset_line(cpuset_line, &cpus_count)); |
394 | 2 | ret = std::min(cpus_count, ret); |
395 | 2 | break; |
396 | 3 | } |
397 | 3 | } |
398 | 2 | return ret; |
399 | 2 | } |
400 | | |
401 | | int CGroupUtil::get_cgroup_v1_cpu_quota_number(std::filesystem::path& current_path, |
402 | | const std::filesystem::path& default_cg_mout_path, |
403 | 5 | int cpu_num) { |
404 | 5 | int ret = cpu_num; |
405 | 12 | while (current_path != default_cg_mout_path.parent_path()) { |
406 | 9 | std::ifstream cpu_quota_file(current_path / "cpu.cfs_quota_us"); |
407 | 9 | std::ifstream cpu_period_file(current_path / "cpu.cfs_period_us"); |
408 | 9 | if (cpu_quota_file.is_open() && cpu_period_file.is_open()) { |
409 | 7 | double cpu_quota_value; |
410 | 7 | double cpu_period_value; |
411 | 7 | cpu_quota_file >> cpu_quota_value; |
412 | 7 | cpu_period_file >> cpu_period_value; |
413 | 7 | if (cpu_quota_value > 0 && cpu_period_value > 0) { |
414 | 2 | ret = std::min(ret, |
415 | 2 | static_cast<int>(std::ceil(cpu_quota_value / cpu_period_value))); |
416 | 2 | break; |
417 | 2 | } |
418 | 7 | } |
419 | 7 | current_path = current_path.parent_path(); |
420 | 7 | } |
421 | 5 | return ret; |
422 | 5 | } |
423 | | |
424 | 3 | int CGroupUtil::get_cgroup_v1_cpuset_number(std::filesystem::path& current_path, int cpu_num) { |
425 | 3 | int ret = cpu_num; |
426 | 3 | std::string cpuset_line = ""; |
427 | 3 | Status cpuset_ret = CGroupUtil::read_string_line_from_cgroup_file( |
428 | 3 | (current_path / "cpuset.cpus"), &cpuset_line); |
429 | 3 | if (cpuset_ret.ok() && !cpuset_line.empty()) { |
430 | 3 | int cpuset_count = 0; |
431 | 3 | static_cast<void>(CGroupUtil::parse_cpuset_line(cpuset_line, &cpuset_count)); |
432 | 3 | if (cpuset_count > 0) { |
433 | 3 | ret = std::min(ret, cpuset_count); |
434 | 3 | } |
435 | 3 | } |
436 | 3 | return ret; |
437 | 3 | } |
438 | | |
439 | | } // namespace doris |