Path: blob/main/components/ws-daemon/pkg/cgroup/plugin_iolimit_v2.go
2499 views
// Copyright (c) 2022 Gitpod GmbH. All rights reserved.1// Licensed under the GNU Affero General Public License (AGPL).2// See License.AGPL.txt in the project root for license information.34package cgroup56import (7"context"8"os"9"path/filepath"10"strconv"11"strings"12"sync"1314v2 "github.com/containerd/cgroups/v2"15"github.com/gitpod-io/gitpod/common-go/log"16)1718type IOLimiterV2 struct {19limits *v2.Resources2021cond *sync.Cond2223devices []string24}2526func NewIOLimiterV2(writeBytesPerSecond, readBytesPerSecond, writeIOPs, readIOPs int64) (*IOLimiterV2, error) {27devices := buildDevices()28log.WithField("devices", devices).Debug("io limiting devices")29return &IOLimiterV2{30limits: buildV2Limits(writeBytesPerSecond, readBytesPerSecond, writeIOPs, readIOPs, devices),3132cond: sync.NewCond(&sync.Mutex{}),33devices: devices,34}, nil35}3637func (c *IOLimiterV2) Name() string { return "iolimiter-v2" }38func (c *IOLimiterV2) Type() Version { return Version2 }3940func (c *IOLimiterV2) Apply(ctx context.Context, opts *PluginOptions) error {41update := make(chan struct{}, 1)42go func() {43// TODO(cw): this Go-routine will leak per workspace, until we update config or restart ws-daemon44defer close(update)4546for {47c.cond.L.Lock()48c.cond.Wait()49c.cond.L.Unlock()5051if ctx.Err() != nil {52return53}5455update <- struct{}{}56}57}()5859go func() {60log.WithFields(log.OWI("", "", opts.InstanceId)).WithField("cgroupPath", opts.CgroupPath).Debug("starting io limiting")6162_, err := v2.NewManager(opts.BasePath, filepath.Join("/", opts.CgroupPath), c.limits)63if err != nil {64log.WithError(err).WithFields(log.OWI("", "", opts.InstanceId)).WithField("basePath", opts.BasePath).WithField("cgroupPath", opts.CgroupPath).WithField("limits", c.limits).Warn("cannot write IO limits")65}6667for {68select {69case <-update:70_, err := v2.NewManager(opts.BasePath, filepath.Join("/", opts.CgroupPath), c.limits)71if err != nil {72log.WithError(err).WithFields(log.OWI("", "", opts.InstanceId)).WithField("basePath", opts.BasePath).WithField("cgroupPath", opts.CgroupPath).WithField("limits", c.limits).Error("cannot write IO limits")73}74case <-ctx.Done():75// Prior to shutting down though, we need to reset the IO limits to ensure we don't have76// processes stuck in the uninterruptable "D" (disk sleep) state. This would prevent the77// workspace pod from shutting down.78_, err := v2.NewManager(opts.BasePath, filepath.Join("/", opts.CgroupPath), &v2.Resources{})79if err != nil {80log.WithError(err).WithFields(log.OWI("", "", opts.InstanceId)).WithField("cgroupPath", opts.CgroupPath).Error("cannot write IO limits")81}82log.WithFields(log.OWI("", "", opts.InstanceId)).WithField("cgroupPath", opts.CgroupPath).Debug("stopping io limiting")83return84}85}86}()8788return nil89}9091func (c *IOLimiterV2) Update(writeBytesPerSecond, readBytesPerSecond, writeIOPs, readIOPs int64) {92c.cond.L.Lock()93defer c.cond.L.Unlock()9495c.limits = buildV2Limits(writeBytesPerSecond, readBytesPerSecond, writeIOPs, readIOPs, c.devices)96log.WithField("limits", c.limits.IO).Info("updating I/O cgroups v2 limits")9798c.cond.Broadcast()99}100101func buildV2Limits(writeBytesPerSecond, readBytesPerSecond, writeIOPs, readIOPs int64, devices []string) *v2.Resources {102resources := &v2.Resources{103IO: &v2.IO{},104}105106for _, device := range devices {107majmin := strings.Split(device, ":")108if len(majmin) != 2 {109log.WithField("device", device).Error("invalid device")110continue111}112113major, err := strconv.ParseInt(majmin[0], 10, 64)114if err != nil {115log.WithError(err).Error("invalid major device")116continue117}118119minor, err := strconv.ParseInt(majmin[1], 10, 64)120if err != nil {121log.WithError(err).Error("invalid minor device")122continue123}124125if readBytesPerSecond > 0 {126resources.IO.Max = append(resources.IO.Max, v2.Entry{Major: major, Minor: minor, Type: v2.ReadBPS, Rate: uint64(readBytesPerSecond)})127}128129if readIOPs > 0 {130resources.IO.Max = append(resources.IO.Max, v2.Entry{Major: major, Minor: minor, Type: v2.ReadIOPS, Rate: uint64(readIOPs)})131}132133if writeBytesPerSecond > 0 {134resources.IO.Max = append(resources.IO.Max, v2.Entry{Major: major, Minor: minor, Type: v2.WriteBPS, Rate: uint64(writeBytesPerSecond)})135}136137if writeIOPs > 0 {138resources.IO.Max = append(resources.IO.Max, v2.Entry{Major: major, Minor: minor, Type: v2.WriteIOPS, Rate: uint64(writeIOPs)})139}140}141142log.WithField("resources", resources).Debug("cgroups v2 limits")143144return resources145}146147// TODO: enable custom configuration148var blockDevices = []string{"dm*", "sd*", "md*", "nvme*"}149150func buildDevices() []string {151var devices []string152for _, wc := range blockDevices {153matches, err := filepath.Glob(filepath.Join("/sys/block", wc, "dev"))154if err != nil {155log.WithField("wc", wc).Warn("cannot glob devices")156continue157}158159for _, dev := range matches {160fc, err := os.ReadFile(dev)161if err != nil {162log.WithField("dev", dev).WithError(err).Error("cannot read device file")163}164devices = append(devices, strings.TrimSpace(string(fc)))165}166}167168return devices169}170171172