Merge pull request #8552 from dcantah/cross-plat-stats
CRI: Make stats respect sandbox's platform
This commit is contained in:
commit
6d7060099b
@ -40,7 +40,12 @@ func (c *criService) ContainerStats(ctx context.Context, in *runtime.ContainerSt
|
|||||||
return nil, fmt.Errorf("unexpected metrics response: %+v", resp.Metrics)
|
return nil, fmt.Errorf("unexpected metrics response: %+v", resp.Metrics)
|
||||||
}
|
}
|
||||||
|
|
||||||
cs, err := c.containerMetrics(cntr.Metadata, resp.Metrics[0])
|
handler, err := c.getMetricsHandler(ctx, cntr.SandboxID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cs, err := handler(cntr.Metadata, resp.Metrics[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to decode container metrics: %w", err)
|
return nil, fmt.Errorf("failed to decode container metrics: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -18,12 +18,20 @@ package sbserver
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"reflect"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
wstats "github.com/Microsoft/hcsshim/cmd/containerd-shim-runhcs-v1/stats"
|
||||||
|
cg1 "github.com/containerd/cgroups/v3/cgroup1/stats"
|
||||||
|
cg2 "github.com/containerd/cgroups/v3/cgroup2/stats"
|
||||||
"github.com/containerd/containerd/api/services/tasks/v1"
|
"github.com/containerd/containerd/api/services/tasks/v1"
|
||||||
"github.com/containerd/containerd/api/types"
|
"github.com/containerd/containerd/api/types"
|
||||||
|
"github.com/containerd/containerd/errdefs"
|
||||||
"github.com/containerd/containerd/pkg/cri/store/stats"
|
"github.com/containerd/containerd/pkg/cri/store/stats"
|
||||||
|
"github.com/containerd/containerd/protobuf"
|
||||||
|
"github.com/containerd/typeurl/v2"
|
||||||
runtime "k8s.io/cri-api/pkg/apis/runtime/v1"
|
runtime "k8s.io/cri-api/pkg/apis/runtime/v1"
|
||||||
|
|
||||||
containerstore "github.com/containerd/containerd/pkg/cri/store/container"
|
containerstore "github.com/containerd/containerd/pkg/cri/store/container"
|
||||||
@ -42,14 +50,48 @@ func (c *criService) ListContainerStats(
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to fetch metrics for tasks: %w", err)
|
return nil, fmt.Errorf("failed to fetch metrics for tasks: %w", err)
|
||||||
}
|
}
|
||||||
criStats, err := c.toCRIContainerStats(resp.Metrics, containers)
|
criStats, err := c.toCRIContainerStats(ctx, resp.Metrics, containers)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to convert to cri containerd stats format: %w", err)
|
return nil, fmt.Errorf("failed to convert to cri containerd stats format: %w", err)
|
||||||
}
|
}
|
||||||
return criStats, nil
|
return criStats, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type metricsHandler func(containerstore.Metadata, *types.Metric) (*runtime.ContainerStats, error)
|
||||||
|
|
||||||
|
// Returns a function to be used for transforming container metrics into the right format.
|
||||||
|
// Uses the platform the given sandbox advertises to implement its logic. If the platform is
|
||||||
|
// unsupported for metrics this will return a wrapped [errdefs.ErrNotImplemented].
|
||||||
|
func (c *criService) getMetricsHandler(ctx context.Context, sandboxID string) (metricsHandler, error) {
|
||||||
|
sandbox, err := c.sandboxStore.Get(sandboxID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to find sandbox id %q: %w", sandboxID, err)
|
||||||
|
}
|
||||||
|
controller, err := c.getSandboxController(sandbox.Config, sandbox.RuntimeHandler)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get sandbox controller: %w", err)
|
||||||
|
}
|
||||||
|
// Grab the platform that this containers sandbox advertises. Reason being, even if
|
||||||
|
// the host may be {insert platform}, if it virtualizes or emulates a different platform
|
||||||
|
// it will return stats in that format, and we need to handle the conversion logic based
|
||||||
|
// off of this info.
|
||||||
|
p, err := controller.Platform(ctx, sandboxID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch p.OS {
|
||||||
|
case "windows":
|
||||||
|
return c.windowsContainerMetrics, nil
|
||||||
|
case "linux":
|
||||||
|
return c.linuxContainerMetrics, nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("container metrics for platform %+v: %w", p, errdefs.ErrNotImplemented)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (c *criService) toCRIContainerStats(
|
func (c *criService) toCRIContainerStats(
|
||||||
|
ctx context.Context,
|
||||||
stats []*types.Metric,
|
stats []*types.Metric,
|
||||||
containers []containerstore.Container,
|
containers []containerstore.Container,
|
||||||
) (*runtime.ListContainerStatsResponse, error) {
|
) (*runtime.ListContainerStatsResponse, error) {
|
||||||
@ -58,8 +100,29 @@ func (c *criService) toCRIContainerStats(
|
|||||||
statsMap[stat.ID] = stat
|
statsMap[stat.ID] = stat
|
||||||
}
|
}
|
||||||
containerStats := new(runtime.ListContainerStatsResponse)
|
containerStats := new(runtime.ListContainerStatsResponse)
|
||||||
|
|
||||||
|
// Unfortunately if no filter was passed we're asking for every containers stats which
|
||||||
|
// generally belong to multiple different pods, who all might have different platforms.
|
||||||
|
// To avoid recalculating the right metricsHandler to invoke, if we've already calculated
|
||||||
|
// the platform and handler for a given sandbox just pull it from our map here.
|
||||||
|
var (
|
||||||
|
err error
|
||||||
|
handler metricsHandler
|
||||||
|
)
|
||||||
|
sandboxToMetricsHandler := make(map[string]metricsHandler)
|
||||||
for _, cntr := range containers {
|
for _, cntr := range containers {
|
||||||
cs, err := c.containerMetrics(cntr.Metadata, statsMap[cntr.ID])
|
h, ok := sandboxToMetricsHandler[cntr.SandboxID]
|
||||||
|
if !ok {
|
||||||
|
handler, err = c.getMetricsHandler(ctx, cntr.SandboxID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get metrics handler for container %q: %w", cntr.ID, err)
|
||||||
|
}
|
||||||
|
sandboxToMetricsHandler[cntr.SandboxID] = handler
|
||||||
|
} else {
|
||||||
|
handler = h
|
||||||
|
}
|
||||||
|
|
||||||
|
cs, err := handler(cntr.Metadata, statsMap[cntr.ID])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to decode container metrics for %q: %w", cntr.ID, err)
|
return nil, fmt.Errorf("failed to decode container metrics for %q: %w", cntr.ID, err)
|
||||||
}
|
}
|
||||||
@ -72,7 +135,6 @@ func (c *criService) toCRIContainerStats(
|
|||||||
}
|
}
|
||||||
cs.Cpu.UsageNanoCores = &runtime.UInt64Value{Value: nanoUsage}
|
cs.Cpu.UsageNanoCores = &runtime.UInt64Value{Value: nanoUsage}
|
||||||
}
|
}
|
||||||
|
|
||||||
containerStats.Stats = append(containerStats.Stats, cs)
|
containerStats.Stats = append(containerStats.Stats, cs)
|
||||||
}
|
}
|
||||||
return containerStats, nil
|
return containerStats, nil
|
||||||
@ -133,7 +195,6 @@ func (c *criService) getUsageNanoCores(containerID string, isSandbox bool, curre
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, fmt.Errorf("failed to update sandbox container stats: %s: %w", containerID, err)
|
return 0, fmt.Errorf("failed to update sandbox container stats: %s: %w", containerID, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
err := c.containerStore.UpdateContainerStats(containerID, newStats)
|
err := c.containerStore.UpdateContainerStats(containerID, newStats)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -193,3 +254,238 @@ func matchLabelSelector(selector, labels map[string]string) bool {
|
|||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *criService) windowsContainerMetrics(
|
||||||
|
meta containerstore.Metadata,
|
||||||
|
stats *types.Metric,
|
||||||
|
) (*runtime.ContainerStats, error) {
|
||||||
|
var cs runtime.ContainerStats
|
||||||
|
var usedBytes, inodesUsed uint64
|
||||||
|
sn, err := c.GetSnapshot(meta.ID)
|
||||||
|
// If snapshotstore doesn't have cached snapshot information
|
||||||
|
// set WritableLayer usage to zero
|
||||||
|
if err == nil {
|
||||||
|
usedBytes = sn.Size
|
||||||
|
inodesUsed = sn.Inodes
|
||||||
|
}
|
||||||
|
cs.WritableLayer = &runtime.FilesystemUsage{
|
||||||
|
Timestamp: sn.Timestamp,
|
||||||
|
FsId: &runtime.FilesystemIdentifier{
|
||||||
|
Mountpoint: c.imageFSPath,
|
||||||
|
},
|
||||||
|
UsedBytes: &runtime.UInt64Value{Value: usedBytes},
|
||||||
|
InodesUsed: &runtime.UInt64Value{Value: inodesUsed},
|
||||||
|
}
|
||||||
|
cs.Attributes = &runtime.ContainerAttributes{
|
||||||
|
Id: meta.ID,
|
||||||
|
Metadata: meta.Config.GetMetadata(),
|
||||||
|
Labels: meta.Config.GetLabels(),
|
||||||
|
Annotations: meta.Config.GetAnnotations(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if stats != nil {
|
||||||
|
s, err := typeurl.UnmarshalAny(stats.Data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to extract container metrics: %w", err)
|
||||||
|
}
|
||||||
|
wstats := s.(*wstats.Statistics).GetWindows()
|
||||||
|
if wstats == nil {
|
||||||
|
return nil, errors.New("windows stats is empty")
|
||||||
|
}
|
||||||
|
if wstats.Processor != nil {
|
||||||
|
cs.Cpu = &runtime.CpuUsage{
|
||||||
|
Timestamp: wstats.Timestamp.UnixNano(),
|
||||||
|
UsageCoreNanoSeconds: &runtime.UInt64Value{Value: wstats.Processor.TotalRuntimeNS},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if wstats.Memory != nil {
|
||||||
|
cs.Memory = &runtime.MemoryUsage{
|
||||||
|
Timestamp: wstats.Timestamp.UnixNano(),
|
||||||
|
WorkingSetBytes: &runtime.UInt64Value{
|
||||||
|
Value: wstats.Memory.MemoryUsagePrivateWorkingSetBytes,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &cs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *criService) linuxContainerMetrics(
|
||||||
|
meta containerstore.Metadata,
|
||||||
|
stats *types.Metric,
|
||||||
|
) (*runtime.ContainerStats, error) {
|
||||||
|
var cs runtime.ContainerStats
|
||||||
|
var usedBytes, inodesUsed uint64
|
||||||
|
sn, err := c.GetSnapshot(meta.ID)
|
||||||
|
// If snapshotstore doesn't have cached snapshot information
|
||||||
|
// set WritableLayer usage to zero
|
||||||
|
if err == nil {
|
||||||
|
usedBytes = sn.Size
|
||||||
|
inodesUsed = sn.Inodes
|
||||||
|
}
|
||||||
|
cs.WritableLayer = &runtime.FilesystemUsage{
|
||||||
|
Timestamp: sn.Timestamp,
|
||||||
|
FsId: &runtime.FilesystemIdentifier{
|
||||||
|
Mountpoint: c.imageFSPath,
|
||||||
|
},
|
||||||
|
UsedBytes: &runtime.UInt64Value{Value: usedBytes},
|
||||||
|
InodesUsed: &runtime.UInt64Value{Value: inodesUsed},
|
||||||
|
}
|
||||||
|
cs.Attributes = &runtime.ContainerAttributes{
|
||||||
|
Id: meta.ID,
|
||||||
|
Metadata: meta.Config.GetMetadata(),
|
||||||
|
Labels: meta.Config.GetLabels(),
|
||||||
|
Annotations: meta.Config.GetAnnotations(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if stats != nil {
|
||||||
|
var data interface{}
|
||||||
|
switch {
|
||||||
|
case typeurl.Is(stats.Data, (*cg1.Metrics)(nil)):
|
||||||
|
data = &cg1.Metrics{}
|
||||||
|
case typeurl.Is(stats.Data, (*cg2.Metrics)(nil)):
|
||||||
|
data = &cg2.Metrics{}
|
||||||
|
case typeurl.Is(stats.Data, (*wstats.Statistics)(nil)):
|
||||||
|
data = &wstats.Statistics{}
|
||||||
|
default:
|
||||||
|
return nil, errors.New("cannot convert metric data to cgroups.Metrics or windows.Statistics")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := typeurl.UnmarshalTo(stats.Data, data); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to extract container metrics: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cpuStats, err := c.cpuContainerStats(meta.ID, false /* isSandbox */, data, protobuf.FromTimestamp(stats.Timestamp))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to obtain cpu stats: %w", err)
|
||||||
|
}
|
||||||
|
cs.Cpu = cpuStats
|
||||||
|
|
||||||
|
memoryStats, err := c.memoryContainerStats(meta.ID, data, protobuf.FromTimestamp(stats.Timestamp))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to obtain memory stats: %w", err)
|
||||||
|
}
|
||||||
|
cs.Memory = memoryStats
|
||||||
|
}
|
||||||
|
|
||||||
|
return &cs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getWorkingSet calculates workingset memory from cgroup memory stats.
|
||||||
|
// The caller should make sure memory is not nil.
|
||||||
|
// workingset = usage - total_inactive_file
|
||||||
|
func getWorkingSet(memory *cg1.MemoryStat) uint64 {
|
||||||
|
if memory.Usage == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
var workingSet uint64
|
||||||
|
if memory.TotalInactiveFile < memory.Usage.Usage {
|
||||||
|
workingSet = memory.Usage.Usage - memory.TotalInactiveFile
|
||||||
|
}
|
||||||
|
return workingSet
|
||||||
|
}
|
||||||
|
|
||||||
|
// getWorkingSetV2 calculates workingset memory from cgroupv2 memory stats.
|
||||||
|
// The caller should make sure memory is not nil.
|
||||||
|
// workingset = usage - inactive_file
|
||||||
|
func getWorkingSetV2(memory *cg2.MemoryStat) uint64 {
|
||||||
|
var workingSet uint64
|
||||||
|
if memory.InactiveFile < memory.Usage {
|
||||||
|
workingSet = memory.Usage - memory.InactiveFile
|
||||||
|
}
|
||||||
|
return workingSet
|
||||||
|
}
|
||||||
|
|
||||||
|
func isMemoryUnlimited(v uint64) bool {
|
||||||
|
// Size after which we consider memory to be "unlimited". This is not
|
||||||
|
// MaxInt64 due to rounding by the kernel.
|
||||||
|
// TODO: k8s or cadvisor should export this https://github.com/google/cadvisor/blob/2b6fbacac7598e0140b5bc8428e3bdd7d86cf5b9/metrics/prometheus.go#L1969-L1971
|
||||||
|
const maxMemorySize = uint64(1 << 62)
|
||||||
|
|
||||||
|
return v > maxMemorySize
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://github.com/kubernetes/kubernetes/blob/b47f8263e18c7b13dba33fba23187e5e0477cdbd/pkg/kubelet/stats/helper.go#L68-L71
|
||||||
|
func getAvailableBytes(memory *cg1.MemoryStat, workingSetBytes uint64) uint64 {
|
||||||
|
// memory limit - working set bytes
|
||||||
|
if !isMemoryUnlimited(memory.Usage.Limit) {
|
||||||
|
return memory.Usage.Limit - workingSetBytes
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAvailableBytesV2(memory *cg2.MemoryStat, workingSetBytes uint64) uint64 {
|
||||||
|
// memory limit (memory.max) for cgroupv2 - working set bytes
|
||||||
|
if !isMemoryUnlimited(memory.UsageLimit) {
|
||||||
|
return memory.UsageLimit - workingSetBytes
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *criService) cpuContainerStats(ID string, isSandbox bool, stats interface{}, timestamp time.Time) (*runtime.CpuUsage, error) {
|
||||||
|
switch metrics := stats.(type) {
|
||||||
|
case *cg1.Metrics:
|
||||||
|
metrics.GetCPU().GetUsage()
|
||||||
|
if metrics.CPU != nil && metrics.CPU.Usage != nil {
|
||||||
|
return &runtime.CpuUsage{
|
||||||
|
Timestamp: timestamp.UnixNano(),
|
||||||
|
UsageCoreNanoSeconds: &runtime.UInt64Value{Value: metrics.CPU.Usage.Total},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
case *cg2.Metrics:
|
||||||
|
if metrics.CPU != nil {
|
||||||
|
// convert to nano seconds
|
||||||
|
usageCoreNanoSeconds := metrics.CPU.UsageUsec * 1000
|
||||||
|
|
||||||
|
return &runtime.CpuUsage{
|
||||||
|
Timestamp: timestamp.UnixNano(),
|
||||||
|
UsageCoreNanoSeconds: &runtime.UInt64Value{Value: usageCoreNanoSeconds},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unexpected metrics type: %T from %s", metrics, reflect.TypeOf(metrics).Elem().PkgPath())
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *criService) memoryContainerStats(ID string, stats interface{}, timestamp time.Time) (*runtime.MemoryUsage, error) {
|
||||||
|
switch metrics := stats.(type) {
|
||||||
|
case *cg1.Metrics:
|
||||||
|
if metrics.Memory != nil && metrics.Memory.Usage != nil {
|
||||||
|
workingSetBytes := getWorkingSet(metrics.Memory)
|
||||||
|
|
||||||
|
return &runtime.MemoryUsage{
|
||||||
|
Timestamp: timestamp.UnixNano(),
|
||||||
|
WorkingSetBytes: &runtime.UInt64Value{
|
||||||
|
Value: workingSetBytes,
|
||||||
|
},
|
||||||
|
AvailableBytes: &runtime.UInt64Value{Value: getAvailableBytes(metrics.Memory, workingSetBytes)},
|
||||||
|
UsageBytes: &runtime.UInt64Value{Value: metrics.Memory.Usage.Usage},
|
||||||
|
RssBytes: &runtime.UInt64Value{Value: metrics.Memory.TotalRSS},
|
||||||
|
PageFaults: &runtime.UInt64Value{Value: metrics.Memory.TotalPgFault},
|
||||||
|
MajorPageFaults: &runtime.UInt64Value{Value: metrics.Memory.TotalPgMajFault},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
case *cg2.Metrics:
|
||||||
|
if metrics.Memory != nil {
|
||||||
|
workingSetBytes := getWorkingSetV2(metrics.Memory)
|
||||||
|
|
||||||
|
return &runtime.MemoryUsage{
|
||||||
|
Timestamp: timestamp.UnixNano(),
|
||||||
|
WorkingSetBytes: &runtime.UInt64Value{
|
||||||
|
Value: workingSetBytes,
|
||||||
|
},
|
||||||
|
AvailableBytes: &runtime.UInt64Value{Value: getAvailableBytesV2(metrics.Memory, workingSetBytes)},
|
||||||
|
UsageBytes: &runtime.UInt64Value{Value: metrics.Memory.Usage},
|
||||||
|
// Use Anon memory for RSS as cAdvisor on cgroupv2
|
||||||
|
// see https://github.com/google/cadvisor/blob/a9858972e75642c2b1914c8d5428e33e6392c08a/container/libcontainer/handler.go#L799
|
||||||
|
RssBytes: &runtime.UInt64Value{Value: metrics.Memory.Anon},
|
||||||
|
PageFaults: &runtime.UInt64Value{Value: metrics.Memory.Pgfault},
|
||||||
|
MajorPageFaults: &runtime.UInt64Value{Value: metrics.Memory.Pgmajfault},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unexpected metrics type: %T from %s", metrics, reflect.TypeOf(metrics).Elem().PkgPath())
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
@ -1,213 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright The containerd Authors.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package sbserver
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"reflect"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
wstats "github.com/Microsoft/hcsshim/cmd/containerd-shim-runhcs-v1/stats"
|
|
||||||
v1 "github.com/containerd/cgroups/v3/cgroup1/stats"
|
|
||||||
v2 "github.com/containerd/cgroups/v3/cgroup2/stats"
|
|
||||||
"github.com/containerd/containerd/api/types"
|
|
||||||
"github.com/containerd/containerd/protobuf"
|
|
||||||
"github.com/containerd/typeurl/v2"
|
|
||||||
runtime "k8s.io/cri-api/pkg/apis/runtime/v1"
|
|
||||||
|
|
||||||
containerstore "github.com/containerd/containerd/pkg/cri/store/container"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (c *criService) containerMetrics(
|
|
||||||
meta containerstore.Metadata,
|
|
||||||
stats *types.Metric,
|
|
||||||
) (*runtime.ContainerStats, error) {
|
|
||||||
var cs runtime.ContainerStats
|
|
||||||
var usedBytes, inodesUsed uint64
|
|
||||||
sn, err := c.GetSnapshot(meta.ID)
|
|
||||||
// If snapshotstore doesn't have cached snapshot information
|
|
||||||
// set WritableLayer usage to zero
|
|
||||||
if err == nil {
|
|
||||||
usedBytes = sn.Size
|
|
||||||
inodesUsed = sn.Inodes
|
|
||||||
}
|
|
||||||
cs.WritableLayer = &runtime.FilesystemUsage{
|
|
||||||
Timestamp: sn.Timestamp,
|
|
||||||
FsId: &runtime.FilesystemIdentifier{
|
|
||||||
Mountpoint: c.imageFSPath,
|
|
||||||
},
|
|
||||||
UsedBytes: &runtime.UInt64Value{Value: usedBytes},
|
|
||||||
InodesUsed: &runtime.UInt64Value{Value: inodesUsed},
|
|
||||||
}
|
|
||||||
cs.Attributes = &runtime.ContainerAttributes{
|
|
||||||
Id: meta.ID,
|
|
||||||
Metadata: meta.Config.GetMetadata(),
|
|
||||||
Labels: meta.Config.GetLabels(),
|
|
||||||
Annotations: meta.Config.GetAnnotations(),
|
|
||||||
}
|
|
||||||
|
|
||||||
if stats != nil {
|
|
||||||
var data interface{}
|
|
||||||
switch {
|
|
||||||
case typeurl.Is(stats.Data, (*v1.Metrics)(nil)):
|
|
||||||
data = &v1.Metrics{}
|
|
||||||
case typeurl.Is(stats.Data, (*v2.Metrics)(nil)):
|
|
||||||
data = &v2.Metrics{}
|
|
||||||
case typeurl.Is(stats.Data, (*wstats.Statistics)(nil)):
|
|
||||||
data = &wstats.Statistics{}
|
|
||||||
default:
|
|
||||||
return nil, errors.New("cannot convert metric data to cgroups.Metrics or windows.Statistics")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := typeurl.UnmarshalTo(stats.Data, data); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to extract container metrics: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cpuStats, err := c.cpuContainerStats(meta.ID, false /* isSandbox */, data, protobuf.FromTimestamp(stats.Timestamp))
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to obtain cpu stats: %w", err)
|
|
||||||
}
|
|
||||||
cs.Cpu = cpuStats
|
|
||||||
|
|
||||||
memoryStats, err := c.memoryContainerStats(meta.ID, data, protobuf.FromTimestamp(stats.Timestamp))
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to obtain memory stats: %w", err)
|
|
||||||
}
|
|
||||||
cs.Memory = memoryStats
|
|
||||||
}
|
|
||||||
|
|
||||||
return &cs, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// getWorkingSet calculates workingset memory from cgroup memory stats.
|
|
||||||
// The caller should make sure memory is not nil.
|
|
||||||
// workingset = usage - total_inactive_file
|
|
||||||
func getWorkingSet(memory *v1.MemoryStat) uint64 {
|
|
||||||
if memory.Usage == nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
var workingSet uint64
|
|
||||||
if memory.TotalInactiveFile < memory.Usage.Usage {
|
|
||||||
workingSet = memory.Usage.Usage - memory.TotalInactiveFile
|
|
||||||
}
|
|
||||||
return workingSet
|
|
||||||
}
|
|
||||||
|
|
||||||
// getWorkingSetV2 calculates workingset memory from cgroupv2 memory stats.
|
|
||||||
// The caller should make sure memory is not nil.
|
|
||||||
// workingset = usage - inactive_file
|
|
||||||
func getWorkingSetV2(memory *v2.MemoryStat) uint64 {
|
|
||||||
var workingSet uint64
|
|
||||||
if memory.InactiveFile < memory.Usage {
|
|
||||||
workingSet = memory.Usage - memory.InactiveFile
|
|
||||||
}
|
|
||||||
return workingSet
|
|
||||||
}
|
|
||||||
|
|
||||||
func isMemoryUnlimited(v uint64) bool {
|
|
||||||
// Size after which we consider memory to be "unlimited". This is not
|
|
||||||
// MaxInt64 due to rounding by the kernel.
|
|
||||||
// TODO: k8s or cadvisor should export this https://github.com/google/cadvisor/blob/2b6fbacac7598e0140b5bc8428e3bdd7d86cf5b9/metrics/prometheus.go#L1969-L1971
|
|
||||||
const maxMemorySize = uint64(1 << 62)
|
|
||||||
|
|
||||||
return v > maxMemorySize
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://github.com/kubernetes/kubernetes/blob/b47f8263e18c7b13dba33fba23187e5e0477cdbd/pkg/kubelet/stats/helper.go#L68-L71
|
|
||||||
func getAvailableBytes(memory *v1.MemoryStat, workingSetBytes uint64) uint64 {
|
|
||||||
// memory limit - working set bytes
|
|
||||||
if !isMemoryUnlimited(memory.Usage.Limit) {
|
|
||||||
return memory.Usage.Limit - workingSetBytes
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func getAvailableBytesV2(memory *v2.MemoryStat, workingSetBytes uint64) uint64 {
|
|
||||||
// memory limit (memory.max) for cgroupv2 - working set bytes
|
|
||||||
if !isMemoryUnlimited(memory.UsageLimit) {
|
|
||||||
return memory.UsageLimit - workingSetBytes
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *criService) cpuContainerStats(ID string, isSandbox bool, stats interface{}, timestamp time.Time) (*runtime.CpuUsage, error) {
|
|
||||||
switch metrics := stats.(type) {
|
|
||||||
case *v1.Metrics:
|
|
||||||
if metrics.CPU != nil && metrics.CPU.Usage != nil {
|
|
||||||
return &runtime.CpuUsage{
|
|
||||||
Timestamp: timestamp.UnixNano(),
|
|
||||||
UsageCoreNanoSeconds: &runtime.UInt64Value{Value: metrics.CPU.Usage.Total},
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
case *v2.Metrics:
|
|
||||||
if metrics.CPU != nil {
|
|
||||||
// convert to nano seconds
|
|
||||||
usageCoreNanoSeconds := metrics.CPU.UsageUsec * 1000
|
|
||||||
|
|
||||||
return &runtime.CpuUsage{
|
|
||||||
Timestamp: timestamp.UnixNano(),
|
|
||||||
UsageCoreNanoSeconds: &runtime.UInt64Value{Value: usageCoreNanoSeconds},
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("unexpected metrics type: %T from %s", metrics, reflect.TypeOf(metrics).Elem().PkgPath())
|
|
||||||
}
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *criService) memoryContainerStats(ID string, stats interface{}, timestamp time.Time) (*runtime.MemoryUsage, error) {
|
|
||||||
switch metrics := stats.(type) {
|
|
||||||
case *v1.Metrics:
|
|
||||||
if metrics.Memory != nil && metrics.Memory.Usage != nil {
|
|
||||||
workingSetBytes := getWorkingSet(metrics.Memory)
|
|
||||||
|
|
||||||
return &runtime.MemoryUsage{
|
|
||||||
Timestamp: timestamp.UnixNano(),
|
|
||||||
WorkingSetBytes: &runtime.UInt64Value{
|
|
||||||
Value: workingSetBytes,
|
|
||||||
},
|
|
||||||
AvailableBytes: &runtime.UInt64Value{Value: getAvailableBytes(metrics.Memory, workingSetBytes)},
|
|
||||||
UsageBytes: &runtime.UInt64Value{Value: metrics.Memory.Usage.Usage},
|
|
||||||
RssBytes: &runtime.UInt64Value{Value: metrics.Memory.TotalRSS},
|
|
||||||
PageFaults: &runtime.UInt64Value{Value: metrics.Memory.TotalPgFault},
|
|
||||||
MajorPageFaults: &runtime.UInt64Value{Value: metrics.Memory.TotalPgMajFault},
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
case *v2.Metrics:
|
|
||||||
if metrics.Memory != nil {
|
|
||||||
workingSetBytes := getWorkingSetV2(metrics.Memory)
|
|
||||||
|
|
||||||
return &runtime.MemoryUsage{
|
|
||||||
Timestamp: timestamp.UnixNano(),
|
|
||||||
WorkingSetBytes: &runtime.UInt64Value{
|
|
||||||
Value: workingSetBytes,
|
|
||||||
},
|
|
||||||
AvailableBytes: &runtime.UInt64Value{Value: getAvailableBytesV2(metrics.Memory, workingSetBytes)},
|
|
||||||
UsageBytes: &runtime.UInt64Value{Value: metrics.Memory.Usage},
|
|
||||||
// Use Anon memory for RSS as cAdvisor on cgroupv2
|
|
||||||
// see https://github.com/google/cadvisor/blob/a9858972e75642c2b1914c8d5428e33e6392c08a/container/libcontainer/handler.go#L799
|
|
||||||
RssBytes: &runtime.UInt64Value{Value: metrics.Memory.Anon},
|
|
||||||
PageFaults: &runtime.UInt64Value{Value: metrics.Memory.Pgfault},
|
|
||||||
MajorPageFaults: &runtime.UInt64Value{Value: metrics.Memory.Pgmajfault},
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("unexpected metrics type: %T from %s", metrics, reflect.TypeOf(metrics).Elem().PkgPath())
|
|
||||||
}
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
@ -1,281 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright The containerd Authors.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package sbserver
|
|
||||||
|
|
||||||
import (
|
|
||||||
"math"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
v1 "github.com/containerd/cgroups/v3/cgroup1/stats"
|
|
||||||
v2 "github.com/containerd/cgroups/v3/cgroup2/stats"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
runtime "k8s.io/cri-api/pkg/apis/runtime/v1"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestGetWorkingSet(t *testing.T) {
|
|
||||||
for _, test := range []struct {
|
|
||||||
desc string
|
|
||||||
memory *v1.MemoryStat
|
|
||||||
expected uint64
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
desc: "nil memory usage",
|
|
||||||
memory: &v1.MemoryStat{},
|
|
||||||
expected: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "memory usage higher than inactive_total_file",
|
|
||||||
memory: &v1.MemoryStat{
|
|
||||||
TotalInactiveFile: 1000,
|
|
||||||
Usage: &v1.MemoryEntry{Usage: 2000},
|
|
||||||
},
|
|
||||||
expected: 1000,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "memory usage lower than inactive_total_file",
|
|
||||||
memory: &v1.MemoryStat{
|
|
||||||
TotalInactiveFile: 2000,
|
|
||||||
Usage: &v1.MemoryEntry{Usage: 1000},
|
|
||||||
},
|
|
||||||
expected: 0,
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
test := test
|
|
||||||
t.Run(test.desc, func(t *testing.T) {
|
|
||||||
got := getWorkingSet(test.memory)
|
|
||||||
assert.Equal(t, test.expected, got)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetWorkingSetV2(t *testing.T) {
|
|
||||||
for _, test := range []struct {
|
|
||||||
desc string
|
|
||||||
memory *v2.MemoryStat
|
|
||||||
expected uint64
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
desc: "nil memory usage",
|
|
||||||
memory: &v2.MemoryStat{},
|
|
||||||
expected: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "memory usage higher than inactive_total_file",
|
|
||||||
memory: &v2.MemoryStat{
|
|
||||||
InactiveFile: 1000,
|
|
||||||
Usage: 2000,
|
|
||||||
},
|
|
||||||
expected: 1000,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "memory usage lower than inactive_total_file",
|
|
||||||
memory: &v2.MemoryStat{
|
|
||||||
InactiveFile: 2000,
|
|
||||||
Usage: 1000,
|
|
||||||
},
|
|
||||||
expected: 0,
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
test := test
|
|
||||||
t.Run(test.desc, func(t *testing.T) {
|
|
||||||
got := getWorkingSetV2(test.memory)
|
|
||||||
assert.Equal(t, test.expected, got)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetAvailableBytes(t *testing.T) {
|
|
||||||
for _, test := range []struct {
|
|
||||||
desc string
|
|
||||||
memory *v1.MemoryStat
|
|
||||||
workingSetBytes uint64
|
|
||||||
expected uint64
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
desc: "no limit",
|
|
||||||
memory: &v1.MemoryStat{
|
|
||||||
Usage: &v1.MemoryEntry{
|
|
||||||
Limit: math.MaxUint64, // no limit
|
|
||||||
Usage: 1000,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
workingSetBytes: 500,
|
|
||||||
expected: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "with limit",
|
|
||||||
memory: &v1.MemoryStat{
|
|
||||||
Usage: &v1.MemoryEntry{
|
|
||||||
Limit: 5000,
|
|
||||||
Usage: 1000,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
workingSetBytes: 500,
|
|
||||||
expected: 5000 - 500,
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
test := test
|
|
||||||
t.Run(test.desc, func(t *testing.T) {
|
|
||||||
got := getAvailableBytes(test.memory, test.workingSetBytes)
|
|
||||||
assert.Equal(t, test.expected, got)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetAvailableBytesV2(t *testing.T) {
|
|
||||||
for _, test := range []struct {
|
|
||||||
desc string
|
|
||||||
memory *v2.MemoryStat
|
|
||||||
workingSetBytes uint64
|
|
||||||
expected uint64
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
desc: "no limit",
|
|
||||||
memory: &v2.MemoryStat{
|
|
||||||
UsageLimit: math.MaxUint64, // no limit
|
|
||||||
Usage: 1000,
|
|
||||||
},
|
|
||||||
workingSetBytes: 500,
|
|
||||||
expected: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "with limit",
|
|
||||||
memory: &v2.MemoryStat{
|
|
||||||
UsageLimit: 5000,
|
|
||||||
Usage: 1000,
|
|
||||||
},
|
|
||||||
workingSetBytes: 500,
|
|
||||||
expected: 5000 - 500,
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
test := test
|
|
||||||
t.Run(test.desc, func(t *testing.T) {
|
|
||||||
got := getAvailableBytesV2(test.memory, test.workingSetBytes)
|
|
||||||
assert.Equal(t, test.expected, got)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestContainerMetricsMemory(t *testing.T) {
|
|
||||||
c := newTestCRIService()
|
|
||||||
timestamp := time.Now()
|
|
||||||
|
|
||||||
for _, test := range []struct {
|
|
||||||
desc string
|
|
||||||
metrics interface{}
|
|
||||||
expected *runtime.MemoryUsage
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
desc: "v1 metrics - no memory limit",
|
|
||||||
metrics: &v1.Metrics{
|
|
||||||
Memory: &v1.MemoryStat{
|
|
||||||
Usage: &v1.MemoryEntry{
|
|
||||||
Limit: math.MaxUint64, // no limit
|
|
||||||
Usage: 1000,
|
|
||||||
},
|
|
||||||
TotalRSS: 10,
|
|
||||||
TotalPgFault: 11,
|
|
||||||
TotalPgMajFault: 12,
|
|
||||||
TotalInactiveFile: 500,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expected: &runtime.MemoryUsage{
|
|
||||||
Timestamp: timestamp.UnixNano(),
|
|
||||||
WorkingSetBytes: &runtime.UInt64Value{Value: 500},
|
|
||||||
AvailableBytes: &runtime.UInt64Value{Value: 0},
|
|
||||||
UsageBytes: &runtime.UInt64Value{Value: 1000},
|
|
||||||
RssBytes: &runtime.UInt64Value{Value: 10},
|
|
||||||
PageFaults: &runtime.UInt64Value{Value: 11},
|
|
||||||
MajorPageFaults: &runtime.UInt64Value{Value: 12},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "v1 metrics - memory limit",
|
|
||||||
metrics: &v1.Metrics{
|
|
||||||
Memory: &v1.MemoryStat{
|
|
||||||
Usage: &v1.MemoryEntry{
|
|
||||||
Limit: 5000,
|
|
||||||
Usage: 1000,
|
|
||||||
},
|
|
||||||
TotalRSS: 10,
|
|
||||||
TotalPgFault: 11,
|
|
||||||
TotalPgMajFault: 12,
|
|
||||||
TotalInactiveFile: 500,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expected: &runtime.MemoryUsage{
|
|
||||||
Timestamp: timestamp.UnixNano(),
|
|
||||||
WorkingSetBytes: &runtime.UInt64Value{Value: 500},
|
|
||||||
AvailableBytes: &runtime.UInt64Value{Value: 4500},
|
|
||||||
UsageBytes: &runtime.UInt64Value{Value: 1000},
|
|
||||||
RssBytes: &runtime.UInt64Value{Value: 10},
|
|
||||||
PageFaults: &runtime.UInt64Value{Value: 11},
|
|
||||||
MajorPageFaults: &runtime.UInt64Value{Value: 12},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "v2 metrics - memory limit",
|
|
||||||
metrics: &v2.Metrics{
|
|
||||||
Memory: &v2.MemoryStat{
|
|
||||||
Usage: 1000,
|
|
||||||
UsageLimit: 5000,
|
|
||||||
InactiveFile: 0,
|
|
||||||
Pgfault: 11,
|
|
||||||
Pgmajfault: 12,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expected: &runtime.MemoryUsage{
|
|
||||||
Timestamp: timestamp.UnixNano(),
|
|
||||||
WorkingSetBytes: &runtime.UInt64Value{Value: 1000},
|
|
||||||
AvailableBytes: &runtime.UInt64Value{Value: 4000},
|
|
||||||
UsageBytes: &runtime.UInt64Value{Value: 1000},
|
|
||||||
RssBytes: &runtime.UInt64Value{Value: 0},
|
|
||||||
PageFaults: &runtime.UInt64Value{Value: 11},
|
|
||||||
MajorPageFaults: &runtime.UInt64Value{Value: 12},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "v2 metrics - no memory limit",
|
|
||||||
metrics: &v2.Metrics{
|
|
||||||
Memory: &v2.MemoryStat{
|
|
||||||
Usage: 1000,
|
|
||||||
UsageLimit: math.MaxUint64, // no limit
|
|
||||||
InactiveFile: 0,
|
|
||||||
Pgfault: 11,
|
|
||||||
Pgmajfault: 12,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expected: &runtime.MemoryUsage{
|
|
||||||
Timestamp: timestamp.UnixNano(),
|
|
||||||
WorkingSetBytes: &runtime.UInt64Value{Value: 1000},
|
|
||||||
AvailableBytes: &runtime.UInt64Value{Value: 0},
|
|
||||||
UsageBytes: &runtime.UInt64Value{Value: 1000},
|
|
||||||
RssBytes: &runtime.UInt64Value{Value: 0},
|
|
||||||
PageFaults: &runtime.UInt64Value{Value: 11},
|
|
||||||
MajorPageFaults: &runtime.UInt64Value{Value: 12},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
test := test
|
|
||||||
t.Run(test.desc, func(t *testing.T) {
|
|
||||||
got, err := c.memoryContainerStats("ID", test.metrics, timestamp)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, test.expected, got)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,37 +0,0 @@
|
|||||||
//go:build !windows && !linux
|
|
||||||
|
|
||||||
/*
|
|
||||||
Copyright The containerd Authors.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package sbserver
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/containerd/containerd/api/types"
|
|
||||||
"github.com/containerd/containerd/errdefs"
|
|
||||||
runtime "k8s.io/cri-api/pkg/apis/runtime/v1"
|
|
||||||
|
|
||||||
containerstore "github.com/containerd/containerd/pkg/cri/store/container"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (c *criService) containerMetrics(
|
|
||||||
meta containerstore.Metadata,
|
|
||||||
stats *types.Metric,
|
|
||||||
) (*runtime.ContainerStats, error) {
|
|
||||||
var cs runtime.ContainerStats
|
|
||||||
return &cs, fmt.Errorf("container metrics: %w", errdefs.ErrNotImplemented)
|
|
||||||
}
|
|
@ -17,11 +17,15 @@
|
|||||||
package sbserver
|
package sbserver
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"math"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
v1 "github.com/containerd/cgroups/v3/cgroup1/stats"
|
||||||
|
v2 "github.com/containerd/cgroups/v3/cgroup2/stats"
|
||||||
containerstore "github.com/containerd/containerd/pkg/cri/store/container"
|
containerstore "github.com/containerd/containerd/pkg/cri/store/container"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
runtime "k8s.io/cri-api/pkg/apis/runtime/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestContainerMetricsCPUNanoCoreUsage(t *testing.T) {
|
func TestContainerMetricsCPUNanoCoreUsage(t *testing.T) {
|
||||||
@ -74,3 +78,256 @@ func TestContainerMetricsCPUNanoCoreUsage(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGetWorkingSet(t *testing.T) {
|
||||||
|
for _, test := range []struct {
|
||||||
|
desc string
|
||||||
|
memory *v1.MemoryStat
|
||||||
|
expected uint64
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "nil memory usage",
|
||||||
|
memory: &v1.MemoryStat{},
|
||||||
|
expected: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "memory usage higher than inactive_total_file",
|
||||||
|
memory: &v1.MemoryStat{
|
||||||
|
TotalInactiveFile: 1000,
|
||||||
|
Usage: &v1.MemoryEntry{Usage: 2000},
|
||||||
|
},
|
||||||
|
expected: 1000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "memory usage lower than inactive_total_file",
|
||||||
|
memory: &v1.MemoryStat{
|
||||||
|
TotalInactiveFile: 2000,
|
||||||
|
Usage: &v1.MemoryEntry{Usage: 1000},
|
||||||
|
},
|
||||||
|
expected: 0,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
test := test
|
||||||
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
|
got := getWorkingSet(test.memory)
|
||||||
|
assert.Equal(t, test.expected, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetWorkingSetV2(t *testing.T) {
|
||||||
|
for _, test := range []struct {
|
||||||
|
desc string
|
||||||
|
memory *v2.MemoryStat
|
||||||
|
expected uint64
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "nil memory usage",
|
||||||
|
memory: &v2.MemoryStat{},
|
||||||
|
expected: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "memory usage higher than inactive_total_file",
|
||||||
|
memory: &v2.MemoryStat{
|
||||||
|
InactiveFile: 1000,
|
||||||
|
Usage: 2000,
|
||||||
|
},
|
||||||
|
expected: 1000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "memory usage lower than inactive_total_file",
|
||||||
|
memory: &v2.MemoryStat{
|
||||||
|
InactiveFile: 2000,
|
||||||
|
Usage: 1000,
|
||||||
|
},
|
||||||
|
expected: 0,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
test := test
|
||||||
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
|
got := getWorkingSetV2(test.memory)
|
||||||
|
assert.Equal(t, test.expected, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetAvailableBytes(t *testing.T) {
|
||||||
|
for _, test := range []struct {
|
||||||
|
desc string
|
||||||
|
memory *v1.MemoryStat
|
||||||
|
workingSetBytes uint64
|
||||||
|
expected uint64
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "no limit",
|
||||||
|
memory: &v1.MemoryStat{
|
||||||
|
Usage: &v1.MemoryEntry{
|
||||||
|
Limit: math.MaxUint64, // no limit
|
||||||
|
Usage: 1000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
workingSetBytes: 500,
|
||||||
|
expected: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "with limit",
|
||||||
|
memory: &v1.MemoryStat{
|
||||||
|
Usage: &v1.MemoryEntry{
|
||||||
|
Limit: 5000,
|
||||||
|
Usage: 1000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
workingSetBytes: 500,
|
||||||
|
expected: 5000 - 500,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
test := test
|
||||||
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
|
got := getAvailableBytes(test.memory, test.workingSetBytes)
|
||||||
|
assert.Equal(t, test.expected, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetAvailableBytesV2(t *testing.T) {
|
||||||
|
for _, test := range []struct {
|
||||||
|
desc string
|
||||||
|
memory *v2.MemoryStat
|
||||||
|
workingSetBytes uint64
|
||||||
|
expected uint64
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "no limit",
|
||||||
|
memory: &v2.MemoryStat{
|
||||||
|
UsageLimit: math.MaxUint64, // no limit
|
||||||
|
Usage: 1000,
|
||||||
|
},
|
||||||
|
workingSetBytes: 500,
|
||||||
|
expected: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "with limit",
|
||||||
|
memory: &v2.MemoryStat{
|
||||||
|
UsageLimit: 5000,
|
||||||
|
Usage: 1000,
|
||||||
|
},
|
||||||
|
workingSetBytes: 500,
|
||||||
|
expected: 5000 - 500,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
test := test
|
||||||
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
|
got := getAvailableBytesV2(test.memory, test.workingSetBytes)
|
||||||
|
assert.Equal(t, test.expected, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContainerMetricsMemory(t *testing.T) {
|
||||||
|
c := newTestCRIService()
|
||||||
|
timestamp := time.Now()
|
||||||
|
|
||||||
|
for _, test := range []struct {
|
||||||
|
desc string
|
||||||
|
metrics interface{}
|
||||||
|
expected *runtime.MemoryUsage
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "v1 metrics - no memory limit",
|
||||||
|
metrics: &v1.Metrics{
|
||||||
|
Memory: &v1.MemoryStat{
|
||||||
|
Usage: &v1.MemoryEntry{
|
||||||
|
Limit: math.MaxUint64, // no limit
|
||||||
|
Usage: 1000,
|
||||||
|
},
|
||||||
|
TotalRSS: 10,
|
||||||
|
TotalPgFault: 11,
|
||||||
|
TotalPgMajFault: 12,
|
||||||
|
TotalInactiveFile: 500,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: &runtime.MemoryUsage{
|
||||||
|
Timestamp: timestamp.UnixNano(),
|
||||||
|
WorkingSetBytes: &runtime.UInt64Value{Value: 500},
|
||||||
|
AvailableBytes: &runtime.UInt64Value{Value: 0},
|
||||||
|
UsageBytes: &runtime.UInt64Value{Value: 1000},
|
||||||
|
RssBytes: &runtime.UInt64Value{Value: 10},
|
||||||
|
PageFaults: &runtime.UInt64Value{Value: 11},
|
||||||
|
MajorPageFaults: &runtime.UInt64Value{Value: 12},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "v1 metrics - memory limit",
|
||||||
|
metrics: &v1.Metrics{
|
||||||
|
Memory: &v1.MemoryStat{
|
||||||
|
Usage: &v1.MemoryEntry{
|
||||||
|
Limit: 5000,
|
||||||
|
Usage: 1000,
|
||||||
|
},
|
||||||
|
TotalRSS: 10,
|
||||||
|
TotalPgFault: 11,
|
||||||
|
TotalPgMajFault: 12,
|
||||||
|
TotalInactiveFile: 500,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: &runtime.MemoryUsage{
|
||||||
|
Timestamp: timestamp.UnixNano(),
|
||||||
|
WorkingSetBytes: &runtime.UInt64Value{Value: 500},
|
||||||
|
AvailableBytes: &runtime.UInt64Value{Value: 4500},
|
||||||
|
UsageBytes: &runtime.UInt64Value{Value: 1000},
|
||||||
|
RssBytes: &runtime.UInt64Value{Value: 10},
|
||||||
|
PageFaults: &runtime.UInt64Value{Value: 11},
|
||||||
|
MajorPageFaults: &runtime.UInt64Value{Value: 12},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "v2 metrics - memory limit",
|
||||||
|
metrics: &v2.Metrics{
|
||||||
|
Memory: &v2.MemoryStat{
|
||||||
|
Usage: 1000,
|
||||||
|
UsageLimit: 5000,
|
||||||
|
InactiveFile: 0,
|
||||||
|
Pgfault: 11,
|
||||||
|
Pgmajfault: 12,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: &runtime.MemoryUsage{
|
||||||
|
Timestamp: timestamp.UnixNano(),
|
||||||
|
WorkingSetBytes: &runtime.UInt64Value{Value: 1000},
|
||||||
|
AvailableBytes: &runtime.UInt64Value{Value: 4000},
|
||||||
|
UsageBytes: &runtime.UInt64Value{Value: 1000},
|
||||||
|
RssBytes: &runtime.UInt64Value{Value: 0},
|
||||||
|
PageFaults: &runtime.UInt64Value{Value: 11},
|
||||||
|
MajorPageFaults: &runtime.UInt64Value{Value: 12},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "v2 metrics - no memory limit",
|
||||||
|
metrics: &v2.Metrics{
|
||||||
|
Memory: &v2.MemoryStat{
|
||||||
|
Usage: 1000,
|
||||||
|
UsageLimit: math.MaxUint64, // no limit
|
||||||
|
InactiveFile: 0,
|
||||||
|
Pgfault: 11,
|
||||||
|
Pgmajfault: 12,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: &runtime.MemoryUsage{
|
||||||
|
Timestamp: timestamp.UnixNano(),
|
||||||
|
WorkingSetBytes: &runtime.UInt64Value{Value: 1000},
|
||||||
|
AvailableBytes: &runtime.UInt64Value{Value: 0},
|
||||||
|
UsageBytes: &runtime.UInt64Value{Value: 1000},
|
||||||
|
RssBytes: &runtime.UInt64Value{Value: 0},
|
||||||
|
PageFaults: &runtime.UInt64Value{Value: 11},
|
||||||
|
MajorPageFaults: &runtime.UInt64Value{Value: 12},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
test := test
|
||||||
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
|
got, err := c.memoryContainerStats("ID", test.metrics, timestamp)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, test.expected, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,84 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright The containerd Authors.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package sbserver
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
wstats "github.com/Microsoft/hcsshim/cmd/containerd-shim-runhcs-v1/stats"
|
|
||||||
"github.com/containerd/containerd/api/types"
|
|
||||||
"github.com/containerd/typeurl/v2"
|
|
||||||
runtime "k8s.io/cri-api/pkg/apis/runtime/v1"
|
|
||||||
|
|
||||||
containerstore "github.com/containerd/containerd/pkg/cri/store/container"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (c *criService) containerMetrics(
|
|
||||||
meta containerstore.Metadata,
|
|
||||||
stats *types.Metric,
|
|
||||||
) (*runtime.ContainerStats, error) {
|
|
||||||
var cs runtime.ContainerStats
|
|
||||||
var usedBytes, inodesUsed uint64
|
|
||||||
sn, err := c.GetSnapshot(meta.ID)
|
|
||||||
// If snapshotstore doesn't have cached snapshot information
|
|
||||||
// set WritableLayer usage to zero
|
|
||||||
if err == nil {
|
|
||||||
usedBytes = sn.Size
|
|
||||||
inodesUsed = sn.Inodes
|
|
||||||
}
|
|
||||||
cs.WritableLayer = &runtime.FilesystemUsage{
|
|
||||||
Timestamp: sn.Timestamp,
|
|
||||||
FsId: &runtime.FilesystemIdentifier{
|
|
||||||
Mountpoint: c.imageFSPath,
|
|
||||||
},
|
|
||||||
UsedBytes: &runtime.UInt64Value{Value: usedBytes},
|
|
||||||
InodesUsed: &runtime.UInt64Value{Value: inodesUsed},
|
|
||||||
}
|
|
||||||
cs.Attributes = &runtime.ContainerAttributes{
|
|
||||||
Id: meta.ID,
|
|
||||||
Metadata: meta.Config.GetMetadata(),
|
|
||||||
Labels: meta.Config.GetLabels(),
|
|
||||||
Annotations: meta.Config.GetAnnotations(),
|
|
||||||
}
|
|
||||||
|
|
||||||
if stats != nil {
|
|
||||||
s, err := typeurl.UnmarshalAny(stats.Data)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to extract container metrics: %w", err)
|
|
||||||
}
|
|
||||||
wstats := s.(*wstats.Statistics).GetWindows()
|
|
||||||
if wstats == nil {
|
|
||||||
return nil, errors.New("windows stats is empty")
|
|
||||||
}
|
|
||||||
if wstats.Processor != nil {
|
|
||||||
cs.Cpu = &runtime.CpuUsage{
|
|
||||||
Timestamp: wstats.Timestamp.UnixNano(),
|
|
||||||
UsageCoreNanoSeconds: &runtime.UInt64Value{Value: wstats.Processor.TotalRuntimeNS},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if wstats.Memory != nil {
|
|
||||||
cs.Memory = &runtime.MemoryUsage{
|
|
||||||
Timestamp: wstats.Timestamp.UnixNano(),
|
|
||||||
WorkingSetBytes: &runtime.UInt64Value{
|
|
||||||
Value: wstats.Memory.MemoryUsagePrivateWorkingSetBytes,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return &cs, nil
|
|
||||||
}
|
|
@ -36,7 +36,8 @@ import (
|
|||||||
|
|
||||||
func (c *criService) podSandboxStats(
|
func (c *criService) podSandboxStats(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
sandbox sandboxstore.Sandbox) (*runtime.PodSandboxStats, error) {
|
sandbox sandboxstore.Sandbox,
|
||||||
|
) (*runtime.PodSandboxStats, error) {
|
||||||
meta := sandbox.Metadata
|
meta := sandbox.Metadata
|
||||||
|
|
||||||
if sandbox.Status.Get().State != sandboxstore.StateReady {
|
if sandbox.Status.Get().State != sandboxstore.StateReady {
|
||||||
|
Loading…
Reference in New Issue
Block a user