diff --git a/contrib/completions/bash/runc b/contrib/completions/bash/runc index 165405e91d9..5c8de1979b9 100644 --- a/contrib/completions/bash/runc +++ b/contrib/completions/bash/runc @@ -732,6 +732,7 @@ _runc_update() { --blkio-weight --cpu-period --cpu-quota + --cpu-burst --cpu-rt-period --cpu-rt-runtime --cpu-share diff --git a/libcontainer/cgroups/fs/cpu.go b/libcontainer/cgroups/fs/cpu.go index cd81cabc185..013f54cb579 100644 --- a/libcontainer/cgroups/fs/cpu.go +++ b/libcontainer/cgroups/fs/cpu.go @@ -89,6 +89,25 @@ func (s *CpuGroup) Set(path string, r *configs.Resources) error { period = "" } } + + var burst string + if r.CpuBurst != 0 { + burst = strconv.FormatUint(r.CpuBurst, 10) + if err := cgroups.WriteFile(path, "cpu.cfs_burst_us", burst); err != nil { + // Sometimes when the burst to be set is larger + // than the current one, it is rejected by the kernel + // (EINVAL) as old_quota/new_burst exceeds the parent + // cgroup quota limit. If this happens and the quota is + // going to be set, ignore the error for now and retry + // after setting the quota. + if !errors.Is(err, unix.EINVAL) || r.CpuQuota == 0 { + return err + } + } else { + burst = "" + } + } + if r.CpuQuota != 0 { if err := cgroups.WriteFile(path, "cpu.cfs_quota_us", strconv.FormatInt(r.CpuQuota, 10)); err != nil { return err @@ -98,7 +117,13 @@ func (s *CpuGroup) Set(path string, r *configs.Resources) error { return err } } + if burst != "" { + if err := cgroups.WriteFile(path, "cpu.cfs_burst_us", burst); err != nil { + return err + } + } } + return s.SetRtSched(path, r) } diff --git a/libcontainer/cgroups/fs/cpu_test.go b/libcontainer/cgroups/fs/cpu_test.go index 958e6040e72..ef571c7ea52 100644 --- a/libcontainer/cgroups/fs/cpu_test.go +++ b/libcontainer/cgroups/fs/cpu_test.go @@ -42,6 +42,8 @@ func TestCpuSetBandWidth(t *testing.T) { const ( quotaBefore = 8000 quotaAfter = 5000 + burstBefore = 2000 + burstAfter = 1000 periodBefore = 10000 periodAfter = 7000 rtRuntimeBefore = 8000 @@ -52,12 +54,14 @@ func TestCpuSetBandWidth(t *testing.T) { helper.writeFileContents(map[string]string{ "cpu.cfs_quota_us": strconv.Itoa(quotaBefore), + "cpu.cfs_burst_us": strconv.Itoa(burstBefore), "cpu.cfs_period_us": strconv.Itoa(periodBefore), "cpu.rt_runtime_us": strconv.Itoa(rtRuntimeBefore), "cpu.rt_period_us": strconv.Itoa(rtPeriodBefore), }) helper.CgroupData.config.Resources.CpuQuota = quotaAfter + helper.CgroupData.config.Resources.CpuBurst = burstAfter helper.CgroupData.config.Resources.CpuPeriod = periodAfter helper.CgroupData.config.Resources.CpuRtRuntime = rtRuntimeAfter helper.CgroupData.config.Resources.CpuRtPeriod = rtPeriodAfter @@ -74,6 +78,14 @@ func TestCpuSetBandWidth(t *testing.T) { t.Fatal("Got the wrong value, set cpu.cfs_quota_us failed.") } + burst, err := fscommon.GetCgroupParamUint(helper.CgroupPath, "cpu.cfs_burst_us") + if err != nil { + t.Fatal(err) + } + if burst != burstAfter { + t.Fatal("Got the wrong value, set cpu.cfs_burst_us failed.") + } + period, err := fscommon.GetCgroupParamUint(helper.CgroupPath, "cpu.cfs_period_us") if err != nil { t.Fatal(err) diff --git a/libcontainer/cgroups/fs2/cpu.go b/libcontainer/cgroups/fs2/cpu.go index bbbae4d58c4..ab5a077775f 100644 --- a/libcontainer/cgroups/fs2/cpu.go +++ b/libcontainer/cgroups/fs2/cpu.go @@ -2,16 +2,18 @@ package fs2 import ( "bufio" + "errors" "os" "strconv" "github.com/opencontainers/runc/libcontainer/cgroups" "github.com/opencontainers/runc/libcontainer/cgroups/fscommon" "github.com/opencontainers/runc/libcontainer/configs" + "golang.org/x/sys/unix" ) func isCpuSet(r *configs.Resources) bool { - return r.CpuWeight != 0 || r.CpuQuota != 0 || r.CpuPeriod != 0 + return r.CpuWeight != 0 || r.CpuQuota != 0 || r.CpuPeriod != 0 || r.CpuBurst != 0 } func setCpu(dirPath string, r *configs.Resources) error { @@ -26,6 +28,24 @@ func setCpu(dirPath string, r *configs.Resources) error { } } + var burst string + if r.CpuBurst != 0 { + burst = strconv.FormatUint(r.CpuBurst, 10) + if err := cgroups.WriteFile(dirPath, "cpu.max.burst", burst); err != nil { + // Sometimes when the burst to be set is larger + // than the current one, it is rejected by the kernel + // (EINVAL) as old_quota/new_burst exceeds the parent + // cgroup quota limit. If this happens and the quota is + // going to be set, ignore the error for now and retry + // after setting the quota. + if !errors.Is(err, unix.EINVAL) || r.CpuQuota == 0 { + return err + } + } else { + burst = "" + } + } + if r.CpuQuota != 0 || r.CpuPeriod != 0 { str := "max" if r.CpuQuota > 0 { @@ -41,6 +61,11 @@ func setCpu(dirPath string, r *configs.Resources) error { if err := cgroups.WriteFile(dirPath, "cpu.max", str); err != nil { return err } + if burst != "" { + if err := cgroups.WriteFile(dirPath, "cpu.max.burst", burst); err != nil { + return err + } + } } return nil diff --git a/libcontainer/configs/cgroup_linux.go b/libcontainer/configs/cgroup_linux.go index 1432b50ba26..57ee6a37b70 100644 --- a/libcontainer/configs/cgroup_linux.go +++ b/libcontainer/configs/cgroup_linux.go @@ -56,6 +56,9 @@ type Resources struct { // CPU hardcap limit (in usecs). Allowed cpu time in a given period. CpuQuota int64 `json:"cpu_quota"` + // CPU hardcap burst limit (in usecs). Allowed accumulated cpu time additionally for burst in a given period. + CpuBurst uint64 `json:"cpu_burst"` + // CPU period to be used for hardcapping (in usecs). 0 to use system default. CpuPeriod uint64 `json:"cpu_period"` diff --git a/libcontainer/specconv/spec_linux.go b/libcontainer/specconv/spec_linux.go index 991c08a1996..45465dd639d 100644 --- a/libcontainer/specconv/spec_linux.go +++ b/libcontainer/specconv/spec_linux.go @@ -536,6 +536,9 @@ func CreateCgroupConfig(opts *CreateOpts, defaultDevs []*devices.Device) (*confi if r.CPU.Quota != nil { c.Resources.CpuQuota = *r.CPU.Quota } + if r.CPU.Burst != nil { + c.Resources.CpuBurst = *r.CPU.Burst + } if r.CPU.Period != nil { c.Resources.CpuPeriod = *r.CPU.Period } diff --git a/man/runc-update.8.md b/man/runc-update.8.md index 8ceaa386fe8..8ca69451ddd 100644 --- a/man/runc-update.8.md +++ b/man/runc-update.8.md @@ -28,6 +28,7 @@ In case **-r** is used, the JSON format is like this: "cpu": { "shares": 0, "quota": 0, + "burst": 0, "period": 0, "realtimeRuntime": 0, "realtimePeriod": 0, @@ -53,6 +54,9 @@ stdin. If this option is used, all other options are ignored. **--cpu-quota** _num_ : Set CPU usage limit within a given period (in microseconds). +**--cpu-burst** _num_ +: Set CPU burst limit within a given period (in microseconds). + **--cpu-rt-period** _num_ : Set CPU realtime period to be used for hardcapping (in microseconds). diff --git a/update.go b/update.go index d02e7af90d3..89834fa8de8 100644 --- a/update.go +++ b/update.go @@ -42,6 +42,7 @@ The accepted format is as follow (unchanged values can be omitted): "cpu": { "shares": 0, "quota": 0, + "burst": 0, "period": 0, "realtimeRuntime": 0, "realtimePeriod": 0, @@ -70,6 +71,10 @@ other options are ignored. Name: "cpu-quota", Usage: "CPU CFS hardcap limit (in usecs). Allowed cpu time in a given period", }, + cli.StringFlag{ + Name: "cpu-burst", + Usage: "CPU CFS hardcap burst limit (in usecs). Allowed accumulated cpu time additionally for burst a given period", + }, cli.StringFlag{ Name: "cpu-share", Usage: "CPU shares (relative weight vs. other containers)", @@ -145,6 +150,7 @@ other options are ignored. CPU: &specs.LinuxCPU{ Shares: u64Ptr(0), Quota: i64Ptr(0), + Burst: u64Ptr(0), Period: u64Ptr(0), RealtimeRuntime: i64Ptr(0), RealtimePeriod: u64Ptr(0), @@ -194,7 +200,7 @@ other options are ignored. opt string dest *uint64 }{ - + {"cpu-burst", r.CPU.Burst}, {"cpu-period", r.CPU.Period}, {"cpu-rt-period", r.CPU.RealtimePeriod}, {"cpu-share", r.CPU.Shares}, @@ -258,30 +264,34 @@ other options are ignored. // Update the values config.Cgroups.Resources.BlkioWeight = *r.BlockIO.Weight - // Setting CPU quota and period independently does not make much sense, - // but historically runc allowed it and this needs to be supported - // to not break compatibility. + // Setting CPU quota, period and burst independently does not make much + // sense, but historically runc allowed it and this needs to be + // supported to not break compatibility. // // For systemd cgroup drivers to set CPU quota/period correctly, // it needs to know both values. For fs2 cgroup driver to be compatible // with the fs driver, it also needs to know both values. // // Here in update, previously set values are available from config. - // If only one of {quota,period} is set and the other is not, leave - // the unset parameter at the old value (don't overwrite config). - p, q := *r.CPU.Period, *r.CPU.Quota - if (p == 0 && q == 0) || (p != 0 && q != 0) { - // both values are either set or unset (0) + // If only one of {quota,period,burst} is set and the others are not, + // leave the unset parameter at the old value (don't overwrite config). + p, q, b := *r.CPU.Period, *r.CPU.Quota, *r.CPU.Burst + if (p == 0 && q == 0 && b == 0) || (p != 0 && q != 0 && b != 0) { + // all values are either set or unset (0) config.Cgroups.Resources.CpuPeriod = p config.Cgroups.Resources.CpuQuota = q + config.Cgroups.Resources.CpuBurst = b } else { - // one is set and the other is not + // one is set and the others are not if p != 0 { - // set new period, leave quota at old value + // set new period, leave quota and burst at old values config.Cgroups.Resources.CpuPeriod = p } else if q != 0 { - // set new quota, leave period at old value + // set new quota, leave period and burst at old values config.Cgroups.Resources.CpuQuota = q + } else if b != 0 { + // set new burst, leave quota and period at old values + config.Cgroups.Resources.CpuBurst = b } }