mirror of
https://github.com/netbirdio/gvisor.git
synced 2026-05-22 17:12:49 -07:00
Merge pull request #6821 from dqminh:feature/cgroupv2
PiperOrigin-RevId: 413032543
This commit is contained in:
@@ -1,14 +1,16 @@
|
||||
# Install packages we need. Docker must be installed and configured,
|
||||
# as should Go itself. We just install some extra bits and pieces.
|
||||
function install_pkgs() {
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
while true; do
|
||||
if sudo apt-get update && sudo apt-get install -y "$@"; then
|
||||
if sudo -E apt-get update && sudo -E apt-get install -y "$@"; then
|
||||
break
|
||||
fi
|
||||
done
|
||||
}
|
||||
install_pkgs make linux-libc-dev graphviz jq curl binutils gnupg gnupg-agent \
|
||||
gcc pkg-config apt-transport-https ca-certificates software-properties-common
|
||||
gcc pkg-config apt-transport-https ca-certificates software-properties-common \
|
||||
jq
|
||||
|
||||
# Install headers, only if available.
|
||||
if test -n "$(apt-cache search --names-only "^linux-headers-$(uname -r)$")"; then
|
||||
@@ -22,8 +24,9 @@ export TOTAL_PARTITIONS=${BUILDKITE_PARALLEL_JOB_COUNT:-1}
|
||||
|
||||
# Ensure Docker has experimental enabled.
|
||||
EXPERIMENTAL=$(sudo docker version --format='{{.Server.Experimental}}')
|
||||
if test "${EXPERIMENTAL}" != "true"; then
|
||||
make sudo TARGETS=//runsc:runsc ARGS="install --experimental=true"
|
||||
CGDRIVER=$(sudo docker info --format='{{.CgroupDriver}}')
|
||||
if test "${EXPERIMENTAL}" != "true" || test "${CGDRIVER}" != "cgroupfs"; then
|
||||
make sudo TARGETS=//runsc:runsc ARGS="install --experimental=true --cgroupdriver=cgroupfs"
|
||||
sudo systemctl restart docker
|
||||
fi
|
||||
|
||||
|
||||
@@ -111,9 +111,19 @@ steps:
|
||||
- <<: *common
|
||||
label: ":test_tube: Unit tests"
|
||||
command: make unit-tests
|
||||
- <<: *common
|
||||
label: ":test_tube: Unit tests (cgroupv2)"
|
||||
command: make unit-tests
|
||||
agents:
|
||||
queue: "cgroupv2"
|
||||
- <<: *common
|
||||
label: ":test_tube: Container tests"
|
||||
command: make container-tests
|
||||
- <<: *common
|
||||
label: ":test_tube: Container tests (cgroupv2)"
|
||||
command: make container-tests
|
||||
agents:
|
||||
queue: "cgroupv2"
|
||||
|
||||
# All system call tests.
|
||||
- <<: *common
|
||||
@@ -125,6 +135,11 @@ steps:
|
||||
- <<: *common
|
||||
label: ":docker: Docker tests"
|
||||
command: make docker-tests
|
||||
- <<: *common
|
||||
label: ":docker: Docker tests (cgroupv2)"
|
||||
command: make docker-tests
|
||||
agents:
|
||||
queue: "cgroupv2"
|
||||
- <<: *common
|
||||
label: ":goggles: Overlay tests"
|
||||
command: make overlay-tests
|
||||
@@ -152,6 +167,11 @@ steps:
|
||||
- <<: *common
|
||||
label: ":docker: Containerd 1.5.4 tests"
|
||||
command: make containerd-test-1.5.4
|
||||
- <<: *common
|
||||
label: ":docker: Containerd 1.5.4 tests (cgroupv2)"
|
||||
command: make containerd-test-1.5.4
|
||||
agents:
|
||||
queue: "cgroupv2"
|
||||
|
||||
# Check the website builds.
|
||||
- <<: *common
|
||||
|
||||
@@ -113,6 +113,12 @@ RUNTIME_BIN := $(RUNTIME_DIR)/runsc
|
||||
RUNTIME_LOG_DIR := $(RUNTIME_DIR)/logs
|
||||
RUNTIME_LOGS := $(RUNTIME_LOG_DIR)/runsc.log.%TEST%.%TIMESTAMP%.%COMMAND%
|
||||
|
||||
ifeq ($(shell stat -f -c "%T" /sys/fs/cgroup 2>/dev/null),cgroup2fs)
|
||||
CGROUPV2 := true
|
||||
else
|
||||
CGROUPV2 := false
|
||||
endif
|
||||
|
||||
$(RUNTIME_BIN): # See below.
|
||||
@mkdir -p "$(RUNTIME_DIR)"
|
||||
ifeq (,$(STAGED_BINARIES))
|
||||
@@ -204,7 +210,7 @@ tests: unit-tests nogo-tests container-tests syscall-tests
|
||||
|
||||
integration-tests: ## Run all standard integration tests.
|
||||
integration-tests: docker-tests overlay-tests hostnet-tests swgso-tests
|
||||
integration-tests: do-tests kvm-tests containerd-test-1.3.9
|
||||
integration-tests: do-tests kvm-tests containerd-tests-min
|
||||
.PHONY: integration-tests
|
||||
|
||||
network-tests: ## Run all networking integration tests.
|
||||
@@ -320,10 +326,19 @@ else
|
||||
endif
|
||||
@$(call sudo,test/root:root_test,--runtime=$(RUNTIME) -test.v)
|
||||
|
||||
ifeq ($(CGROUPV2),false)
|
||||
containerd-tests-min: containerd-test-1.3.9
|
||||
else
|
||||
containerd-tests-min: containerd-test-1.4.3
|
||||
endif
|
||||
|
||||
# The shim builds with containerd 1.3.9 and it's not backward compatible. Test
|
||||
# with 1.3.9 and newer versions.
|
||||
# When run under cgroupv2 environment, skip 1.3.9 as it does not support cgroupv2
|
||||
containerd-tests: ## Runs all supported containerd version tests.
|
||||
ifeq ($(CGROUPV2),false)
|
||||
containerd-tests: containerd-test-1.3.9
|
||||
endif
|
||||
containerd-tests: containerd-test-1.4.3
|
||||
containerd-tests: containerd-test-1.5.4
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ go_library(
|
||||
"api.go",
|
||||
"debug.go",
|
||||
"epoll.go",
|
||||
"oom_v2.go",
|
||||
"options.go",
|
||||
"service.go",
|
||||
"service_linux.go",
|
||||
@@ -25,6 +26,7 @@ go_library(
|
||||
"@com_github_burntsushi_toml//:go_default_library",
|
||||
"@com_github_containerd_cgroups//:go_default_library",
|
||||
"@com_github_containerd_cgroups//stats/v1:go_default_library",
|
||||
"@com_github_containerd_cgroups//v2:go_default_library",
|
||||
"@com_github_containerd_console//:go_default_library",
|
||||
"@com_github_containerd_containerd//api/events:go_default_library",
|
||||
"@com_github_containerd_containerd//api/types/task:go_default_library",
|
||||
|
||||
+5
-1
@@ -81,9 +81,13 @@ func (e *epoller) run(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
func (e *epoller) add(id string, cg cgroups.Cgroup) error {
|
||||
func (e *epoller) add(id string, cgx interface{}) error {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
cg, ok := cgx.(cgroups.Cgroup)
|
||||
if !ok {
|
||||
return fmt.Errorf("expected cgroups.Cgroup, got: %T", cgx)
|
||||
}
|
||||
fd, err := cg.OOMEventFD()
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
// Copyright The containerd Authors.
|
||||
// Copyright 2021 The gVisor 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
|
||||
//
|
||||
// https://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.
|
||||
|
||||
//go:build linux
|
||||
// +build linux
|
||||
|
||||
package shim
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
cgroupsv2 "github.com/containerd/cgroups/v2"
|
||||
"github.com/containerd/containerd/runtime"
|
||||
"github.com/containerd/containerd/runtime/v2/shim"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// newOOMv2Epoller returns an implementation that listens to OOM events
|
||||
// from a container's cgroups v2. This is copied from containerd to avoid
|
||||
// having to upgrade containerd package just to get it
|
||||
func newOOMv2Poller(publisher shim.Publisher) (oomPoller, error) {
|
||||
return &watcherV2{
|
||||
itemCh: make(chan itemV2),
|
||||
publisher: publisher,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// watcher implementation for handling OOM events from a container's cgroup
|
||||
type watcherV2 struct {
|
||||
itemCh chan itemV2
|
||||
publisher shim.Publisher
|
||||
}
|
||||
|
||||
type itemV2 struct {
|
||||
id string
|
||||
ev cgroupsv2.Event
|
||||
err error
|
||||
}
|
||||
|
||||
// Close closes the watcher
|
||||
func (w *watcherV2) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Run the loop
|
||||
func (w *watcherV2) run(ctx context.Context) {
|
||||
lastOOMMap := make(map[string]uint64) // key: id, value: ev.OOM
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
w.Close()
|
||||
return
|
||||
case i := <-w.itemCh:
|
||||
if i.err != nil {
|
||||
delete(lastOOMMap, i.id)
|
||||
continue
|
||||
}
|
||||
lastOOM := lastOOMMap[i.id]
|
||||
if i.ev.OOM > lastOOM {
|
||||
if err := w.publisher.Publish(ctx, runtime.TaskOOMEventTopic, &TaskOOM{
|
||||
ContainerID: i.id,
|
||||
}); err != nil {
|
||||
logrus.WithError(err).Error("publish OOM event")
|
||||
}
|
||||
}
|
||||
if i.ev.OOM > 0 {
|
||||
lastOOMMap[i.id] = i.ev.OOM
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add cgroups.Cgroup to the epoll monitor
|
||||
func (w *watcherV2) add(id string, cgx interface{}) error {
|
||||
cg, ok := cgx.(*cgroupsv2.Manager)
|
||||
if !ok {
|
||||
return fmt.Errorf("expected *cgroupsv2.Manager, got: %T", cgx)
|
||||
}
|
||||
// NOTE: containerd/cgroups/v2 does not support closing eventCh routine currently.
|
||||
// The routine shuts down when an error happens, mostly when the cgroup is deleted.
|
||||
eventCh, errCh := cg.EventChan()
|
||||
go func() {
|
||||
for {
|
||||
i := itemV2{id: id}
|
||||
select {
|
||||
case ev := <-eventCh:
|
||||
i.ev = ev
|
||||
w.itemCh <- i
|
||||
case err := <-errCh:
|
||||
i.err = err
|
||||
w.itemCh <- i
|
||||
// we no longer get any event/err when we got an err
|
||||
logrus.WithError(err).Warn("error from eventChan")
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
+33
-3
@@ -29,6 +29,7 @@ import (
|
||||
"github.com/BurntSushi/toml"
|
||||
"github.com/containerd/cgroups"
|
||||
cgroupsstats "github.com/containerd/cgroups/stats/v1"
|
||||
cgroupsv2 "github.com/containerd/cgroups/v2"
|
||||
"github.com/containerd/console"
|
||||
"github.com/containerd/containerd/api/events"
|
||||
"github.com/containerd/containerd/api/types/task"
|
||||
@@ -82,6 +83,15 @@ const (
|
||||
cgroupParentAnnotation = "dev.gvisor.spec.cgroup-parent"
|
||||
)
|
||||
|
||||
type oomPoller interface {
|
||||
io.Closer
|
||||
// add adds `cg` cgroup to oom poller. `cg` is cgroups.Cgroup in v1 and
|
||||
// `cgroupsv2.Manager` in v2
|
||||
add(id string, cg interface{}) error
|
||||
// run monitors oom event and notifies the shim about them
|
||||
run(ctx context.Context)
|
||||
}
|
||||
|
||||
// New returns a new shim service that can be used via GRPC.
|
||||
func New(ctx context.Context, id string, publisher shim.Publisher, cancel func()) (shim.Shim, error) {
|
||||
var opts shim.Opts
|
||||
@@ -89,7 +99,15 @@ func New(ctx context.Context, id string, publisher shim.Publisher, cancel func()
|
||||
opts = ctxOpts.(shim.Opts)
|
||||
}
|
||||
|
||||
ep, err := newOOMEpoller(publisher)
|
||||
var (
|
||||
ep oomPoller
|
||||
err error
|
||||
)
|
||||
if cgroups.Mode() == cgroups.Unified {
|
||||
ep, err = newOOMv2Poller(publisher)
|
||||
} else {
|
||||
ep, err = newOOMEpoller(publisher)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -161,7 +179,7 @@ type service struct {
|
||||
ec chan proc.Exit
|
||||
|
||||
// oomPoller monitors the sandbox's cgroup for OOM notifications.
|
||||
oomPoller *epoller
|
||||
oomPoller oomPoller
|
||||
|
||||
// cancel is a function that needs to be called before the shim stops. The
|
||||
// function is provided by the caller to New().
|
||||
@@ -473,7 +491,19 @@ func (s *service) Create(ctx context.Context, r *taskAPI.CreateTaskRequest) (*ta
|
||||
// sandbox create since the sandbox process will be created here.
|
||||
pid := process.Pid()
|
||||
if pid > 0 {
|
||||
cg, err := cgroups.Load(cgroups.V1, cgroups.PidPath(pid))
|
||||
var (
|
||||
cg interface{}
|
||||
err error
|
||||
)
|
||||
if cgroups.Mode() == cgroups.Unified {
|
||||
var cgPath string
|
||||
cgPath, err = cgroupsv2.PidGroupPath(pid)
|
||||
if err == nil {
|
||||
cg, err = cgroupsv2.LoadManager("/sys/fs/cgroup", cgPath)
|
||||
}
|
||||
} else {
|
||||
cg, err = cgroups.Load(cgroups.V1, cgroups.PidPath(pid))
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("loading cgroup for %d: %w", pid, err)
|
||||
}
|
||||
|
||||
+8
-2
@@ -4,7 +4,10 @@ package(licenses = ["notice"])
|
||||
|
||||
go_library(
|
||||
name = "cgroup",
|
||||
srcs = ["cgroup.go"],
|
||||
srcs = [
|
||||
"cgroup.go",
|
||||
"cgroup_v2.go",
|
||||
],
|
||||
visibility = ["//:sandbox"],
|
||||
deps = [
|
||||
"//pkg/cleanup",
|
||||
@@ -19,7 +22,10 @@ go_library(
|
||||
go_test(
|
||||
name = "cgroup_test",
|
||||
size = "small",
|
||||
srcs = ["cgroup_test.go"],
|
||||
srcs = [
|
||||
"cgroup_test.go",
|
||||
"cgroup_v2_test.go",
|
||||
],
|
||||
library = ":cgroup",
|
||||
tags = ["local"],
|
||||
deps = [
|
||||
|
||||
+74
-25
@@ -217,10 +217,10 @@ func loadPaths(pid string) (map[string]string, error) {
|
||||
}
|
||||
defer mountinfo.Close()
|
||||
|
||||
return loadPathsHelper(procCgroup, mountinfo)
|
||||
return loadPathsHelper(procCgroup, mountinfo, IsOnlyV2())
|
||||
}
|
||||
|
||||
func loadPathsHelper(cgroup, mountinfo io.Reader) (map[string]string, error) {
|
||||
func loadPathsHelper(cgroup, mountinfo io.Reader, unified bool) (map[string]string, error) {
|
||||
paths := make(map[string]string)
|
||||
|
||||
scanner := bufio.NewScanner(cgroup)
|
||||
@@ -231,6 +231,10 @@ func loadPathsHelper(cgroup, mountinfo io.Reader) (map[string]string, error) {
|
||||
if len(tokens) != 3 {
|
||||
return nil, fmt.Errorf("invalid cgroups file, line: %q", scanner.Text())
|
||||
}
|
||||
if len(tokens[1]) == 0 && unified {
|
||||
paths[cgroup2Key] = tokens[2]
|
||||
continue
|
||||
}
|
||||
if len(tokens[1]) == 0 {
|
||||
continue
|
||||
}
|
||||
@@ -255,30 +259,42 @@ func loadPathsHelper(cgroup, mountinfo io.Reader) (map[string]string, error) {
|
||||
// Format: ID parent major:minor root mount-point options opt-fields - fs-type source super-options
|
||||
// Example: 39 32 0:34 / /sys/fs/cgroup/devices rw,noexec shared:18 - cgroup cgroup rw,devices
|
||||
fields := strings.Fields(mountScanner.Text())
|
||||
if len(fields) < 9 || fields[len(fields)-3] != "cgroup" {
|
||||
if len(fields) < 9 {
|
||||
// Skip mounts that are not cgroup mounts.
|
||||
continue
|
||||
}
|
||||
// Cgroup controller type is in the super-options field.
|
||||
superOptions := strings.Split(fields[len(fields)-1], ",")
|
||||
for _, opt := range superOptions {
|
||||
// Remove prefix for cgroups with no controller, eg. systemd.
|
||||
opt = strings.TrimPrefix(opt, "name=")
|
||||
switch fields[len(fields)-3] {
|
||||
case "cgroup":
|
||||
// Cgroup controller type is in the super-options field.
|
||||
superOptions := strings.Split(fields[len(fields)-1], ",")
|
||||
for _, opt := range superOptions {
|
||||
// Remove prefix for cgroups with no controller, eg. systemd.
|
||||
opt = strings.TrimPrefix(opt, "name=")
|
||||
|
||||
// Only considers cgroup controllers that are registered, and skip other
|
||||
// irrelevant options, e.g. rw.
|
||||
if cgroupPath, ok := paths[opt]; ok {
|
||||
rootDir := fields[3]
|
||||
if rootDir != "/" {
|
||||
// When cgroup is in submount, remove repeated path components from
|
||||
// cgroup path to avoid duplicating them.
|
||||
relCgroupPath, err := filepath.Rel(rootDir, cgroupPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
// Only considers cgroup controllers that are registered, and skip other
|
||||
// irrelevant options, e.g. rw.
|
||||
if cgroupPath, ok := paths[opt]; ok {
|
||||
rootDir := fields[3]
|
||||
if rootDir != "/" {
|
||||
// When cgroup is in submount, remove repeated path components from
|
||||
// cgroup path to avoid duplicating them.
|
||||
relCgroupPath, err := filepath.Rel(rootDir, cgroupPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
paths[opt] = relCgroupPath
|
||||
}
|
||||
paths[opt] = relCgroupPath
|
||||
}
|
||||
}
|
||||
case "cgroup2":
|
||||
if cgroupPath, ok := paths[cgroup2Key]; ok {
|
||||
root := fields[3]
|
||||
relCgroupPath, err := filepath.Rel(root, cgroupPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
paths[cgroup2Key] = relCgroupPath
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := mountScanner.Err(); err != nil {
|
||||
@@ -335,21 +351,38 @@ func NewFromPid(pid int) (Cgroup, error) {
|
||||
}
|
||||
|
||||
func new(pid, cgroupsPath string) (Cgroup, error) {
|
||||
var parents map[string]string
|
||||
var (
|
||||
parents map[string]string
|
||||
err error
|
||||
cg Cgroup
|
||||
)
|
||||
|
||||
// If path is relative, load cgroup paths for the process to build the
|
||||
// relative paths.
|
||||
if !filepath.IsAbs(cgroupsPath) {
|
||||
var err error
|
||||
parents, err = loadPaths(pid)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("finding current cgroups: %w", err)
|
||||
}
|
||||
}
|
||||
cg := &cgroupV1{
|
||||
Name: cgroupsPath,
|
||||
Parents: parents,
|
||||
Own: make(map[string]bool),
|
||||
|
||||
if IsOnlyV2() {
|
||||
if p, ok := parents[cgroup2Key]; ok {
|
||||
// The cgroup of current pid will have tasks in it and we can't use
|
||||
// that, instead, use the its parent which should not have tasks in it.
|
||||
cgroupsPath = filepath.Join(filepath.Dir(p), cgroupsPath)
|
||||
}
|
||||
// Assume that for v2, cgroup is always mounted at cgroupRoot.
|
||||
cg, err = newCgroupV2(cgroupRoot, cgroupsPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
cg = &cgroupV1{
|
||||
Name: cgroupsPath,
|
||||
Parents: parents,
|
||||
Own: make(map[string]bool),
|
||||
}
|
||||
}
|
||||
log.Debugf("New cgroup for pid: %s, %+v", pid, cg)
|
||||
return cg, nil
|
||||
@@ -364,8 +397,20 @@ type cgroupJSONv1 struct {
|
||||
Cgroup *cgroupV1 `json:"cgroup"`
|
||||
}
|
||||
|
||||
type cgroupJSONv2 struct {
|
||||
Cgroup *cgroupV2 `json:"cgroup"`
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements json.Unmarshaler.UnmarshalJSON
|
||||
func (c *CgroupJSON) UnmarshalJSON(data []byte) error {
|
||||
if IsOnlyV2() {
|
||||
v2 := cgroupJSONv2{}
|
||||
err := json.Unmarshal(data, &v2)
|
||||
if v2.Cgroup != nil {
|
||||
c.Cgroup = v2.Cgroup
|
||||
}
|
||||
return err
|
||||
}
|
||||
v1 := cgroupJSONv1{}
|
||||
err := json.Unmarshal(data, &v1)
|
||||
if v1.Cgroup != nil {
|
||||
@@ -380,6 +425,10 @@ func (c *CgroupJSON) MarshalJSON() ([]byte, error) {
|
||||
v1 := cgroupJSONv1{}
|
||||
return json.Marshal(&v1)
|
||||
}
|
||||
if IsOnlyV2() {
|
||||
v2 := cgroupJSONv2{Cgroup: c.Cgroup.(*cgroupV2)}
|
||||
return json.Marshal(&v2)
|
||||
}
|
||||
v1 := cgroupJSONv1{Cgroup: c.Cgroup.(*cgroupV1)}
|
||||
return json.Marshal(&v1)
|
||||
}
|
||||
|
||||
@@ -831,7 +831,7 @@ func TestLoadPaths(t *testing.T) {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
r := strings.NewReader(tc.cgroups)
|
||||
mountinfo := strings.NewReader(tc.mountinfo)
|
||||
got, err := loadPathsHelper(r, mountinfo)
|
||||
got, err := loadPathsHelper(r, mountinfo, false)
|
||||
if len(tc.err) == 0 {
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,242 @@
|
||||
// Copyright The runc Authors.
|
||||
// Copyright 2021 The gVisor 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
|
||||
//
|
||||
// https://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 cgroup
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var cgroupv2MountInfo = `29 22 0:26 / /sys/fs/cgroup rw shared:4 - cgroup2 cgroup2 rw,seclabel,nsdelegate`
|
||||
|
||||
func TestLoadPathsCgroupv2(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
cgroups string
|
||||
mountinfo string
|
||||
want map[string]string
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "cgroupv2",
|
||||
cgroups: "0::/docker/123",
|
||||
mountinfo: cgroupv2MountInfo,
|
||||
want: map[string]string{
|
||||
"cgroup2": "docker/123",
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: "cgroupv2-nested",
|
||||
cgroups: "0::/",
|
||||
mountinfo: cgroupv2MountInfo,
|
||||
want: map[string]string{
|
||||
"cgroup2": ".",
|
||||
},
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
r := strings.NewReader(tc.cgroups)
|
||||
mountinfo := strings.NewReader(tc.mountinfo)
|
||||
got, err := loadPathsHelper(r, mountinfo, true)
|
||||
if len(tc.err) == 0 {
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
} else if !strings.Contains(err.Error(), tc.err) {
|
||||
t.Fatalf("Wrong error message, want: *%s*, got: %v", tc.err, err)
|
||||
}
|
||||
for key, vWant := range tc.want {
|
||||
vGot, ok := got[key]
|
||||
if !ok {
|
||||
t.Errorf("Missing controller %q", key)
|
||||
}
|
||||
if vWant != vGot {
|
||||
t.Errorf("Wrong controller %q value, want: %q, got: %q", key, vWant, vGot)
|
||||
}
|
||||
delete(got, key)
|
||||
}
|
||||
for k, v := range got {
|
||||
t.Errorf("Unexpected controller %q: %q", k, v)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNumToStr(t *testing.T) {
|
||||
cases := map[int64]string{
|
||||
0: "",
|
||||
-1: "max",
|
||||
10: "10",
|
||||
}
|
||||
for i, expected := range cases {
|
||||
got := numToStr(i)
|
||||
if got != expected {
|
||||
t.Errorf("expected numToStr(%d) to be %q, got %q", i, expected, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertBlkIOToIOWeightValue(t *testing.T) {
|
||||
cases := map[uint16]uint64{
|
||||
0: 0,
|
||||
10: 1,
|
||||
1000: 10000,
|
||||
}
|
||||
for i, expected := range cases {
|
||||
got := convertBlkIOToIOWeightValue(i)
|
||||
if got != expected {
|
||||
t.Errorf("expected ConvertBlkIOToIOWeightValue(%d) to be %d, got %d", i, expected, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertCPUSharesToCgroupV2Value(t *testing.T) {
|
||||
cases := map[uint64]uint64{
|
||||
0: 0,
|
||||
2: 1,
|
||||
262144: 10000,
|
||||
}
|
||||
for i, expected := range cases {
|
||||
got := convertCPUSharesToCgroupV2Value(i)
|
||||
if got != expected {
|
||||
t.Errorf("expected ConvertCPUSharesToCgroupV2Value(%d) to be %d, got %d", i, expected, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertMemorySwapToCgroupV2Value(t *testing.T) {
|
||||
cases := []struct {
|
||||
memswap, memory int64
|
||||
expected int64
|
||||
expErr bool
|
||||
}{
|
||||
{
|
||||
memswap: 0,
|
||||
memory: 0,
|
||||
expected: 0,
|
||||
},
|
||||
{
|
||||
memswap: -1,
|
||||
memory: 0,
|
||||
expected: -1,
|
||||
},
|
||||
{
|
||||
memswap: -1,
|
||||
memory: -1,
|
||||
expected: -1,
|
||||
},
|
||||
{
|
||||
memswap: -2,
|
||||
memory: 0,
|
||||
expErr: true,
|
||||
},
|
||||
{
|
||||
memswap: -1,
|
||||
memory: 1000,
|
||||
expected: -1,
|
||||
},
|
||||
{
|
||||
memswap: 1000,
|
||||
memory: 1000,
|
||||
expected: 0,
|
||||
},
|
||||
{
|
||||
memswap: 500,
|
||||
memory: 200,
|
||||
expected: 300,
|
||||
},
|
||||
{
|
||||
memswap: 300,
|
||||
memory: 400,
|
||||
expErr: true,
|
||||
},
|
||||
{
|
||||
memswap: 300,
|
||||
memory: 0,
|
||||
expErr: true,
|
||||
},
|
||||
{
|
||||
memswap: 300,
|
||||
memory: -300,
|
||||
expErr: true,
|
||||
},
|
||||
{
|
||||
memswap: 300,
|
||||
memory: -1,
|
||||
expErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
swap, err := convertMemorySwapToCgroupV2Value(c.memswap, c.memory)
|
||||
if c.expErr {
|
||||
if err == nil {
|
||||
t.Errorf("memswap: %d, memory %d, expected error, got %d, nil", c.memswap, c.memory, swap)
|
||||
}
|
||||
// no more checks
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("memswap: %d, memory %d, expected success, got error %s", c.memswap, c.memory, err)
|
||||
}
|
||||
if swap != c.expected {
|
||||
t.Errorf("memswap: %d, memory %d, expected %d, got %d", c.memswap, c.memory, c.expected, swap)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseCPUQuota(t *testing.T) {
|
||||
cases := []struct {
|
||||
quota string
|
||||
expected float64
|
||||
expErr bool
|
||||
}{
|
||||
{
|
||||
quota: "max 100000\n",
|
||||
expected: -1,
|
||||
},
|
||||
{
|
||||
quota: "10000 100000",
|
||||
expected: 0.1,
|
||||
},
|
||||
{
|
||||
quota: "20000 100000\n",
|
||||
expected: 0.2,
|
||||
},
|
||||
|
||||
{
|
||||
quota: "-1",
|
||||
expected: -1,
|
||||
expErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
res, err := parseCPUQuota(c.quota)
|
||||
if c.expErr {
|
||||
if err == nil {
|
||||
t.Errorf("quota: %q, expected error, got %.2f, nil", c.quota, res)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("quota: %q, expected success, got error %s", c.quota, err)
|
||||
}
|
||||
if res != c.expected {
|
||||
t.Errorf("quota: %q, expected %.2f, got error %.2f", c.quota, c.expected, res)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,7 @@ type Install struct {
|
||||
ConfigFile string
|
||||
Runtime string
|
||||
Experimental bool
|
||||
CgroupDriver string
|
||||
}
|
||||
|
||||
// Name implements subcommands.Command.Name.
|
||||
@@ -55,6 +56,7 @@ func (i *Install) SetFlags(fs *flag.FlagSet) {
|
||||
fs.StringVar(&i.ConfigFile, "config_file", "/etc/docker/daemon.json", "path to Docker daemon config file")
|
||||
fs.StringVar(&i.Runtime, "runtime", "runsc", "runtime name")
|
||||
fs.BoolVar(&i.Experimental, "experimental", false, "enable experimental features")
|
||||
fs.StringVar(&i.CgroupDriver, "cgroupdriver", "", "docker cgroup driver")
|
||||
}
|
||||
|
||||
// Execute implements subcommands.Command.Execute.
|
||||
@@ -95,6 +97,16 @@ func (i *Install) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{})
|
||||
c["experimental"] = true
|
||||
}
|
||||
|
||||
if i.CgroupDriver != "" {
|
||||
v, ok := c["exec-opts"]
|
||||
if ok {
|
||||
opts := v.([]interface{})
|
||||
c["exec-opts"] = append(opts, fmt.Sprintf("native.cgroupdriver=%s", i.CgroupDriver))
|
||||
} else {
|
||||
c["exec-opts"] = []string{fmt.Sprintf("native.cgroupdriver=%s", i.CgroupDriver)}
|
||||
}
|
||||
}
|
||||
|
||||
// Write out the runtime.
|
||||
if err := writeConfig(c, i.ConfigFile); err != nil {
|
||||
log.Fatalf("Error writing config file %q: %v", i.ConfigFile, err)
|
||||
|
||||
@@ -1277,13 +1277,6 @@ func (c *Container) setupCgroupForSubcontainer(conf *config.Config, spec *specs.
|
||||
// error is suppressed and a nil cgroups instance is returned to indicate that
|
||||
// no cgroups was configured.
|
||||
func cgroupInstall(conf *config.Config, cg cgroup.Cgroup, res *specs.LinuxResources) (cgroup.Cgroup, error) {
|
||||
// TODO(gvisor.dev/issue/3481): Remove when cgroups v2 is supported.
|
||||
if cgroup.IsOnlyV2() {
|
||||
if conf.Rootless {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("cgroups V2 is not yet supported. Enable cgroups V1 and retry")
|
||||
}
|
||||
if err := cg.Install(res); err != nil {
|
||||
switch {
|
||||
case errors.Is(err, unix.EACCES) && conf.Rootless:
|
||||
|
||||
@@ -777,7 +777,7 @@ func (s *Sandbox) createSandboxProcess(conf *config.Config, args *Args, startSyn
|
||||
|
||||
quota, err := s.CgroupJSON.Cgroup.CPUQuota()
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting cpu qouta from cgroups: %v", err)
|
||||
return fmt.Errorf("getting cpu quota from cgroups: %v", err)
|
||||
}
|
||||
if n := int(math.Ceil(quota)); n > 0 {
|
||||
if n < minCPUs {
|
||||
|
||||
+167
-3
@@ -86,8 +86,15 @@ func TestMemCgroup(t *testing.T) {
|
||||
// or after looping below (so the application can start).
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
var path string
|
||||
|
||||
// Read the cgroup memory limit.
|
||||
if cgroup.IsOnlyV2() {
|
||||
path = filepath.Join("/sys/fs/cgroup/docker", gid, "memory.max")
|
||||
} else {
|
||||
path = filepath.Join("/sys/fs/cgroup/memory/docker", gid, "memory.limit_in_bytes")
|
||||
}
|
||||
// Read the cgroup memory limit.
|
||||
path := filepath.Join("/sys/fs/cgroup/memory/docker", gid, "memory.limit_in_bytes")
|
||||
outRaw, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
// It's possible that the container does not exist yet.
|
||||
@@ -103,8 +110,14 @@ func TestMemCgroup(t *testing.T) {
|
||||
continue
|
||||
}
|
||||
|
||||
if cgroup.IsOnlyV2() {
|
||||
// v2 does not have max_usage_in_bytes equivalent, so memory.current is the
|
||||
// next best thing that we can use
|
||||
path = filepath.Join("/sys/fs/cgroup/docker", gid, "memory.current")
|
||||
} else {
|
||||
path = filepath.Join("/sys/fs/cgroup/memory/docker", gid, "memory.max_usage_in_bytes")
|
||||
}
|
||||
// Read the cgroup memory usage.
|
||||
path = filepath.Join("/sys/fs/cgroup/memory/docker", gid, "memory.max_usage_in_bytes")
|
||||
outRaw, err = ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("error reading usage: %v", err)
|
||||
@@ -126,7 +139,10 @@ func TestMemCgroup(t *testing.T) {
|
||||
}
|
||||
|
||||
// TestCgroup sets cgroup options and checks that cgroup was properly configured.
|
||||
func TestCgroup(t *testing.T) {
|
||||
func TestCgroupV1(t *testing.T) {
|
||||
if cgroup.IsOnlyV2() {
|
||||
t.Skip("skipping cgroupv1 attribute testing in cgroupv2 setup")
|
||||
}
|
||||
ctx := context.Background()
|
||||
d := dockerutil.MakeContainer(ctx, t)
|
||||
defer d.CleanUp(ctx)
|
||||
@@ -308,6 +324,154 @@ func TestCgroup(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestCgroupV2 sets cgroup options and checks that cgroup was properly configured with
|
||||
// cgroupv2 setup
|
||||
func TestCgroupV2(t *testing.T) {
|
||||
if !cgroup.IsOnlyV2() {
|
||||
t.Skip("skipping cgroupv2 attribute testing in cgroupv1 setup")
|
||||
}
|
||||
ctx := context.Background()
|
||||
d := dockerutil.MakeContainer(ctx, t)
|
||||
defer d.CleanUp(ctx)
|
||||
|
||||
// This is not a comprehensive list of attributes.
|
||||
//
|
||||
// Note that we are specifically missing cpusets, which fail if specified.
|
||||
// In any case, it's unclear if cpusets can be reliably tested here: these
|
||||
// are often run on a single core virtual machine, and there is only a single
|
||||
// CPU available in our current set, and every container's set.
|
||||
attrs := []struct {
|
||||
field string
|
||||
value int64
|
||||
file string
|
||||
want string
|
||||
skipIfNotFound bool
|
||||
}{
|
||||
{
|
||||
field: "cpu-shares",
|
||||
value: 3333,
|
||||
file: "cpu.weight",
|
||||
want: "128",
|
||||
},
|
||||
{
|
||||
field: "cpu-period",
|
||||
value: 2000,
|
||||
file: "cpu.max",
|
||||
want: "max 2000",
|
||||
},
|
||||
{
|
||||
field: "memory",
|
||||
value: 1 << 30,
|
||||
file: "memory.max",
|
||||
want: "1073741824",
|
||||
},
|
||||
{
|
||||
field: "memory-reservation",
|
||||
value: 500 << 20,
|
||||
file: "memory.low",
|
||||
want: "524288000",
|
||||
},
|
||||
{
|
||||
field: "memory-swap",
|
||||
value: 1 << 31,
|
||||
file: "memory.swap.max",
|
||||
// memory.swap.max is only the swap value, unlike cgroupv1
|
||||
want: fmt.Sprintf("%d", 1<<31-1<<30),
|
||||
skipIfNotFound: true, // swap may be disabled on the machine.
|
||||
},
|
||||
{
|
||||
field: "blkio-weight",
|
||||
value: 750,
|
||||
file: "io.bfq.weight",
|
||||
want: fmt.Sprintf("default %d", 750),
|
||||
skipIfNotFound: true, // blkio groups may not be available.
|
||||
},
|
||||
{
|
||||
field: "pids-limit",
|
||||
value: 1000,
|
||||
file: "pids.max",
|
||||
want: "1000",
|
||||
},
|
||||
}
|
||||
|
||||
// Make configs.
|
||||
conf, hostconf, _ := d.ConfigsFrom(dockerutil.RunOpts{
|
||||
Image: "basic/alpine",
|
||||
}, "sleep", "10000")
|
||||
|
||||
// Add Cgroup arguments to configs.
|
||||
for _, attr := range attrs {
|
||||
switch attr.field {
|
||||
case "cpu-shares":
|
||||
hostconf.Resources.CPUShares = attr.value
|
||||
case "cpu-period":
|
||||
hostconf.Resources.CPUPeriod = attr.value
|
||||
case "cpu-quota":
|
||||
hostconf.Resources.CPUQuota = attr.value
|
||||
case "kernel-memory":
|
||||
hostconf.Resources.KernelMemory = attr.value
|
||||
case "memory":
|
||||
hostconf.Resources.Memory = attr.value
|
||||
case "memory-reservation":
|
||||
hostconf.Resources.MemoryReservation = attr.value
|
||||
case "memory-swap":
|
||||
hostconf.Resources.MemorySwap = attr.value
|
||||
case "memory-swappiness":
|
||||
val := attr.value
|
||||
hostconf.Resources.MemorySwappiness = &val
|
||||
case "blkio-weight":
|
||||
// detect existence of io.bfq.weight as this is not always loaded
|
||||
_, err := ioutil.ReadFile(filepath.Join("/sys/fs/cgroup/docker", attr.file))
|
||||
if err == nil || !attr.skipIfNotFound {
|
||||
hostconf.Resources.BlkioWeight = uint16(attr.value)
|
||||
}
|
||||
case "pids-limit":
|
||||
val := attr.value
|
||||
hostconf.Resources.PidsLimit = &val
|
||||
}
|
||||
}
|
||||
|
||||
// Create container.
|
||||
if err := d.CreateFrom(ctx, "basic/alpine", conf, hostconf, nil); err != nil {
|
||||
t.Fatalf("create failed with: %v", err)
|
||||
}
|
||||
|
||||
// Start container.
|
||||
if err := d.Start(ctx); err != nil {
|
||||
t.Fatalf("start failed with: %v", err)
|
||||
}
|
||||
|
||||
// Lookup the relevant cgroup ID.
|
||||
gid := d.ID()
|
||||
t.Logf("cgroup ID: %s", gid)
|
||||
|
||||
// Check list of attributes defined above.
|
||||
for _, attr := range attrs {
|
||||
path := filepath.Join("/sys/fs/cgroup/docker", gid, attr.file)
|
||||
out, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) && attr.skipIfNotFound {
|
||||
t.Logf("skipped %s", attr.file)
|
||||
continue
|
||||
}
|
||||
t.Fatalf("failed to read %q: %v", path, err)
|
||||
}
|
||||
if got := strings.TrimSpace(string(out)); got != attr.want {
|
||||
t.Errorf("field: %q, cgroup attribute %s, got: %q, want: %q", attr.field, attr.file, got, attr.want)
|
||||
}
|
||||
}
|
||||
|
||||
// Check that sandbox is inside cgroup.
|
||||
pid, err := d.SandboxPid(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("SandboxPid: %v", err)
|
||||
}
|
||||
path := filepath.Join("/sys/fs/cgroup/docker", gid, "cgroup.procs")
|
||||
if err := verifyPid(pid, path); err != nil {
|
||||
t.Errorf("cgroup control processes: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCgroupParent sets the "CgroupParent" option and checks that the child and
|
||||
// parent's cgroups are created correctly relative to each other.
|
||||
func TestCgroupParent(t *testing.T) {
|
||||
|
||||
@@ -26,6 +26,12 @@ if [[ "${CONTAINERD_MAJOR}" -eq 1 ]] && [[ "${CONTAINERD_MINOR}" -le 4 ]]; then
|
||||
export GO111MODULE=off
|
||||
fi
|
||||
|
||||
# containerd < 1.4 doesn't work with cgroupv2 setup, so we check for that here
|
||||
if [[ "$(stat -f -c %T /sys/fs/cgroup 2>/dev/null)" == "cgroup2fs" && "${CONTAINERD_MAJOR}" -eq 1 && "${CONTAINERD_MINOR}" -lt 4 ]]; then
|
||||
echo "containerd < 1.4 does not work with cgroup2"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Helper for Go packages below.
|
||||
install_helper() {
|
||||
declare -r PACKAGE="${1}"
|
||||
|
||||
Reference in New Issue
Block a user