Module: Wurk::CpuCount
- Defined in:
- lib/wurk/cpu_count.rb
Overview
How many CPUs this process may actually use — the default swarm size.
Etc.nprocessors reports the host's cores (or the affinity mask), not the
container's share of them: a pod limited to 2 CPUs on a 64-core node reads
64, and a swarm that forks one child per core then runs 64 processes on 2
CPUs' worth of CFS quota — throttled, with 32x the memory. The quota lives
in the cgroup, so it is read from there and the smaller of the two wins.
cgroup v2: cpu.max is <quota> <period> or max <period>, read in this
process's own cgroup and every ancestor up to the mount root (a limit set on
a parent slice binds the child too), smallest wins. cgroup v1:
cpu.cfs_quota_us (-1 = unlimited) over cpu.cfs_period_us, in the cpu
controller's mount. Either way ceil(quota / period), floored at 1: a
1.5-CPU quota can keep two processes busy part of the time, and rounding a
0.5-CPU quota down to zero would fork nothing.
Defined Under Namespace
Classes: Result
Constant Summary collapse
- ROOT =
'/sys/fs/cgroup'- PROC_SELF_CGROUP =
'/proc/self/cgroup'- V1_CONTROLLER_DIRS =
%w[cpu cpu,cpuacct cpuacct,cpu].freeze
Class Method Summary collapse
- .ceil_ratio(quota, period) ⇒ Object
- .cgroup_v1_quota(root) ⇒ Object
- .cgroup_v2_quota(root, proc_cgroup) ⇒ Object
- .detect(root: ROOT, proc_cgroup: PROC_SELF_CGROUP, nprocessors: Etc.nprocessors) ⇒ Object
- .read(path) ⇒ Object
-
.v2_dirs(root, proc_cgroup) ⇒ Object
0::<path>is the v2 entry. - .v2_limit(file) ⇒ Object
Class Method Details
.ceil_ratio(quota, period) ⇒ Object
81 82 83 84 85 86 87 88 89 |
# File 'lib/wurk/cpu_count.rb', line 81 def ceil_ratio(quota, period) quota = Integer(quota.to_s.strip, 10) period = Integer(period.to_s.strip, 10) return nil unless quota.positive? && period.positive? (quota + period - 1) / period rescue ArgumentError nil end |
.cgroup_v1_quota(root) ⇒ Object
69 70 71 72 73 74 75 76 77 78 79 |
# File 'lib/wurk/cpu_count.rb', line 69 def cgroup_v1_quota(root) V1_CONTROLLER_DIRS.each do |dir| quota = read(File.join(root, dir, 'cpu.cfs_quota_us')) next unless quota return nil if quota.strip.start_with?('-') return ceil_ratio(quota, read(File.join(root, dir, 'cpu.cfs_period_us'))) end nil end |
.cgroup_v2_quota(root, proc_cgroup) ⇒ Object
42 43 44 |
# File 'lib/wurk/cpu_count.rb', line 42 def cgroup_v2_quota(root, proc_cgroup) v2_dirs(root, proc_cgroup).filter_map { |dir| v2_limit(File.join(dir, 'cpu.max')) }.min end |
.detect(root: ROOT, proc_cgroup: PROC_SELF_CGROUP, nprocessors: Etc.nprocessors) ⇒ Object
30 31 32 33 34 35 36 37 38 39 40 |
# File 'lib/wurk/cpu_count.rb', line 30 def detect(root: ROOT, proc_cgroup: PROC_SELF_CGROUP, nprocessors: Etc.nprocessors) quota = cgroup_v2_quota(root, proc_cgroup) source = 'cgroup v2 cpu.max' unless quota quota = cgroup_v1_quota(root) source = 'cgroup v1 cpu.cfs_quota_us' end return Result.new(nprocessors, 'Etc.nprocessors') if quota.nil? || quota >= nprocessors Result.new(quota, source) end |
.read(path) ⇒ Object
91 92 93 94 95 |
# File 'lib/wurk/cpu_count.rb', line 91 def read(path) File.read(path) rescue SystemCallError, IOError nil end |
.v2_dirs(root, proc_cgroup) ⇒ Object
0::<path> is the v2 entry. Inside a container with its own cgroup
namespace the path is / and the limit sits at the mount root; on a host
(or without the namespace) it sits under the full path.
49 50 51 52 53 54 55 56 57 58 59 60 |
# File 'lib/wurk/cpu_count.rb', line 49 def v2_dirs(root, proc_cgroup) line = read(proc_cgroup)&.lines&.find { |l| l.start_with?('0::') } path = line ? line.strip.delete_prefix('0::') : '/' dirs = [] loop do dirs << File.join(root, path) break if path == '/' || path.empty? path = File.dirname(path) end dirs.uniq end |
.v2_limit(file) ⇒ Object
62 63 64 65 66 67 |
# File 'lib/wurk/cpu_count.rb', line 62 def v2_limit(file) quota, period = read(file)&.split return nil if quota.nil? || quota == 'max' ceil_ratio(quota, period) end |