Files
containerd/internal/cri/server/sandbox_stats_linux.go
T
2026-06-17 11:43:00 -07:00

220 lines
7.0 KiB
Go

/*
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 server
import (
"context"
"fmt"
"time"
cg1 "github.com/containerd/cgroups/v3/cgroup1/stats"
cg2 "github.com/containerd/cgroups/v3/cgroup2/stats"
"github.com/containerd/errdefs"
"github.com/containerd/log"
"github.com/containerd/typeurl/v2"
"github.com/containernetworking/plugins/pkg/ns"
"github.com/vishvananda/netlink"
runtime "k8s.io/cri-api/pkg/apis/runtime/v1"
"github.com/containerd/containerd/api/types"
sandboxstore "github.com/containerd/containerd/v2/internal/cri/store/sandbox"
)
func (c *criService) podSandboxStats(
ctx context.Context,
sandbox sandboxstore.Sandbox) (*runtime.PodSandboxStats, error) {
meta := sandbox.Metadata
if sandbox.Status.Get().State != sandboxstore.StateReady {
return nil, fmt.Errorf("failed to get pod sandbox stats since sandbox container %q is not in ready state: %w", meta.ID, errdefs.ErrUnavailable)
}
ctrl, err := c.sandboxService.SandboxController(sandbox.Sandboxer)
if err != nil {
return nil, fmt.Errorf("failed to get controller for sandbox %s: %w", sandbox.ID, err)
}
metric, err := ctrl.Metrics(ctx, sandbox.ID)
if err != nil {
return nil, fmt.Errorf("failed getting metrics for sandbox %s: %w", sandbox.ID, err)
}
stats, err := decodeSandboxCgroupMetrics(metric)
if err != nil {
return nil, fmt.Errorf("failed decoding metrics for sandbox %s: %w", sandbox.ID, err)
}
podSandboxStats := &runtime.PodSandboxStats{
Linux: &runtime.LinuxPodSandboxStats{},
Attributes: &runtime.PodSandboxAttributes{
Id: meta.ID,
Metadata: meta.Config.GetMetadata(),
Labels: meta.Config.GetLabels(),
Annotations: meta.Config.GetAnnotations(),
},
}
timestamp := time.Now()
cpuStats, err := c.cpuContainerStats(*stats, timestamp)
if err != nil {
return nil, fmt.Errorf("failed to obtain cpu stats: %w", err)
}
if cpuStats != nil && cpuStats.UsageCoreNanoSeconds != nil {
nanoUsage, err := c.getUsageNanoCores(meta.ID, true /* isSandbox */, cpuStats.UsageCoreNanoSeconds.Value, timestamp)
if err != nil {
return nil, fmt.Errorf("failed to get usage nano cores: %w", err)
}
if nanoUsage != nil {
cpuStats.UsageNanoCores = &runtime.UInt64Value{Value: *nanoUsage}
}
}
podSandboxStats.Linux.Cpu = cpuStats
memoryStats, err := c.memoryContainerStats(*stats, timestamp)
if err != nil {
return nil, fmt.Errorf("failed to obtain memory stats: %w", err)
}
podSandboxStats.Linux.Memory = memoryStats
if sandbox.NetNSPath != "" {
allStats, err := getAllContainerNetIO(ctx, sandbox.NetNSPath)
if err != nil {
return nil, fmt.Errorf("failed to obtain network stats: %w", err)
}
networkUsage := &runtime.NetworkUsage{
Timestamp: timestamp.UnixNano(),
}
for _, ifStats := range allStats {
interfaceUsage := &runtime.NetworkInterfaceUsage{
Name: ifStats.name,
RxBytes: &runtime.UInt64Value{Value: ifStats.stats.RxBytes},
RxErrors: &runtime.UInt64Value{Value: ifStats.stats.RxErrors},
TxBytes: &runtime.UInt64Value{Value: ifStats.stats.TxBytes},
TxErrors: &runtime.UInt64Value{Value: ifStats.stats.TxErrors},
}
networkUsage.Interfaces = append(networkUsage.Interfaces, interfaceUsage)
// Set the default interface to eth0, or the first interface if eth0 is not found
if ifStats.name == defaultIfName {
networkUsage.DefaultInterface = interfaceUsage
}
}
// If we didn't find eth0, use the first interface as default
if networkUsage.DefaultInterface == nil && len(networkUsage.Interfaces) > 0 {
networkUsage.DefaultInterface = networkUsage.Interfaces[0]
}
podSandboxStats.Linux.Network = networkUsage
}
listContainerStatsRequest := &runtime.ListContainerStatsRequest{Filter: &runtime.ContainerStatsFilter{PodSandboxId: meta.ID}}
css, err := c.listContainerStats(ctx, listContainerStatsRequest)
if err != nil {
return nil, fmt.Errorf("failed to obtain container stats during podSandboxStats call: %w", err)
}
var pidCount uint64
for _, cs := range css {
pidCount += cs.pids
podSandboxStats.Linux.Containers = append(podSandboxStats.Linux.Containers, cs.stats)
}
podSandboxStats.Linux.Process = &runtime.ProcessUsage{
Timestamp: timestamp.UnixNano(),
ProcessCount: &runtime.UInt64Value{Value: pidCount},
}
return podSandboxStats, nil
}
// interfaceStats contains statistics for a single network interface
type interfaceStats struct {
name string
stats netlink.LinkStatistics64
}
// getContainerNetIO returns network stats for the default interface (eth0)
// https://github.com/cri-o/cri-o/blob/74a5cf8dffd305b311eb1c7f43a4781738c388c1/internal/oci/stats.go#L32
func getContainerNetIO(ctx context.Context, netNsPath string) (netlink.LinkStatistics64, error) {
var stats netlink.LinkStatistics64
err := ns.WithNetNSPath(netNsPath, func(_ ns.NetNS) error {
link, err := netlink.LinkByName(defaultIfName)
if err != nil {
log.G(ctx).WithError(err).Errorf("unable to retrieve network namespace stats for netNsPath: %v, interface: %v", netNsPath, defaultIfName)
return err
}
attrs := link.Attrs()
if attrs != nil && attrs.Statistics != nil {
stats = netlink.LinkStatistics64(*attrs.Statistics)
}
return nil
})
return stats, err
}
// getAllContainerNetIO returns network stats for all interfaces in the network namespace
func getAllContainerNetIO(ctx context.Context, netNsPath string) ([]interfaceStats, error) {
var allStats []interfaceStats
err := ns.WithNetNSPath(netNsPath, func(_ ns.NetNS) error {
links, err := netlink.LinkList()
if err != nil {
log.G(ctx).WithError(err).Errorf("unable to list network interfaces for netNsPath: %v", netNsPath)
return err
}
for _, link := range links {
attrs := link.Attrs()
if attrs == nil {
continue
}
// Skip loopback interface
if attrs.Name == "lo" {
continue
}
var stats netlink.LinkStatistics64
if attrs.Statistics != nil {
stats = netlink.LinkStatistics64(*attrs.Statistics)
}
allStats = append(allStats, interfaceStats{
name: attrs.Name,
stats: stats,
})
}
return nil
})
return allStats, err
}
func decodeSandboxCgroupMetrics(m *types.Metric) (*cgroupMetrics, error) {
if m == nil || m.Data == nil {
return nil, fmt.Errorf("sandbox metric is empty")
}
a, err := typeurl.UnmarshalAny(m.Data)
if err != nil {
return nil, fmt.Errorf("failed to extract sandbox metric: %w", err)
}
switch v := a.(type) {
case *cg1.Metrics:
return &cgroupMetrics{v1: v}, nil
case *cg2.Metrics:
return &cgroupMetrics{v2: v}, nil
default:
return nil, fmt.Errorf("unexpected sandbox metric type %T", v)
}
}