From 9cc0c8ae143070c4d4ee691cab6a0fd0ebcaa538 Mon Sep 17 00:00:00 2001 From: baude Date: Mon, 4 Dec 2017 15:06:06 -0600 Subject: kpod stats Move kpod stats to the libpod backend. Signed-off-by: baude Closes: #113 Approved by: baude --- libpod/container.go | 11 ++++- libpod/runtime_ctr.go | 28 ++++++++++++ libpod/stats.go | 120 ++++++++++++++++++++++++++++++++++++++++++++++++++ libpod/util.go | 20 +++++++++ libpod/util_test.go | 10 +++++ 5 files changed, 188 insertions(+), 1 deletion(-) create mode 100644 libpod/stats.go (limited to 'libpod') diff --git a/libpod/container.go b/libpod/container.go index d53a863c0..604b5fe10 100644 --- a/libpod/container.go +++ b/libpod/container.go @@ -11,6 +11,7 @@ import ( "syscall" "time" + "github.com/containerd/cgroups" "github.com/containers/storage" "github.com/containers/storage/pkg/archive" "github.com/docker/docker/daemon/caps" @@ -55,6 +56,9 @@ const ( artifactsDir = "artifacts" ) +// CGroupParent is the prefix to a cgroup path in libpod +var CGroupParent = "/libpod_parent" + // Container is a single OCI container type Container struct { config *ContainerConfig @@ -577,7 +581,7 @@ func (c *Container) Init() (err error) { // With the spec complete, do an OCI create // TODO set cgroup parent in a sane fashion - if err := c.runtime.ociRuntime.createContainer(c, "/libpod_parent"); err != nil { + if err := c.runtime.ociRuntime.createContainer(c, CGroupParent); err != nil { return err } @@ -1044,3 +1048,8 @@ func (c *Container) cleanupStorage() error { return c.save() } + +// CGroupPath returns a cgroups "path" for a given container. +func (c *Container) CGroupPath() cgroups.Path { + return cgroups.StaticPath(filepath.Join(CGroupParent, fmt.Sprintf("libpod-conmon-%s", c.ID()))) +} diff --git a/libpod/runtime_ctr.go b/libpod/runtime_ctr.go index f314b302f..320821b38 100644 --- a/libpod/runtime_ctr.go +++ b/libpod/runtime_ctr.go @@ -232,3 +232,31 @@ func (r *Runtime) GetContainers(filters ...ContainerFilter) ([]*Container, error return ctrsFiltered, nil } + +// GetAllContainers is a helper function for GetContainers +func (r *Runtime) GetAllContainers() ([]*Container, error) { + return r.state.AllContainers() +} + +// GetRunningContainers is a helper function for GetContainers +func (r *Runtime) GetRunningContainers() ([]*Container, error) { + running := func(c *Container) bool { + state, _ := c.State() + return state == ContainerStateRunning + } + return r.GetContainers(running) +} + +// GetContainersByList is a helper function for GetContainers +// which takes a []string of container IDs or names +func (r *Runtime) GetContainersByList(containers []string) ([]*Container, error) { + var ctrs []*Container + for _, inputContainer := range containers { + ctr, err := r.LookupContainer(inputContainer) + if err != nil { + return ctrs, errors.Wrapf(err, "unable to lookup container %s", inputContainer) + } + ctrs = append(ctrs, ctr) + } + return ctrs, nil +} diff --git a/libpod/stats.go b/libpod/stats.go new file mode 100644 index 000000000..86da0679e --- /dev/null +++ b/libpod/stats.go @@ -0,0 +1,120 @@ +package libpod + +import ( + "strings" + "syscall" + "time" + + "github.com/containerd/cgroups" + "github.com/opencontainers/runc/libcontainer" + "github.com/pkg/errors" +) + +// ContainerStats contains the statistics information for a running container +type ContainerStats struct { + ContainerID string + CPU float64 + CPUNano uint64 + SystemNano uint64 + MemUsage uint64 + MemLimit uint64 + MemPerc float64 + NetInput uint64 + NetOutput uint64 + BlockInput uint64 + BlockOutput uint64 + PIDs uint64 +} + +// GetContainerStats gets the running stats for a given container +func (c *Container) GetContainerStats(previousStats *ContainerStats) (*ContainerStats, error) { + stats := new(ContainerStats) + c.lock.Lock() + defer c.lock.Unlock() + if err := c.syncContainer(); err != nil { + return stats, errors.Wrapf(err, "error updating container %s state", c.ID()) + } + cgroup, err := cgroups.Load(cgroups.V1, c.CGroupPath()) + if err != nil { + return stats, errors.Wrapf(err, "unable to load cgroup at %+v", c.CGroupPath()) + } + + cgroupStats, err := cgroup.Stat() + if err != nil { + return stats, errors.Wrapf(err, "unable to obtain cgroup stats") + } + conState := c.state.State + if err != nil { + return stats, errors.Wrapf(err, "unable to determine container state") + } + + previousCPU := previousStats.CPUNano + previousSystem := previousStats.SystemNano + stats.ContainerID = c.ID() + stats.CPU = calculateCPUPercent(cgroupStats, previousCPU, previousSystem) + stats.MemUsage = cgroupStats.Memory.Usage.Usage + stats.MemLimit = getMemLimit(cgroupStats.Memory.Usage.Limit) + stats.MemPerc = (float64(stats.MemUsage) / float64(stats.MemLimit)) * 100 + stats.PIDs = 0 + if conState == ContainerStateRunning { + stats.PIDs = cgroupStats.Pids.Current - 1 + } + stats.BlockInput, stats.BlockOutput = calculateBlockIO(cgroupStats) + stats.CPUNano = cgroupStats.Cpu.Usage.Total + stats.SystemNano = cgroupStats.Cpu.Usage.Kernel + // TODO Figure out where to get the Netout stuff. + //stats.NetInput, stats.NetOutput = getContainerNetIO(cgroupStats) + return stats, nil +} + +// getMemory limit returns the memory limit for a given cgroup +// If the configured memory limit is larger than the total memory on the sys, the +// physical system memory size is returned +func getMemLimit(cgroupLimit uint64) uint64 { + si := &syscall.Sysinfo_t{} + err := syscall.Sysinfo(si) + if err != nil { + return cgroupLimit + } + + physicalLimit := uint64(si.Totalram) + if cgroupLimit > physicalLimit { + return physicalLimit + } + return cgroupLimit +} + +// Returns the total number of bytes transmitted and received for the given container stats +func getContainerNetIO(stats *libcontainer.Stats) (received uint64, transmitted uint64) { //nolint + for _, iface := range stats.Interfaces { + received += iface.RxBytes + transmitted += iface.TxBytes + } + return +} + +func calculateCPUPercent(stats *cgroups.Stats, previousCPU, previousSystem uint64) float64 { + var ( + cpuPercent = 0.0 + cpuDelta = float64(stats.Cpu.Usage.Total - previousCPU) + systemDelta = float64(uint64(time.Now().UnixNano()) - previousSystem) + ) + if systemDelta > 0.0 && cpuDelta > 0.0 { + // gets a ratio of container cpu usage total, multiplies it by the number of cores (4 cores running + // at 100% utilization should be 400% utilization), and multiplies that by 100 to get a percentage + cpuPercent = (cpuDelta / systemDelta) * float64(len(stats.Cpu.Usage.PerCpu)) * 100 + } + return cpuPercent +} + +func calculateBlockIO(stats *cgroups.Stats) (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 +} diff --git a/libpod/util.go b/libpod/util.go index 61089b525..de5c3ff31 100644 --- a/libpod/util.go +++ b/libpod/util.go @@ -9,6 +9,8 @@ import ( "github.com/containers/image/signature" "github.com/containers/image/types" + "github.com/pkg/errors" + "strconv" ) // Runtime API constants @@ -76,3 +78,21 @@ func GetPolicyContext(path string) (*signature.PolicyContext, error) { } return signature.NewPolicyContext(policy) } + +// RemoveScientificNotationFromFloat returns a float without any +// scientific notation if the number has any. +// golang does not handle conversion of float64s that have scientific +// notation in them and otherwise stinks. please replace this if you have +// a better implementation. +func RemoveScientificNotationFromFloat(x float64) (float64, error) { + bigNum := strconv.FormatFloat(x, 'g', -1, 64) + breakPoint := strings.IndexAny(bigNum, "Ee") + if breakPoint > 0 { + bigNum = bigNum[:breakPoint] + } + result, err := strconv.ParseFloat(bigNum, 64) + if err != nil { + return x, errors.Wrapf(err, "unable to remove scientific number from calculations") + } + return result, nil +} diff --git a/libpod/util_test.go b/libpod/util_test.go index 24e5fdfac..7b9d19a43 100644 --- a/libpod/util_test.go +++ b/libpod/util_test.go @@ -17,3 +17,13 @@ func TestStringInSlice(t *testing.T) { // string is not in empty slice assert.False(t, StringInSlice("one", []string{})) } + +func TestRemoveScientificNotationFromFloat(t *testing.T) { + numbers := []float64{0.0, .5, 1.99999932, 1.04e+10} + results := []float64{0.0, .5, 1.99999932, 1.04} + for i, x := range numbers { + result, err := RemoveScientificNotationFromFloat(x) + assert.NoError(t, err) + assert.Equal(t, result, results[i]) + } +} -- cgit v1.2.3-54-g00ecf