-
Notifications
You must be signed in to change notification settings - Fork 852
Improve CPU metrics calculations for CgroupV2 #6289
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 9 commits
0f277b3
567b639
df3a078
9b4783b
6292c24
d3a6d37
c6e8890
d1caa64
eb4fbc1
ec85353
3bfd050
189c27c
1f657d8
fe71558
e092c51
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,6 +21,10 @@ | |
| { | ||
| private const int Thousand = 1000; | ||
| private const int CpuShares = 1024; | ||
| private const string PathPrefix = "/sys/fs/cgroup"; | ||
nivebb8 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| private const string CpuStat = "cpu.stat"; // File containing CPU usage in nanoseconds. | ||
| private const string CpuLimit = "cpu.max"; // File with amount of CPU time available to the group along with the accounting period in microseconds. | ||
| private const string CpuRequest = "cpu.weight"; // CPU weights, also known as shares in cgroup v1, is used for resource allocation. | ||
| private static readonly ObjectPool<BufferWriter<char>> _sharedBufferWriterPool = BufferWriterPool.CreateBufferWriterPool<char>(); | ||
|
|
||
| /// <remarks> | ||
|
|
@@ -86,15 +90,53 @@ | |
| /// </summary> | ||
| private static readonly FileInfo _cpuPodWeight = new("/sys/fs/cgroup/cpu.weight"); | ||
|
|
||
| private static readonly FileInfo _cpuActualSelfSliceProcFile = new("/proc/self/cgroup"); | ||
nivebb8 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| private readonly IFileSystem _fileSystem; | ||
| private readonly long _userHz; | ||
| // Cache for the trimmed path string to avoid repeated file reads and processing | ||
|
Check failure on line 97 in src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs
|
||
| private string _cachedCgroupPath; | ||
|
|
||
| public LinuxUtilizationParserCgroupV2(IFileSystem fileSystem, IUserHz userHz) | ||
|
Check failure on line 100 in src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs
|
||
| { | ||
| _fileSystem = fileSystem; | ||
| _userHz = userHz.Value; | ||
| } | ||
|
|
||
| public string GetCgroupActualSlicePath(string filename) | ||
nivebb8 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| { | ||
| // If we've already parsed the path, use the cached value | ||
| if (_cachedCgroupPath != null) | ||
| { | ||
| return $"{PathPrefix}{_cachedCgroupPath}{filename}"; | ||
| } | ||
|
|
||
| using ReturnableBufferWriter<char> bufferWriter = new(_sharedBufferWriterPool); | ||
|
|
||
| // Read the content of the file | ||
| _fileSystem.ReadFirstLine(_cpuActualSelfSliceProcFile, bufferWriter.Buffer); | ||
| ReadOnlySpan<char> fileContent = bufferWriter.Buffer.WrittenSpan; | ||
|
|
||
| // Ensure the file content is not empty | ||
| if (fileContent.IsEmpty) | ||
| { | ||
| Throw.InvalidOperationException($"The file '{_cpuActualSelfSliceProcFile}' is empty or could not be read."); | ||
| } | ||
|
|
||
| // Find the index of the first colon (:) | ||
| int colonIndex = fileContent.LastIndexOf(':'); | ||
| if (colonIndex == -1 || colonIndex + 1 >= fileContent.Length) | ||
| { | ||
| Throw.InvalidOperationException($"Invalid format in file '{_cpuActualSelfSliceProcFile}'. Expected content with ':' separator."); | ||
| } | ||
|
|
||
| // Extract the part after the last colon and cache it for future use | ||
| ReadOnlySpan<char> trimmedPath = fileContent.Slice(colonIndex + 1); | ||
| _cachedCgroupPath = trimmedPath.ToString().TrimEnd('/') + "/"; | ||
nivebb8 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
|
Check failure on line 136 in src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs
|
||
| return $"{PathPrefix}{_cachedCgroupPath}{filename}"; | ||
| } | ||
|
|
||
| public long GetCgroupCpuUsageInNanoseconds() | ||
| { | ||
| // The value we are interested in starts with this. We just want to make sure it is true. | ||
|
|
@@ -129,6 +171,44 @@ | |
| return microseconds * Thousand; | ||
| } | ||
|
|
||
| public long GetCgroupCpuUsageInNanosecondsWithoutHost() | ||
| { | ||
| // The value we are interested in starts with this. We just want to make sure it is true. | ||
| const string Usage_usec = "usage_usec"; | ||
|
|
||
| using ReturnableBufferWriter<char> bufferWriter = new(_sharedBufferWriterPool); | ||
nivebb8 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| FileInfo cpuUsageFile = new(GetCgroupActualSlicePath(CpuStat)); | ||
|
|
||
| // If the file doesn't exist, we assume that the system is a Host and we read the CPU usage from /proc/stat. | ||
| if (!_fileSystem.Exists(cpuUsageFile)) | ||
| { | ||
| return GetHostCpuUsageInNanoseconds(); | ||
| } | ||
|
|
||
| _fileSystem.ReadAll(cpuUsageFile, bufferWriter.Buffer); | ||
|
|
||
| ReadOnlySpan<char> usage = bufferWriter.Buffer.WrittenSpan; | ||
|
|
||
| if (!usage.StartsWith(Usage_usec)) | ||
| { | ||
| Throw.InvalidOperationException($"Could not parse '{cpuUsageFile}'. We expected first line of the file to start with '{Usage_usec}' but it was '{new string(usage)}' instead."); | ||
| } | ||
|
|
||
| ReadOnlySpan<char> cpuUsage = usage.Slice(Usage_usec.Length, usage.Length - Usage_usec.Length); | ||
|
|
||
| int next = GetNextNumber(cpuUsage, out long microseconds); | ||
|
|
||
| if (microseconds == -1) | ||
| { | ||
| Throw.InvalidOperationException($"Could not get cpu usage from '{cpuUsageFile}'. Expected positive number, but got '{new string(usage)}'."); | ||
| } | ||
|
|
||
| // In cgroup v2, the Units are microseconds for usage_usec. | ||
| // We multiply by 1000 to convert to nanoseconds to keep the common calculation logic. | ||
| return microseconds * Thousand; | ||
nivebb8 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| public long GetHostCpuUsageInNanoseconds() | ||
| { | ||
| const string StartingTokens = "cpu "; | ||
|
|
@@ -184,6 +264,22 @@ | |
| return GetHostCpuCount(); | ||
| } | ||
|
|
||
| /// <remarks> | ||
| /// When CGroup limits are set, we can calculate number of cores based on the file settings. | ||
| /// It should be 99% of the cases when app is hosted in the container environment. | ||
| /// Otherwise, we assume that all host's CPUs are available, which we read from proc/stat file. | ||
| /// </remarks> | ||
| public float GetCgroupLimitedCpusWithoutHost() | ||
| { | ||
| FileInfo cpuLimitsFile = new(GetCgroupActualSlicePath(CpuLimit)); | ||
| if (LinuxUtilizationParserCgroupV2.TryGetCpuUnitsFromCgroupsWithoutHost(_fileSystem, cpuLimitsFile, out float cpus)) | ||
| { | ||
| return cpus; | ||
| } | ||
|
|
||
| return GetHostCpuCount(); | ||
| } | ||
|
|
||
| /// <remarks> | ||
| /// If we are able to read the CPU share, we calculate the CPU request based on the weight by dividing it by 1024. | ||
| /// If we can't read the CPU weight, we assume that the pod/vm cpu request is 1 core by default. | ||
|
|
@@ -198,6 +294,21 @@ | |
| return GetHostCpuCount(); | ||
| } | ||
|
|
||
| /// <remarks> | ||
| /// If we are able to read the CPU share, we calculate the CPU request based on the weight by dividing it by 1024. | ||
| /// If we can't read the CPU weight, we assume that the pod/vm cpu request is 1 core by default. | ||
| /// </remarks> | ||
| public float GetCgroupRequestCpuWithoutHost() | ||
| { | ||
| FileInfo cpuRequestsFile = new(GetCgroupActualSlicePath(CpuRequest)); | ||
| if (TryGetCgroupRequestCpuWithoutHost(_fileSystem, cpuRequestsFile, out float cpuPodRequest)) | ||
| { | ||
| return cpuPodRequest / CpuShares; | ||
| } | ||
|
|
||
| return GetHostCpuCount(); | ||
| } | ||
|
|
||
| /// <remarks> | ||
| /// If the file doesn't exist, we assume that the system is a Host and we read the memory from /proc/meminfo. | ||
| /// </remarks> | ||
|
|
@@ -531,6 +642,56 @@ | |
| return true; | ||
| } | ||
|
|
||
| /// <remarks> | ||
| /// If the file doesn't exist, we assume that the system is a Host and we read the CPU usage from /proc/stat. | ||
| /// </remarks> | ||
| private static bool TryGetCpuUnitsFromCgroupsWithoutHost(IFileSystem fileSystem, FileInfo cpuLimitsFile, out float cpuUnits) | ||
nivebb8 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| { | ||
| if (!fileSystem.Exists(cpuLimitsFile)) | ||
| { | ||
| cpuUnits = 0; | ||
| return false; | ||
| } | ||
|
|
||
| using ReturnableBufferWriter<char> bufferWriter = new(_sharedBufferWriterPool); | ||
| fileSystem.ReadFirstLine(cpuLimitsFile, bufferWriter.Buffer); | ||
|
|
||
| ReadOnlySpan<char> quotaBuffer = bufferWriter.Buffer.WrittenSpan; | ||
|
|
||
| if (quotaBuffer.IsEmpty || (quotaBuffer.Length == 2 && quotaBuffer[0] == '-' && quotaBuffer[1] == '1')) | ||
| { | ||
| cpuUnits = -1; | ||
| return false; | ||
| } | ||
|
|
||
| if (quotaBuffer.StartsWith("max", StringComparison.InvariantCulture)) | ||
| { | ||
| cpuUnits = 0; | ||
| return false; | ||
| } | ||
|
|
||
| _ = GetNextNumber(quotaBuffer, out long quota); | ||
|
|
||
| if (quota == -1) | ||
| { | ||
| Throw.InvalidOperationException($"Could not parse '{cpuLimitsFile}'. Expected an integer but got: '{new string(quotaBuffer)}'."); | ||
| } | ||
|
|
||
| string quotaString = quota.ToString(CultureInfo.CurrentCulture); | ||
| int index = quotaBuffer.IndexOf(quotaString.AsSpan()); | ||
| ReadOnlySpan<char> cpuPeriodSlice = quotaBuffer.Slice(index + quotaString.Length, quotaBuffer.Length - index - quotaString.Length); | ||
| _ = GetNextNumber(cpuPeriodSlice, out long period); | ||
|
|
||
| if (period == -1) | ||
| { | ||
| Throw.InvalidOperationException($"Could not parse '{cpuLimitsFile}'. Expected to get an integer but got: '{new string(cpuPeriodSlice)}'."); | ||
| } | ||
|
|
||
| cpuUnits = (float)quota / period; | ||
|
|
||
| return true; | ||
| } | ||
|
|
||
| private static bool TryGetCgroupRequestCpu(IFileSystem fileSystem, out float cpuUnits) | ||
| { | ||
| const long CpuPodWeightPossibleMax = 10_000; | ||
|
|
@@ -578,6 +739,53 @@ | |
| return true; | ||
| } | ||
|
|
||
| private static bool TryGetCgroupRequestCpuWithoutHost(IFileSystem fileSystem, FileInfo cpuRequestsFile, out float cpuUnits) | ||
nivebb8 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| { | ||
| const long CpuPodWeightPossibleMax = 10_000; | ||
| const long CpuPodWeightPossibleMin = 1; | ||
|
|
||
| if (!fileSystem.Exists(cpuRequestsFile)) | ||
| { | ||
| cpuUnits = 0; | ||
| return false; | ||
| } | ||
|
|
||
| using ReturnableBufferWriter<char> bufferWriter = new(_sharedBufferWriterPool); | ||
| fileSystem.ReadFirstLine(cpuRequestsFile, bufferWriter.Buffer); | ||
| ReadOnlySpan<char> cpuPodWeightBuffer = bufferWriter.Buffer.WrittenSpan; | ||
|
|
||
| if (cpuPodWeightBuffer.IsEmpty || (cpuPodWeightBuffer.Length == 2 && cpuPodWeightBuffer[0] == '-' && cpuPodWeightBuffer[1] == '1')) | ||
| { | ||
| Throw.InvalidOperationException( | ||
| $"Could not parse '{cpuRequestsFile}' content. Expected to find CPU weight but got '{new string(cpuPodWeightBuffer)}' instead."); | ||
| } | ||
|
|
||
| _ = GetNextNumber(cpuPodWeightBuffer, out long cpuPodWeight); | ||
|
|
||
| if (cpuPodWeight == -1) | ||
| { | ||
| Throw.InvalidOperationException( | ||
| $"Could not parse '{cpuRequestsFile}' content. Expected to get an integer but got: '{cpuPodWeightBuffer}'."); | ||
| } | ||
|
|
||
| if (cpuPodWeight < CpuPodWeightPossibleMin || cpuPodWeight > CpuPodWeightPossibleMax) | ||
| { | ||
| Throw.ArgumentOutOfRangeException("CPU weight", | ||
| $"Expected to find CPU weight in range [{CpuPodWeightPossibleMin}-{CpuPodWeightPossibleMax}] in '{cpuRequestsFile}', but got '{cpuPodWeight}' instead."); | ||
| } | ||
|
|
||
| // The formula to calculate CPU pod weight (measured in millicores) from CPU share: | ||
| // y = (1 + ((x - 2) * 9999) / 262142), | ||
| // where y is the CPU pod weight (e.g. cpuPodWeight) and x is the CPU share of cgroup v1 (e.g. cpuUnits). | ||
| // https://github.com/kubernetes/enhancements/tree/master/keps/sig-node/2254-cgroup-v2#phase-1-convert-from-cgroups-v1-settings-to-v2 | ||
| // We invert the formula to calculate CPU share from CPU pod weight: | ||
| #pragma warning disable S109 // Magic numbers should not be used - using the formula, forgive. | ||
| cpuUnits = ((cpuPodWeight - 1) * 262142 / 9999) + 2; | ||
| #pragma warning restore S109 // Magic numbers should not be used | ||
|
|
||
| return true; | ||
| } | ||
|
|
||
| private long GetMemoryUsageInBytesPod() | ||
| { | ||
| using ReturnableBufferWriter<char> bufferWriter = new(_sharedBufferWriterPool); | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.