Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,10 @@ jobs:
sudo apt-get build-dep -y ruby-libvirt
sudo apt-get install -y --no-install-recommends libxslt-dev libxml2-dev libvirt-dev ruby-bundler ruby-dev zlib1g-dev

# Keep fog-json compatible with Vagrant's embedded json gem.
sudo env VAGRANT_DISABLE_STRICT_DEPENDENCY_ENFORCEMENT=1 vagrant plugin uninstall fog-json || true
sudo env VAGRANT_DISABLE_STRICT_DEPENDENCY_ENFORCEMENT=1 vagrant plugin install fog-json --plugin-version 1.2.0

if ! vagrant plugin list | grep -q vagrant-libvirt; then
sudo env VAGRANT_DISABLE_STRICT_DEPENDENCY_ENFORCEMENT=1 vagrant plugin install vagrant-libvirt
fi
Expand Down
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,22 @@ By adding `cgroups.IgnoreNotExist` all non-existent files will be ignored, e.g.
stats, err := control.Stat(cgroup1.IgnoreNotExist)
```

To collect only CPU and memory metrics, use `cgroup1.StatFiltered`. Include
`Cpu` for throttling statistics and `Cpuacct` for CPU usage. Other subsystems
are skipped entirely, including blkio's reads of `/proc/partitions`.

```go
stats, err := cgroup1.StatFiltered(control,
[]cgroup1.Name{cgroup1.Cpu, cgroup1.Cpuacct, cgroup1.Memory},
cgroup1.IgnoreNotExist,
)
```

An empty subsystem list collects all metrics, just like `Stat()`. Duplicate and
inactive subsystem names are ignored. The existing `Cgroup` interface is unchanged;
custom implementations must implement the optional `StatFiltered` method to use
this helper, otherwise it returns an error wrapping `errors.ErrUnsupported`.

### Move process across cgroups

This allows you to take processes from one cgroup and move them to another.
Expand Down
8 changes: 7 additions & 1 deletion cgroup1/cgroup.go
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,12 @@ func (c *cgroup) Delete() error {

// Stat returns the current metrics for the cgroup
func (c *cgroup) Stat(handlers ...ErrorHandler) (*v1.Metrics, error) {
return c.StatFiltered(nil, handlers...)
}

// StatFiltered returns metrics for the selected active subsystems.
// An empty list selects all subsystems, as in Stat.
func (c *cgroup) StatFiltered(subsystems []Name, handlers ...ErrorHandler) (*v1.Metrics, error) {
c.mu.Lock()
defer c.mu.Unlock()
if c.err != nil {
Expand All @@ -318,7 +324,7 @@ func (c *cgroup) Stat(handlers ...ErrorHandler) (*v1.Metrics, error) {
wg = &sync.WaitGroup{}
errs = make(chan error, len(c.subsystems))
)
for _, s := range c.subsystems {
for _, s := range c.subsystemsFilter(subsystems...) {
if ss, ok := s.(stater); ok {
sp, err := c.path(s.Name())
if err != nil {
Expand Down
1 change: 1 addition & 0 deletions cgroup1/control.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ type Cgroup interface {
// subsystems are moved one at a time
MoveTo(Cgroup) error
// Stat returns the stats for all subsystems in the cgroup
// Use StatFiltered to collect metrics for selected subsystems.
Stat(...ErrorHandler) (*v1.Metrics, error)
// Update updates all the subsystems with the provided resource changes
Update(resources *specs.LinuxResources) error
Expand Down
47 changes: 47 additions & 0 deletions cgroup1/stat.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
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 cgroup1

import (
"errors"
"fmt"

v1 "github.com/containerd/cgroups/v3/cgroup1/stats"
)

// StatFiltered returns metrics for the selected active subsystems without
// reading files or resolving paths for other subsystems. An empty list selects
// all subsystems, as in Cgroup.Stat. Duplicate and inactive names are ignored.
// Select both Cpu and Cpuacct to collect CPU throttling and usage statistics.
// The CPU, CPU.Usage and CPU.Throttling fields are always initialized, as in Stat.
//
// This function keeps the Cgroup interface compatible with existing implementations.
// Cgroups returned by New, Load and Cgroup.New support filtered stats. Other
// implementations can opt in by providing this method:
//
// StatFiltered([]Name, ...ErrorHandler) (*stats.Metrics, error)
//
// If c does not implement that method, StatFiltered returns an error wrapping
// errors.ErrUnsupported.
func StatFiltered(c Cgroup, subsystems []Name, handlers ...ErrorHandler) (*v1.Metrics, error) {
if filtered, ok := c.(interface {
StatFiltered([]Name, ...ErrorHandler) (*v1.Metrics, error)
}); ok {
return filtered.StatFiltered(subsystems, handlers...)
}
return nil, fmt.Errorf("cgroups: filtered stats: %w", errors.ErrUnsupported)
}
226 changes: 226 additions & 0 deletions cgroup1/stat_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
/*
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 cgroup1

import (
"errors"
"os"
"path/filepath"
"testing"

v1 "github.com/containerd/cgroups/v3/cgroup1/stats"
"github.com/opencontainers/runtime-spec/specs-go"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/proto"
)

// Use regular files so filtering and its I/O savings can be tested without
// requiring a mounted cgroup v1 hierarchy.
func newStatFixture(tb testing.TB) (Cgroup, string) {
tb.Helper()
root := tb.TempDir()
files := map[string]string{
"cpu/test/cpu.stat": "nr_periods 10\nnr_throttled 2\nthrottled_time 30\n",
"cpuacct/test/cpuacct.stat": "user 100\nsystem 50\n",
"cpuacct/test/cpuacct.usage": "1500000000\n",
"cpuacct/test/cpuacct.usage_percpu": "1000000000 500000000\n",
"memory/test/memory.stat": memoryData,
"memory/test/memory.oom_control": memoryOomControlData,
"pids/test/pids.current": "3\n",
"pids/test/pids.max": "10\n",
"proc/partitions": data,
"blkio/test/blkio.throttle.io_serviced": "259:0 Read 12\n259:0 Write 4\nTotal 16\n",
"blkio/test/blkio.throttle.io_service_bytes": "259:0 Read 4096\n259:0 Write 1024\nTotal 5120\n",
}
for _, module := range []string{"", "memsw.", "kmem.", "kmem.tcp."} {
for _, name := range []string{"usage_in_bytes", "max_usage_in_bytes", "failcnt", "limit_in_bytes"} {
files["memory/test/memory."+module+name] = "1024\n"
}
}
for name, content := range files {
path := filepath.Join(root, name)
require.NoError(tb, os.MkdirAll(filepath.Dir(path), 0o755))
require.NoError(tb, os.WriteFile(path, []byte(content), 0o644))
}
control, err := New(StaticPath("test"), &specs.LinuxResources{}, WithHierarchy(func() ([]Subsystem, error) {
return []Subsystem{
NewCpu(root), NewCpuacct(root), NewMemory(root), NewPids(root),
NewBlkio(root, ProcRoot(filepath.Join(root, "proc"))),
NewNamed(root, "custom"), // No Stat method.
}, nil
}))
require.NoError(tb, err)
return control, root
}

func TestStatFiltered(t *testing.T) {
control, _ := newStatFixture(t)
all, err := control.Stat()
require.NoError(t, err)
emptyCPU := &v1.CPUStat{Usage: &v1.CPUUsage{}, Throttling: &v1.Throttle{}}
tests := []struct {
name string
subsystems []Name
want *v1.Metrics
}{
{"all", nil, all},
{"empty", []Name{}, all},
{"cpu", []Name{Cpu}, &v1.Metrics{CPU: &v1.CPUStat{Usage: &v1.CPUUsage{}, Throttling: all.CPU.Throttling}}},
{"cpuacct", []Name{Cpuacct}, &v1.Metrics{CPU: &v1.CPUStat{Usage: all.CPU.Usage, Throttling: &v1.Throttle{}}}},
{"cpu-and-cpuacct", []Name{Cpu, Cpuacct}, &v1.Metrics{CPU: all.CPU}},
{"memory", []Name{Memory}, &v1.Metrics{CPU: emptyCPU, Memory: all.Memory, MemoryOomControl: all.MemoryOomControl}},
{"cpu-and-memory", []Name{Cpu, Cpuacct, Memory}, &v1.Metrics{CPU: all.CPU, Memory: all.Memory, MemoryOomControl: all.MemoryOomControl}},
{"pids", []Name{Pids}, &v1.Metrics{CPU: emptyCPU, Pids: all.Pids}},
{"blkio", []Name{Blkio}, &v1.Metrics{CPU: emptyCPU, Blkio: all.Blkio}},
{"inactive", []Name{"inactive"}, &v1.Metrics{CPU: emptyCPU}},
{"non-stater", []Name{"custom"}, &v1.Metrics{CPU: emptyCPU}},
{"duplicates", []Name{Cpu, Cpu, Cpuacct, Cpuacct, "inactive"}, &v1.Metrics{CPU: all.CPU}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := StatFiltered(control, tt.subsystems)
require.NoError(t, err)
require.True(t, proto.Equal(tt.want, got), "want %v, got %v", tt.want, got)
})
}
// Filtering must not change subsequent unfiltered calls.
got, err := control.Stat()
require.NoError(t, err)
require.True(t, proto.Equal(all, got))
}

func TestStatFilteredSkipsBlkio(t *testing.T) {
control, root := newStatFixture(t)
require.NoError(t, os.Remove(filepath.Join(root, "proc", "partitions")))

got, err := StatFiltered(control, []Name{Cpu, Cpuacct, Memory})
require.NoError(t, err)
require.Equal(t, uint64(1500000000), got.CPU.Usage.Total)
require.Equal(t, uint64(10), got.CPU.Throttling.Periods)
require.Equal(t, uint64(1024), got.Memory.Usage.Usage)
require.Nil(t, got.Blkio)

_, err = StatFiltered(control, []Name{Blkio})
require.ErrorIs(t, err, os.ErrNotExist)
_, err = control.Stat()
require.ErrorIs(t, err, os.ErrNotExist)
}

func TestStatFilteredPaths(t *testing.T) {
control, _ := newStatFixture(t)
c := control.(*cgroup)
pathErr := errors.New("unexpected blkio path lookup")
calls := make(map[Name]int)
c.path = func(name Name) (string, error) {
calls[name]++
if name == Blkio {
return "", pathErr
}
return "test", nil
}
_, err := StatFiltered(control, []Name{Cpu, Cpu, Cpuacct})
require.NoError(t, err)
require.Equal(t, map[Name]int{Cpu: 1, Cpuacct: 1}, calls)
_, err = StatFiltered(control, []Name{Blkio})
require.ErrorIs(t, err, pathErr)
}

func TestStatFilteredErrors(t *testing.T) {
control, root := newStatFixture(t)
require.NoError(t, os.Remove(filepath.Join(root, "pids", "test", "pids.current")))
_, err := StatFiltered(control, []Name{Pids})
require.ErrorIs(t, err, os.ErrNotExist)
_, err = StatFiltered(control, []Name{Pids}, IgnoreNotExist)
require.NoError(t, err)
handlerErr := errors.New("handled stat error")
_, err = StatFiltered(control, []Name{Pids}, func(err error) error {
if errors.Is(err, os.ErrNotExist) {
return handlerErr
}
return err
})
require.ErrorIs(t, err, handlerErr)
}

func TestStatFilteredLoadAndChild(t *testing.T) {
control, _ := newStatFixture(t)
loaded, err := Load(StaticPath("test"), WithHierarchy(func() ([]Subsystem, error) {
return control.Subsystems(), nil
}))
require.NoError(t, err)
got, err := StatFiltered(loaded, []Name{Pids})
require.NoError(t, err)
require.Equal(t, uint64(3), got.Pids.Current)

child, err := loaded.New("child", &specs.LinuxResources{})
require.NoError(t, err)
_, err = StatFiltered(child, []Name{Pids}, IgnoreNotExist)
require.NoError(t, err)
require.NoError(t, child.Delete())
_, err = StatFiltered(child, []Name{Pids}, IgnoreNotExist)
require.ErrorIs(t, err, ErrCgroupDeleted)
}

// An implementation of the original interface need not support filtered stats.
type unfilteredCgroup struct{ Cgroup }

type customFilteredCgroup struct {
Cgroup
stat func([]Name, ...ErrorHandler) (*v1.Metrics, error)
}

func (c customFilteredCgroup) StatFiltered(names []Name, handlers ...ErrorHandler) (*v1.Metrics, error) {
return c.stat(names, handlers...)
}

func TestStatFilteredCustomCgroup(t *testing.T) {
_, err := StatFiltered(unfilteredCgroup{}, []Name{Memory})
require.ErrorIs(t, err, errors.ErrUnsupported)

want := &v1.Metrics{Pids: &v1.PidsStat{Current: 42}}
c := customFilteredCgroup{stat: func(names []Name, handlers ...ErrorHandler) (*v1.Metrics, error) {
require.Equal(t, []Name{Pids}, names)
require.Len(t, handlers, 1)
require.NoError(t, handlers[0](os.ErrNotExist))
return want, nil
}}
got, err := StatFiltered(c, []Name{Pids}, IgnoreNotExist)
require.NoError(t, err)
require.Same(t, want, got)
}

func BenchmarkStatFiltered(b *testing.B) {
control, _ := newStatFixture(b)
for _, tt := range []struct {
name string
subsystems []Name
}{
{"All", nil},
{"CPU", []Name{Cpu, Cpuacct}},
{"Memory", []Name{Memory}},
{"CPUAndMemory", []Name{Cpu, Cpuacct, Memory}},
} {
b.Run(tt.name, func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
if _, err := StatFiltered(control, tt.subsystems); err != nil {
b.Fatal(err)
}
}
})
}
}
Loading