Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
190 changes: 172 additions & 18 deletions logstash-core/lib/logstash/instrument/periodic_poller/cgroup.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,21 +38,26 @@ def override(other)
end

## `/proc/self/cgroup` contents look like this
# 5:cpu,cpuacct:/
# 4:cpuset:/
# 2:net_cls,net_prio:/
# 0::/user.slice/user-1000.slice/session-932.scope
## e.g. N:controller:/path-to-info
# we find the controller and path
# we skip the line without a controller e.g. 0::/path
# we assume there are these symlinks:
# `/sys/fs/cgroup/cpu` -> `/sys/fs/cgroup/cpu,cpuacct
# `/sys/fs/cgroup/cpuacct` -> `/sys/fs/cgroup/cpu,cpuacct
# cgroupv1 (per-controller hierarchies):
# 5:cpu,cpuacct:/
# 4:cpuset:/
# 2:net_cls,net_prio:/
# 0::/user.slice/user-1000.slice/session-932.scope
# cgroupv2 (unified hierarchy):
# 0::/path
#
# For v1, we find the controller and path from lines matching N:controller:/path.
# We assume these symlinks exist:
# `/sys/fs/cgroup/cpu` -> `/sys/fs/cgroup/cpu,cpuacct`
# `/sys/fs/cgroup/cpuacct` -> `/sys/fs/cgroup/cpu,cpuacct`
# For v2, the 0::/path line identifies the cgroup path under the unified
# hierarchy at /sys/fs/cgroup. Data files (cpu.stat, cpu.max) live there.

CGROUP_FILE = "/proc/self/cgroup"
CPUACCT_DIR = "/sys/fs/cgroup/cpuacct"
CPU_DIR = "/sys/fs/cgroup/cpu"
CRITICAL_PATHS = [CGROUP_FILE, CPUACCT_DIR, CPU_DIR]
CGROUP_V2_DIR = "/sys/fs/cgroup"

CONTROLLER_CPUACCT_LABEL = "cpuacct"
CONTROLLER_CPU_LABEL = "cpu"
Expand Down Expand Up @@ -88,6 +93,42 @@ def controller_groups
end
end

class CGroupV2Resources
CGROUP_V2_RE = Regexp.compile("^0::(/.*)")

def cgroup_available?
path = resolve_v2_path
return false if path.nil?
cpu_override = Override.new("ls.cgroup.cpu.path.override")
resolved = cpu_override.override(path)
::File.exist?(::File.join(CGROUP_V2_DIR, resolved, "cpu.stat"))
end

def controller_groups
path = resolve_v2_path
return {} if path.nil?
{
CONTROLLER_CPU_LABEL => CpuResourceV2.new(path),
CONTROLLER_CPUACCT_LABEL => CpuAcctResourceV2.new(path)
}
end

private

def resolve_v2_path
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will this be called often? Should we set an instance variable once then use that for subsequent calls?

@v2_path ||= read_v2_path
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice

end

def read_v2_path
return nil unless ::File.exist?(CGROUP_FILE)
IO.readlines(CGROUP_FILE).each do |line|
match = CGROUP_V2_RE.match(line.strip)
return match[1] if match
end
nil
end
end

module ControllerResource
attr_reader :base_path, :override, :offset_path

Expand Down Expand Up @@ -138,6 +179,27 @@ def cpuacct_usage
end
end

class CpuAcctResourceV2
include LogStash::Util::Loggable
include ControllerResource
def initialize(original_path)
common_initialize(CGROUP_V2_DIR, "ls.cgroup.cpuacct.path.override", original_path)
end

def to_hash
{:control_group => offset_path, :usage_nanos => cpuacct_usage}
end
private
def cpuacct_usage
lines = call_if_file_exists(:read_lines, "cpu.stat", [])
lines.each do |line|
fields = line.split(/\s+/)
return fields[1].to_i * 1000 if fields.first == "usage_usec"
end
-1
end
end

class CpuResource
include LogStash::Util::Loggable
include ControllerResource
Expand Down Expand Up @@ -170,6 +232,52 @@ def build_cpu_stats_hash
end
end

class CpuResourceV2
include LogStash::Util::Loggable
include ControllerResource
def initialize(original_path)
common_initialize(CGROUP_V2_DIR, "ls.cgroup.cpu.path.override", original_path)
end

def to_hash
{
:control_group => offset_path,
:cfs_period_micros => cfs_period_us,
:cfs_quota_micros => cfs_quota_us,
:stat => build_cpu_stats_hash
}
end
private
def cfs_period_us
read_cpu_max[1]
end

def cfs_quota_us
read_cpu_max[0]
end

def read_cpu_max
@cpu_max ||= begin
line = call_if_file_exists(:read_lines, "cpu.max", []).first
if line.nil?
[-1, -1]
else
parts = line.split(/\s+/)
quota = parts[0] == "max" ? -1 : parts[0].to_i
period = parts[1].to_i
[quota, period]
end
end
end

def build_cpu_stats_hash
stats = CpuStatsV2.new
lines = call_if_file_exists(:read_lines, "cpu.stat", [])
stats.update(lines)
stats.to_hash
end
end

class UnimplementedResource
attr_reader :controller, :original_path

Expand Down Expand Up @@ -210,19 +318,47 @@ def to_hash
end
end

class CpuStatsV2
def initialize
@number_of_elapsed_periods = -1
@number_of_times_throttled = -1
@time_throttled_nanos = -1
end

def update(lines)
lines.each do |line|
fields = line.split(/\s+/)
next unless fields.size > 1
case fields.first
when "nr_periods" then @number_of_elapsed_periods = fields[1].to_i
when "nr_throttled" then @number_of_times_throttled = fields[1].to_i
when "throttled_usec" then @time_throttled_nanos = fields[1].to_i * 1000
end
end
end

def to_hash
{
:number_of_elapsed_periods => @number_of_elapsed_periods,
:number_of_times_throttled => @number_of_times_throttled,
:time_throttled_nanos => @time_throttled_nanos
}
end
end

CGROUP_RESOURCES = CGroupResources.new
CGROUP_V2_RESOURCES = CGroupV2Resources.new

class << self
def get_all
unless CGROUP_RESOURCES.cgroup_available?
logger.debug("One or more required cgroup files or directories not found: #{CRITICAL_PATHS.join(', ')}")
return
end
resolve_resources unless @resolved
return nil if @active_resources.nil?

groups = CGROUP_RESOURCES.controller_groups
groups = @active_resources.controller_groups

if groups.empty?
logger.debug("The main cgroup file did not have any controllers: #{CGROUP_FILE}")
logger.debug("#{@active_label}: no controllers found") unless @logged_empty
@logged_empty = true
return
end

Expand All @@ -231,15 +367,33 @@ def get_all
next unless controller.implemented?
cgroups_stats[name.to_sym] = controller.to_hash
end
cgroups_stats
cgroups_stats.empty? ? nil : cgroups_stats
rescue => e
logger.debug("Error, cannot retrieve cgroups information", :exception => e.class.name, :message => e.message, :backtrace => e.backtrace.take(4)) if logger.debug?
logger.debug("Error, cannot retrieve #{@active_label} cgroups information", :exception => e.class.name, :message => e.message, :backtrace => e.backtrace.take(4)) if logger.debug?
nil
end

def get
get_all
end

private

def resolve_resources
@resolved = true
if CGROUP_RESOURCES.cgroup_available?
@active_resources = CGROUP_RESOURCES
@active_label = "cgroupv1"
logger.debug("using cgroupv1")
elsif CGROUP_V2_RESOURCES.cgroup_available?
@active_resources = CGROUP_V2_RESOURCES
@active_label = "cgroupv2"
logger.debug("using cgroupv2")
else
@active_resources = nil
logger.debug("no cgroup support detected (neither v1 nor v2)")
end
end
end
end
end end end
Loading
Loading