Skip to content

Commit c57e7f0

Browse files
waltossgiordano
authored andcommitted
linux: fix uv_available_parallelism using cgroup (libuv#4278)
uv_available_parallelism does not handle container cpu limit set by systems like Docker or Kubernetes. This patch fixes this limitation by comparing the amount of available cpus returned by syscall with the quota of cpus available defined in the cgroup. Fixes: libuv#4146 (cherry picked from commit 6b56200)
1 parent e763959 commit c57e7f0

File tree

4 files changed

+188
-2
lines changed

4 files changed

+188
-2
lines changed

src/unix/core.c

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1869,6 +1869,8 @@ unsigned int uv_available_parallelism(void) {
18691869
#ifdef __linux__
18701870
cpu_set_t set;
18711871
long rc;
1872+
double rc_with_cgroup;
1873+
uv__cpu_constraint c = {0, 0, 0.0};
18721874

18731875
memset(&set, 0, sizeof(set));
18741876

@@ -1880,8 +1882,13 @@ unsigned int uv_available_parallelism(void) {
18801882
rc = CPU_COUNT(&set);
18811883
else
18821884
rc = sysconf(_SC_NPROCESSORS_ONLN);
1883-
1884-
if (rc < 1)
1885+
1886+
if (uv__get_constrained_cpu(&c) == 0 && c.period_length > 0) {
1887+
rc_with_cgroup = (double)c.quota_per_period / c.period_length * c.proportions;
1888+
if (rc_with_cgroup < rc)
1889+
rc = (long)rc_with_cgroup; /* Casting is safe since rc_with_cgroup < rc < LONG_MAX */
1890+
}
1891+
if (rc < 1)
18851892
rc = 1;
18861893

18871894
return (unsigned) rc;

src/unix/internal.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,4 +461,14 @@ uv__fs_copy_file_range(int fd_in,
461461
#define UV__CPU_AFFINITY_SUPPORTED 0
462462
#endif
463463

464+
#ifdef __linux__
465+
typedef struct {
466+
long long quota_per_period;
467+
long long period_length;
468+
double proportions;
469+
} uv__cpu_constraint;
470+
471+
int uv__get_constrained_cpu(uv__cpu_constraint* constraint);
472+
#endif
473+
464474
#endif /* UV_UNIX_INTERNAL_H_ */

src/unix/linux.c

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2251,6 +2251,136 @@ uint64_t uv_get_available_memory(void) {
22512251
}
22522252

22532253

2254+
static int uv__get_cgroupv2_constrained_cpu(const char* cgroup,
2255+
uv__cpu_constraint* constraint) {
2256+
char path[256];
2257+
char buf[1024];
2258+
unsigned int weight;
2259+
int cgroup_size;
2260+
const char* cgroup_trimmed;
2261+
char quota_buf[16];
2262+
2263+
if (strncmp(cgroup, "0::/", 4) != 0)
2264+
return UV_EINVAL;
2265+
2266+
/* Trim ending \n by replacing it with a 0 */
2267+
cgroup_trimmed = cgroup + sizeof("0::/") - 1; /* Skip the prefix "0::/" */
2268+
cgroup_size = (int)strcspn(cgroup_trimmed, "\n"); /* Find the first slash */
2269+
2270+
/* Construct the path to the cpu.max file */
2271+
snprintf(path, sizeof(path), "/sys/fs/cgroup/%.*s/cpu.max", cgroup_size,
2272+
cgroup_trimmed);
2273+
2274+
/* Read cpu.max */
2275+
if (uv__slurp(path, buf, sizeof(buf)) < 0)
2276+
return UV_EIO;
2277+
2278+
if (sscanf(buf, "%15s %llu", quota_buf, &constraint->period_length) != 2)
2279+
return UV_EINVAL;
2280+
2281+
if (strncmp(quota_buf, "max", 3) == 0)
2282+
constraint->quota_per_period = LLONG_MAX;
2283+
else if (sscanf(quota_buf, "%lld", &constraint->quota_per_period) != 1)
2284+
return UV_EINVAL; // conversion failed
2285+
2286+
/* Construct the path to the cpu.weight file */
2287+
snprintf(path, sizeof(path), "/sys/fs/cgroup/%.*s/cpu.weight", cgroup_size,
2288+
cgroup_trimmed);
2289+
2290+
/* Read cpu.weight */
2291+
if (uv__slurp(path, buf, sizeof(buf)) < 0)
2292+
return UV_EIO;
2293+
2294+
if (sscanf(buf, "%u", &weight) != 1)
2295+
return UV_EINVAL;
2296+
2297+
constraint->proportions = (double)weight / 100.0;
2298+
2299+
return 0;
2300+
}
2301+
2302+
static char* uv__cgroup1_find_cpu_controller(const char* cgroup,
2303+
int* cgroup_size) {
2304+
/* Seek to the cpu controller line. */
2305+
char* cgroup_cpu = strstr(cgroup, ":cpu,");
2306+
2307+
if (cgroup_cpu != NULL) {
2308+
/* Skip the controller prefix to the start of the cgroup path. */
2309+
cgroup_cpu += sizeof(":cpu,") - 1;
2310+
/* Determine the length of the cgroup path, excluding the newline. */
2311+
*cgroup_size = (int)strcspn(cgroup_cpu, "\n");
2312+
}
2313+
2314+
return cgroup_cpu;
2315+
}
2316+
2317+
static int uv__get_cgroupv1_constrained_cpu(const char* cgroup,
2318+
uv__cpu_constraint* constraint) {
2319+
char path[256];
2320+
char buf[1024];
2321+
unsigned int shares;
2322+
int cgroup_size;
2323+
char* cgroup_cpu;
2324+
2325+
cgroup_cpu = uv__cgroup1_find_cpu_controller(cgroup, &cgroup_size);
2326+
2327+
if (cgroup_cpu == NULL)
2328+
return UV_EIO;
2329+
2330+
/* Construct the path to the cpu.cfs_quota_us file */
2331+
snprintf(path, sizeof(path), "/sys/fs/cgroup/%.*s/cpu.cfs_quota_us",
2332+
cgroup_size, cgroup_cpu);
2333+
2334+
if (uv__slurp(path, buf, sizeof(buf)) < 0)
2335+
return UV_EIO;
2336+
2337+
if (sscanf(buf, "%lld", &constraint->quota_per_period) != 1)
2338+
return UV_EINVAL;
2339+
2340+
/* Construct the path to the cpu.cfs_period_us file */
2341+
snprintf(path, sizeof(path), "/sys/fs/cgroup/%.*s/cpu.cfs_period_us",
2342+
cgroup_size, cgroup_cpu);
2343+
2344+
/* Read cpu.cfs_period_us */
2345+
if (uv__slurp(path, buf, sizeof(buf)) < 0)
2346+
return UV_EIO;
2347+
2348+
if (sscanf(buf, "%lld", &constraint->period_length) != 1)
2349+
return UV_EINVAL;
2350+
2351+
/* Construct the path to the cpu.shares file */
2352+
snprintf(path, sizeof(path), "/sys/fs/cgroup/%.*s/cpu.shares", cgroup_size,
2353+
cgroup_cpu);
2354+
2355+
/* Read cpu.shares */
2356+
if (uv__slurp(path, buf, sizeof(buf)) < 0)
2357+
return UV_EIO;
2358+
2359+
if (sscanf(buf, "%u", &shares) != 1)
2360+
return UV_EINVAL;
2361+
2362+
constraint->proportions = (double)shares / 1024.0;
2363+
2364+
return 0;
2365+
}
2366+
2367+
int uv__get_constrained_cpu(uv__cpu_constraint* constraint) {
2368+
char cgroup[1024];
2369+
2370+
/* Read the cgroup from /proc/self/cgroup */
2371+
if (uv__slurp("/proc/self/cgroup", cgroup, sizeof(cgroup)) < 0)
2372+
return UV_EIO;
2373+
2374+
/* Check if the system is using cgroup v2 by examining /proc/self/cgroup
2375+
* The entry for cgroup v2 is always in the format "0::$PATH"
2376+
* see https://docs.kernel.org/admin-guide/cgroup-v2.html */
2377+
if (strncmp(cgroup, "0::/", 4) == 0)
2378+
return uv__get_cgroupv2_constrained_cpu(cgroup, constraint);
2379+
else
2380+
return uv__get_cgroupv1_constrained_cpu(cgroup, constraint);
2381+
}
2382+
2383+
22542384
void uv_loadavg(double avg[3]) {
22552385
struct sysinfo info;
22562386
char buf[128]; /* Large enough to hold all of /proc/loadavg. */

test/test-platform-output.c

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,45 @@ TEST_IMPL(platform_output) {
9090
ASSERT_GE(par, 1);
9191
printf("uv_available_parallelism: %u\n", par);
9292

93+
#ifdef __linux__
94+
FILE* file;
95+
int cgroup_version = 0;
96+
unsigned int cgroup_par = 0;
97+
uint64_t quota, period;
98+
99+
// Attempt to parse cgroup v2 to deduce parallelism constraints
100+
file = fopen("/sys/fs/cgroup/cpu.max", "r");
101+
if (file) {
102+
if (fscanf(file, "%lu %lu", &quota, &period) == 2 && quota > 0) {
103+
cgroup_version = 2;
104+
cgroup_par = (unsigned int)(quota / period);
105+
}
106+
fclose(file);
107+
}
108+
109+
// If cgroup v2 wasn't present, try parsing cgroup v1
110+
if (cgroup_version == 0) {
111+
file = fopen("/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_quota_us", "r");
112+
if (file) {
113+
if (fscanf(file, "%lu", &quota) == 1 && quota > 0) {
114+
fclose(file);
115+
file = fopen("/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_period_us", "r");
116+
if (file && fscanf(file, "%lu", &period) == 1) {
117+
cgroup_version = 1;
118+
cgroup_par = (unsigned int)(quota / period);
119+
}
120+
}
121+
if (file) fclose(file);
122+
}
123+
}
124+
125+
// If we found cgroup parallelism constraints, assert and print them
126+
if (cgroup_par > 0) {
127+
ASSERT_GE(par, cgroup_par);
128+
printf("cgroup v%d available parallelism: %u\n", cgroup_version, cgroup_par);
129+
}
130+
#endif
131+
93132
err = uv_cpu_info(&cpus, &count);
94133
#if defined(__CYGWIN__) || defined(__MSYS__)
95134
ASSERT_EQ(err, UV_ENOSYS);

0 commit comments

Comments
 (0)