Skip to content

Commit 0f5909e

Browse files
authored
Add Kubernetes based Resource Monitoring (#6748)
* Draft * Draft * Add ResourceQuotasProvider * Fix public API surface, remove Quotas provider component * Switch layering of the abstractions to support Kubernetes metadata * Fix namespace * Remove ClusterMetadata * Add Linux changes * Add abstractions project * Add changes after API Review * Add changes after API Review pt.2 * Fixes * Add reading memory min and low for cgroupsv2 * Add unit tests * Fix typos * Address PR comments and fix lint issues * Fix flaky test * Fix tests verified files * Fix tests, switch to experimental * Fix errors * Fix more errors * Remove TestUtilities reference
1 parent ddc0697 commit 0f5909e

File tree

40 files changed

+1365
-170
lines changed

40 files changed

+1365
-170
lines changed
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Globalization;
6+
7+
namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes;
8+
9+
internal sealed class KubernetesMetadata
10+
{
11+
/// <summary>
12+
/// Gets or sets the resource memory limit the container is allowed to use in bytes.
13+
/// </summary>
14+
public ulong LimitsMemory { get; set; }
15+
16+
/// <summary>
17+
/// Gets or sets the resource CPU limit the container is allowed to use in millicores.
18+
/// </summary>
19+
public ulong LimitsCpu { get; set; }
20+
21+
/// <summary>
22+
/// Gets or sets the resource memory request the container is allowed to use in bytes.
23+
/// </summary>
24+
public ulong RequestsMemory { get; set; }
25+
26+
/// <summary>
27+
/// Gets or sets the resource CPU request the container is allowed to use in millicores.
28+
/// </summary>
29+
public ulong RequestsCpu { get; set; }
30+
31+
public static KubernetesMetadata FromEnvironmentVariables(string environmentVariablePrefix)
32+
{
33+
return new KubernetesMetadata
34+
{
35+
LimitsMemory = GetEnvironmentVariableAsUInt64($"{environmentVariablePrefix}LIMITS_MEMORY"),
36+
LimitsCpu = GetEnvironmentVariableAsUInt64($"{environmentVariablePrefix}LIMITS_CPU"),
37+
RequestsMemory = GetEnvironmentVariableAsUInt64($"{environmentVariablePrefix}REQUESTS_MEMORY"),
38+
RequestsCpu = GetEnvironmentVariableAsUInt64($"{environmentVariablePrefix}REQUESTS_CPU"),
39+
};
40+
}
41+
42+
private static ulong GetEnvironmentVariableAsUInt64(string variableName)
43+
{
44+
var value = Environment.GetEnvironmentVariable(variableName);
45+
if (string.IsNullOrWhiteSpace(value))
46+
{
47+
return 0;
48+
}
49+
50+
if (!ulong.TryParse(value, NumberStyles.None, CultureInfo.InvariantCulture, out ulong result))
51+
{
52+
throw new InvalidOperationException($"Environment variable '{variableName}' contains invalid value '{value}'. Expected a non-negative integer.");
53+
}
54+
55+
return result;
56+
}
57+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.Shared.Diagnostics;
5+
6+
namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes;
7+
8+
internal sealed class KubernetesResourceQuotaProvider : ResourceQuotaProvider
9+
{
10+
private const double MillicoresPerCore = 1000.0;
11+
private KubernetesMetadata _kubernetesMetadata;
12+
13+
public KubernetesResourceQuotaProvider(KubernetesMetadata kubernetesMetadata)
14+
{
15+
_ = Throw.IfNull(kubernetesMetadata);
16+
_kubernetesMetadata = kubernetesMetadata;
17+
}
18+
19+
public override ResourceQuota GetResourceQuota()
20+
{
21+
ResourceQuota quota = new()
22+
{
23+
BaselineCpuInCores = ConvertMillicoreToCpuUnit(_kubernetesMetadata.RequestsCpu),
24+
MaxCpuInCores = ConvertMillicoreToCpuUnit(_kubernetesMetadata.LimitsCpu),
25+
BaselineMemoryInBytes = _kubernetesMetadata.RequestsMemory,
26+
MaxMemoryInBytes = _kubernetesMetadata.LimitsMemory,
27+
};
28+
29+
if (quota.BaselineCpuInCores <= 0.0)
30+
{
31+
quota.BaselineCpuInCores = quota.MaxCpuInCores;
32+
}
33+
34+
if (quota.BaselineMemoryInBytes == 0)
35+
{
36+
quota.BaselineMemoryInBytes = quota.MaxMemoryInBytes;
37+
}
38+
39+
return quota;
40+
}
41+
42+
private static double ConvertMillicoreToCpuUnit(ulong millicores)
43+
{
44+
return millicores / MillicoresPerCore;
45+
}
46+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.Extensions.DependencyInjection.Extensions;
5+
using Microsoft.Extensions.Diagnostics.ResourceMonitoring;
6+
using Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes;
7+
8+
namespace Microsoft.Extensions.DependencyInjection;
9+
10+
/// <summary>
11+
/// Lets you configure and register Kubernetes resource monitoring components.
12+
/// </summary>
13+
public static class KubernetesResourceQuotaServiceCollectionExtensions
14+
{
15+
/// <summary>
16+
/// Configures and adds an Kubernetes resource monitoring components to a service collection altogether with necessary basic resource monitoring components.
17+
/// </summary>
18+
/// <param name="services">The dependency injection container to add the Kubernetes resource monitoring to.</param>
19+
/// <param name="environmentVariablePrefix">Optional value of prefix used to read environment variables in the container.</param>
20+
/// <returns>The value of <paramref name="services" />.</returns>
21+
/// <remarks>
22+
/// <para>
23+
/// If you have configured your Kubernetes container with Downward API to add environment variable <c>MYCLUSTER_LIMITS_CPU</c> with CPU limits,
24+
/// then you should pass <c>MYCLUSTER_</c> to <paramref name="environmentVariablePrefix"/> parameter. Environment variables will be read during DI Container resolution.
25+
/// </para>
26+
/// <para>
27+
/// <strong>Important:</strong> Do not call <see cref="ResourceMonitoringServiceCollectionExtensions.AddResourceMonitoring(IServiceCollection)"/>
28+
/// if you are using this method, as it already includes all necessary resource monitoring components and registers a Kubernetes-specific
29+
/// <see cref="ResourceQuotaProvider"/> implementation. Calling both methods may result in conflicting service registrations.
30+
/// </para>
31+
/// </remarks>
32+
public static IServiceCollection AddKubernetesResourceMonitoring(
33+
this IServiceCollection services,
34+
string? environmentVariablePrefix = default)
35+
{
36+
services.TryAddSingleton<ResourceQuotaProvider>(sp =>
37+
{
38+
return new KubernetesResourceQuotaProvider(KubernetesMetadata.FromEnvironmentVariables(environmentVariablePrefix ?? string.Empty));
39+
});
40+
41+
_ = services.AddResourceMonitoring();
42+
43+
return services;
44+
}
45+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<RootNamespace>Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes</RootNamespace>
4+
<Description>Provides Kubernetes data for measurements of processor and memory usage.</Description>
5+
<Workstream>ResourceMonitoring</Workstream>
6+
<NoWarn Condition="'$(TargetFramework)' == 'net462'">$(NoWarn);CS0436</NoWarn>
7+
</PropertyGroup>
8+
9+
<PropertyGroup>
10+
<InjectExperimentalAttributeOnLegacy>true</InjectExperimentalAttributeOnLegacy>
11+
<InjectSharedInstruments>true</InjectSharedInstruments>
12+
</PropertyGroup>
13+
14+
<PropertyGroup>
15+
<Stage>dev</Stage>
16+
<StageDevDiagnosticId>EXTEXP0016</StageDevDiagnosticId>
17+
<MinCodeCoverage>99</MinCodeCoverage>
18+
<MinMutationScore>90</MinMutationScore>
19+
</PropertyGroup>
20+
21+
<ItemGroup>
22+
<ProjectReference Include="..\Microsoft.Extensions.Diagnostics.ResourceMonitoring\Microsoft.Extensions.Diagnostics.ResourceMonitoring.csproj" />
23+
</ItemGroup>
24+
25+
<ItemGroup>
26+
<InternalsVisibleToDynamicProxyGenAssembly2 Include="*" />
27+
<InternalsVisibleToTest Include="$(AssemblyName).Tests" />
28+
</ItemGroup>
29+
</Project>

src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes/Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes.json

Whitespace-only changes.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes
2+
3+
Registers `ResourceQuota` implementation specific to Kubernetes.
4+
5+
## Install the package
6+
7+
From the command-line:
8+
9+
```console
10+
dotnet add package Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes
11+
```
12+
13+
Or directly in the C# project file:
14+
15+
```xml
16+
<ItemGroup>
17+
<PackageReference Include="Microsoft.Extensions.Diagnostics.ResourceMonitoring.Kubernetes" Version="[CURRENTVERSION]" />
18+
</ItemGroup>
19+
```
20+
21+
22+
## Feedback & Contributing
23+
24+
We welcome feedback and contributions in [our GitHub repo](https://github.com/dotnet/extensions).

src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/ILinuxUtilizationParser.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,4 +95,10 @@ internal interface ILinuxUtilizationParser
9595
/// </summary>
9696
/// <returns>The number of CPU periods.</returns>
9797
long GetCgroupPeriodsIntervalInMicroSecondsV2();
98+
99+
/// <summary>
100+
/// For CgroupV2 only. Reads the file /sys/fs/cgroup/memory.min, if 0 reads the file /sys/fs/cgroup/memory.low.
101+
/// </summary>
102+
/// <returns>The minimum memory allocated in bytes, or 0 if not available.</returns>
103+
ulong GetMinMemoryInBytes();
98104
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.Extensions.Options;
5+
6+
namespace Microsoft.Extensions.Diagnostics.ResourceMonitoring.Linux;
7+
8+
internal sealed class LinuxResourceQuotaProvider : ResourceQuotaProvider
9+
{
10+
private readonly ILinuxUtilizationParser _parser;
11+
private bool _useLinuxCalculationV2;
12+
13+
public LinuxResourceQuotaProvider(ILinuxUtilizationParser parser, IOptions<ResourceMonitoringOptions> options)
14+
{
15+
_parser = parser;
16+
_useLinuxCalculationV2 = options.Value.UseLinuxCalculationV2;
17+
}
18+
19+
public override ResourceQuota GetResourceQuota()
20+
{
21+
var resourceQuota = new ResourceQuota();
22+
if (_useLinuxCalculationV2)
23+
{
24+
resourceQuota.MaxCpuInCores = _parser.GetCgroupLimitV2();
25+
resourceQuota.BaselineCpuInCores = _parser.GetCgroupRequestCpuV2();
26+
}
27+
else
28+
{
29+
resourceQuota.MaxCpuInCores = _parser.GetCgroupLimitedCpus();
30+
resourceQuota.BaselineCpuInCores = _parser.GetCgroupRequestCpu();
31+
}
32+
33+
resourceQuota.MaxMemoryInBytes = _parser.GetAvailableMemoryInBytes();
34+
resourceQuota.BaselineMemoryInBytes = _parser.GetMinMemoryInBytes();
35+
36+
if (resourceQuota.BaselineMemoryInBytes == 0)
37+
{
38+
resourceQuota.BaselineMemoryInBytes = resourceQuota.MaxMemoryInBytes;
39+
}
40+
41+
return resourceQuota;
42+
}
43+
}
44+

src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV1.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,12 @@ static void ThrowException(ReadOnlySpan<char> content) =>
368368
$"Could not parse '{_cpuSetCpus}'. Expected comma-separated list of integers, with dashes (\"-\") based ranges (\"0\", \"2-6,12\") but got '{new string(content)}'.");
369369
}
370370

371+
/// <summary>
372+
/// In CgroupV1 there's no equivalent to memory.min or memory.low files.
373+
/// </summary>
374+
/// <returns>0.</returns>
375+
public ulong GetMinMemoryInBytes() => 0;
376+
371377
/// <remarks>
372378
/// The input must contain only number. If there is something more than whitespace before the number, it will return failure (-1).
373379
/// </remarks>

src/Libraries/Microsoft.Extensions.Diagnostics.ResourceMonitoring/Linux/LinuxUtilizationParserCgroupV2.cs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ internal sealed class LinuxUtilizationParserCgroupV2 : ILinuxUtilizationParser
2323
private const string CpuStat = "cpu.stat"; // File containing CPU usage in nanoseconds.
2424
private const string CpuLimit = "cpu.max"; // File with amount of CPU time available to the group along with the accounting period in microseconds.
2525
private const string CpuRequest = "cpu.weight"; // CPU weights, also known as shares in cgroup v1, is used for resource allocation.
26+
private const string MemoryMin = "memory.min"; // File contains min memory, set for QoS by K8s
27+
private const string MemoryLow = "memory.low"; // File contains min memory, set if there was memory reservation for container
2628
private static readonly ObjectPool<BufferWriter<char>> _sharedBufferWriterPool = BufferWriterPool.CreateBufferWriterPool<char>();
2729

2830
/// <remarks>
@@ -513,6 +515,50 @@ static void ThrowException(ReadOnlySpan<char> content) =>
513515
$"Could not parse '{_cpuSetCpus}'. Expected comma-separated list of integers, with dashes (\"-\") based ranges (\"0\", \"2-6,12\") but got '{new string(content)}'.");
514516
}
515517

518+
public ulong GetMinMemoryInBytes()
519+
{
520+
FileInfo memoryMinFile = new(GetCgroupPath(MemoryMin));
521+
if (_fileSystem.Exists(memoryMinFile))
522+
{
523+
using ReturnableBufferWriter<char> bufferWriter = new(_sharedBufferWriterPool);
524+
_fileSystem.ReadAll(memoryMinFile, bufferWriter.Buffer);
525+
526+
ReadOnlySpan<char> memoryMinBuffer = bufferWriter.Buffer.WrittenSpan;
527+
528+
_ = GetNextNumber(memoryMinBuffer, out long memoryMin);
529+
530+
if (memoryMin == -1)
531+
{
532+
Throw.InvalidOperationException($"Could not parse '{memoryMinFile}' content. Expected to find memory minimum in bytes but got '{new string(memoryMinBuffer)}' instead.");
533+
}
534+
535+
if (memoryMin != 0)
536+
{
537+
return (ulong)memoryMin;
538+
}
539+
}
540+
541+
FileInfo memoryLowFile = new(GetCgroupPath(MemoryLow));
542+
if (_fileSystem.Exists(memoryLowFile))
543+
{
544+
using ReturnableBufferWriter<char> bufferWriter = new(_sharedBufferWriterPool);
545+
_fileSystem.ReadAll(memoryLowFile, bufferWriter.Buffer);
546+
547+
ReadOnlySpan<char> memoryLowBuffer = bufferWriter.Buffer.WrittenSpan;
548+
549+
_ = GetNextNumber(memoryLowBuffer, out long memoryLow);
550+
551+
if (memoryLow == -1)
552+
{
553+
Throw.InvalidOperationException($"Could not parse '{memoryLowFile}' content. Expected to find memory low in bytes but got '{new string(memoryLowBuffer)}' instead.");
554+
}
555+
556+
return (ulong)memoryLow;
557+
}
558+
559+
return 0;
560+
}
561+
516562
private static (long cpuUsageNanoseconds, long nrPeriods) ParseCpuUsageFromFile(IFileSystem fileSystem, FileInfo cpuUsageFile)
517563
{
518564
// The values we are interested in start with these prefixes

0 commit comments

Comments
 (0)