Merge pull request #15769 from dfr/freebsd-stats

Add support for 'podman stats' on FreeBSD
This commit is contained in:
OpenShift Merge Robot 2022-09-14 14:21:30 +02:00 committed by GitHub
commit 82651e5974
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 265 additions and 38 deletions

49
libpod/stats_common.go Normal file
View File

@ -0,0 +1,49 @@
//go:build linux || freebsd
// +build linux freebsd
package libpod
import (
"fmt"
"github.com/containers/podman/v4/libpod/define"
)
// GetContainerStats gets the running stats for a given container.
// The previousStats is used to correctly calculate cpu percentages. You
// should pass nil if there is no previous stat for this container.
func (c *Container) GetContainerStats(previousStats *define.ContainerStats) (*define.ContainerStats, error) {
stats := new(define.ContainerStats)
stats.ContainerID = c.ID()
stats.Name = c.Name()
if c.config.NoCgroups {
return nil, fmt.Errorf("cannot run top on container %s as it did not create a cgroup: %w", c.ID(), define.ErrNoCgroups)
}
if !c.batched {
c.lock.Lock()
defer c.lock.Unlock()
if err := c.syncContainer(); err != nil {
return stats, err
}
}
// returns stats with the fields' default values respective of their type
if c.state.State != define.ContainerStateRunning && c.state.State != define.ContainerStatePaused {
return stats, nil
}
if previousStats == nil {
previousStats = &define.ContainerStats{
// if we have no prev stats use the container start time as prev time
// otherwise we cannot correctly calculate the CPU percentage
SystemNano: uint64(c.state.StartedTime.UnixNano()),
}
}
if err := c.getPlatformContainerStats(stats, previousStats); err != nil {
return nil, err
}
return stats, nil
}

153
libpod/stats_freebsd.go Normal file
View File

@ -0,0 +1,153 @@
package libpod
import (
"fmt"
"math"
"strings"
"time"
"github.com/containers/common/pkg/cgroups"
"github.com/containers/podman/v4/libpod/define"
"github.com/containers/podman/v4/pkg/rctl"
"github.com/containers/storage/pkg/system"
"github.com/sirupsen/logrus"
)
// getPlatformContainerStats gets the platform-specific running stats
// for a given container. The previousStats is used to correctly
// calculate cpu percentages. You should pass nil if there is no
// previous stat for this container.
func (c *Container) getPlatformContainerStats(stats *define.ContainerStats, previousStats *define.ContainerStats) error {
now := uint64(time.Now().UnixNano())
jailName := c.ID()
if c.state.NetNS != nil {
jailName = c.state.NetNS.Name + "." + jailName
}
entries, err := rctl.GetRacct("jail:" + jailName)
if err != nil {
return fmt.Errorf("unable to read accounting for %s: %w", jailName, err)
}
// If the current total usage is less than what was previously
// recorded then it means the container was restarted and runs
// in a new jail
if dur, ok := entries["wallclock"]; ok {
if previousStats.Duration > dur*1000000000 {
previousStats = &define.ContainerStats{}
}
}
for key, val := range entries {
switch key {
case "cputime": // CPU time, in seconds
stats.CPUNano = val * 1000000000
stats.AvgCPU = calculateCPUPercent(stats.CPUNano, 0, now, uint64(c.state.StartedTime.UnixNano()))
case "datasize": // data size, in bytes
case "stacksize": // stack size, in bytes
case "coredumpsize": // core dump size, in bytes
case "memoryuse": // resident set size, in bytes
stats.MemUsage = val
case "memorylocked": // locked memory, in bytes
case "maxproc": // number of processes
stats.PIDs = val
case "openfiles": // file descriptor table size
case "vmemoryuse": // address space limit, in bytes
case "pseudoterminals": // number of PTYs
case "swapuse": // swap space that may be reserved or used, in bytes
case "nthr": // number of threads
case "msgqqueued": // number of queued SysV messages
case "msgqsize": // SysV message queue size, in bytes
case "nmsgq": // number of SysV message queues
case "nsem": // number of SysV semaphores
case "nsemop": // number of SysV semaphores modified in a single semop(2) call
case "nshm": // number of SysV shared memory segments
case "shmsize": // SysV shared memory size, in bytes
case "wallclock": // wallclock time, in seconds
stats.Duration = val * 1000000000
stats.UpTime = time.Duration(stats.Duration)
case "pcpu": // %CPU, in percents of a single CPU core
stats.CPU = float64(val)
case "readbps": // filesystem reads, in bytes per second
stats.BlockInput = val
case "writebps": // filesystem writes, in bytes per second
stats.BlockOutput = val
case "readiops": // filesystem reads, in operations per second
case "writeiops": // filesystem writes, in operations per second
}
}
stats.MemLimit = c.getMemLimit()
stats.SystemNano = now
netStats, err := getContainerNetIO(c)
if err != nil {
return err
}
// Handle case where the container is not in a network namespace
if netStats != nil {
stats.NetInput = netStats.TxBytes
stats.NetOutput = netStats.RxBytes
} else {
stats.NetInput = 0
stats.NetOutput = 0
}
return nil
}
// getMemory limit returns the memory limit for a container
func (c *Container) getMemLimit() uint64 {
memLimit := uint64(math.MaxUint64)
if c.config.Spec.Linux != nil && c.config.Spec.Linux.Resources != nil &&
c.config.Spec.Linux.Resources.Memory != nil && c.config.Spec.Linux.Resources.Memory.Limit != nil {
memLimit = uint64(*c.config.Spec.Linux.Resources.Memory.Limit)
}
mi, err := system.ReadMemInfo()
if err != nil {
logrus.Errorf("ReadMemInfo error: %v", err)
return 0
}
//nolint:unconvert
physicalLimit := uint64(mi.MemTotal)
if memLimit <= 0 || memLimit > physicalLimit {
return physicalLimit
}
return memLimit
}
// calculateCPUPercent calculates the cpu usage using the latest measurement in stats.
// previousCPU is the last value of stats.CPU.Usage.Total measured at the time previousSystem.
//
// (now - previousSystem) is the time delta in nanoseconds, between the measurement in previousCPU
//
// and the updated value in stats.
func calculateCPUPercent(currentCPU, previousCPU, now, previousSystem uint64) float64 {
var (
cpuPercent = 0.0
cpuDelta = float64(currentCPU - previousCPU)
systemDelta = float64(now - previousSystem)
)
if systemDelta > 0.0 && cpuDelta > 0.0 {
// gets a ratio of container cpu usage total, and multiplies that by 100 to get a percentage
cpuPercent = (cpuDelta / systemDelta) * 100
}
return cpuPercent
}
func calculateBlockIO(stats *cgroups.Metrics) (read uint64, write uint64) {
for _, blkIOEntry := range stats.Blkio.IoServiceBytesRecursive {
switch strings.ToLower(blkIOEntry.Op) {
case "read":
read += blkIOEntry.Value
case "write":
write += blkIOEntry.Value
}
}
return
}

View File

@ -16,57 +16,33 @@ import (
"github.com/containers/podman/v4/libpod/define"
)
// GetContainerStats gets the running stats for a given container.
// The previousStats is used to correctly calculate cpu percentages. You
// should pass nil if there is no previous stat for this container.
func (c *Container) GetContainerStats(previousStats *define.ContainerStats) (*define.ContainerStats, error) {
stats := new(define.ContainerStats)
stats.ContainerID = c.ID()
stats.Name = c.Name()
// getPlatformContainerStats gets the platform-specific running stats
// for a given container. The previousStats is used to correctly
// calculate cpu percentages. You should pass nil if there is no
// previous stat for this container.
func (c *Container) getPlatformContainerStats(stats *define.ContainerStats, previousStats *define.ContainerStats) error {
if c.config.NoCgroups {
return nil, fmt.Errorf("cannot run top on container %s as it did not create a cgroup: %w", c.ID(), define.ErrNoCgroups)
}
if !c.batched {
c.lock.Lock()
defer c.lock.Unlock()
if err := c.syncContainer(); err != nil {
return stats, err
}
}
// returns stats with the fields' default values respective of their type
if c.state.State != define.ContainerStateRunning && c.state.State != define.ContainerStatePaused {
return stats, nil
}
if previousStats == nil {
previousStats = &define.ContainerStats{
// if we have no prev stats use the container start time as prev time
// otherwise we cannot correctly calculate the CPU percentage
SystemNano: uint64(c.state.StartedTime.UnixNano()),
}
return fmt.Errorf("cannot run top on container %s as it did not create a cgroup: %w", c.ID(), define.ErrNoCgroups)
}
cgroupPath, err := c.cGroupPath()
if err != nil {
return nil, err
return err
}
cgroup, err := cgroups.Load(cgroupPath)
if err != nil {
return stats, fmt.Errorf("unable to load cgroup at %s: %w", cgroupPath, err)
return fmt.Errorf("unable to load cgroup at %s: %w", cgroupPath, err)
}
// Ubuntu does not have swap memory in cgroups because swap is often not enabled.
cgroupStats, err := cgroup.Stat()
if err != nil {
return stats, fmt.Errorf("unable to obtain cgroup stats: %w", err)
return fmt.Errorf("unable to obtain cgroup stats: %w", err)
}
conState := c.state.State
netStats, err := getContainerNetIO(c)
if err != nil {
return nil, err
return err
}
// If the current total usage in the cgroup is less than what was previously
@ -103,7 +79,7 @@ func (c *Container) GetContainerStats(previousStats *define.ContainerStats) (*de
stats.NetOutput = 0
}
return stats, nil
return nil
}
// getMemory limit returns the memory limit for a container
@ -133,7 +109,9 @@ func (c *Container) getMemLimit() uint64 {
// calculateCPUPercent calculates the cpu usage using the latest measurement in stats.
// previousCPU is the last value of stats.CPU.Usage.Total measured at the time previousSystem.
// (now - previousSystem) is the time delta in nanoseconds, between the measurement in previousCPU
//
// (now - previousSystem) is the time delta in nanoseconds, between the measurement in previousCPU
//
// and the updated value in stats.
func calculateCPUPercent(stats *runccgroup.Stats, previousCPU, now, previousSystem uint64) float64 {
var (

View File

@ -1,5 +1,5 @@
//go:build !linux
// +build !linux
//go:build !linux && !freebsd
// +build !linux,!freebsd
package libpod

47
pkg/rctl/rctl.go Normal file
View File

@ -0,0 +1,47 @@
//go:build freebsd
// +build freebsd
package rctl
// #include <sys/rctl.h>
import "C"
import (
"bytes"
"fmt"
"strconv"
"strings"
"syscall"
"unsafe"
"github.com/sirupsen/logrus"
)
func GetRacct(filter string) (map[string]uint64, error) {
bp, err := syscall.ByteSliceFromString(filter)
if err != nil {
return nil, err
}
var buf [1024]byte
_, _, errno := syscall.Syscall6(syscall.SYS_RCTL_GET_RACCT,
uintptr(unsafe.Pointer(&bp[0])),
uintptr(len(bp)),
uintptr(unsafe.Pointer(&buf[0])),
uintptr(len(buf)), 0, 0)
if errno != 0 {
return nil, fmt.Errorf("error calling rctl_get_racct with filter %s: %v", errno)
}
len := bytes.IndexByte(buf[:], byte(0))
entries := strings.Split(string(buf[:len]), ",")
res := make(map[string]uint64)
for _, entry := range entries {
kv := strings.SplitN(entry, "=", 2)
key := kv[0]
val, err := strconv.ParseUint(kv[1], 10, 0)
if err != nil {
logrus.Warnf("unexpected rctl entry, ignoring: %s", entry)
}
res[key] = val
}
return res, nil
}