libct/cg: add CFS bandwidth burst for CPU

Burstable CFS controller is introduced in Linux 5.14. This helps with
parallel workloads that might be bursty. They can get throttled even
when their average utilization is under quota. And they may be latency
sensitive at the same time so that throttling them is undesired.

This feature borrows time now against the future underrun, at the cost
of increased interference against the other system users, by introducing
cfs_burst_us into CFS bandwidth control to enact the cap on unused
bandwidth accumulation, which will then used additionally for burst.

The patch adds the support/control for CFS bandwidth burst.

runtime-spec: https://github.com/opencontainers/runtime-spec/pull/1120

Co-authored-by: Akihiro Suda <suda.kyoto@gmail.com>
Co-authored-by: Nadeshiko Manju <me@manjusaka.me>
Signed-off-by: Kailun Qin <kailun.qin@intel.com>
This commit is contained in:
Kailun Qin
2021-09-09 18:29:31 -04:00
committed by Manjusaka
parent 319b2ba730
commit e1584831b6
12 changed files with 108 additions and 4 deletions
+1
View File
@@ -735,6 +735,7 @@ _runc_update() {
--blkio-weight --blkio-weight
--cpu-period --cpu-period
--cpu-quota --cpu-quota
--cpu-burst
--cpu-rt-period --cpu-rt-period
--cpu-rt-runtime --cpu-rt-runtime
--cpu-share --cpu-share
-1
View File
@@ -10,7 +10,6 @@ Spec version | Feature | PR
v1.0.0 | `SCMP_ARCH_PARISC` | Unplanned, due to lack of users v1.0.0 | `SCMP_ARCH_PARISC` | Unplanned, due to lack of users
v1.0.0 | `SCMP_ARCH_PARISC64` | Unplanned, due to lack of users v1.0.0 | `SCMP_ARCH_PARISC64` | Unplanned, due to lack of users
v1.0.2 | `.linux.personality` | [#3126](https://github.com/opencontainers/runc/pull/3126) v1.0.2 | `.linux.personality` | [#3126](https://github.com/opencontainers/runc/pull/3126)
v1.1.0 | `.linux.resources.cpu.burst` | [#3749](https://github.com/opencontainers/runc/pull/3749)
v1.1.0 | `SECCOMP_FILTER_FLAG_WAIT_KILLABLE_RECV` | [#3862](https://github.com/opencontainers/runc/pull/3862) v1.1.0 | `SECCOMP_FILTER_FLAG_WAIT_KILLABLE_RECV` | [#3862](https://github.com/opencontainers/runc/pull/3862)
v1.1.0 | time namespaces | [#3876](https://github.com/opencontainers/runc/pull/3876) v1.1.0 | time namespaces | [#3876](https://github.com/opencontainers/runc/pull/3876)
v1.1.0 | rsvd hugetlb cgroup | TODO ([#3859](https://github.com/opencontainers/runc/issues/3859)) v1.1.0 | rsvd hugetlb cgroup | TODO ([#3859](https://github.com/opencontainers/runc/issues/3859))
+29
View File
@@ -84,6 +84,28 @@ func (s *CpuGroup) Set(path string, r *configs.Resources) error {
period = "" period = ""
} }
} }
var burst string
if r.CpuBurst != nil {
burst = strconv.FormatUint(*r.CpuBurst, 10)
if err := cgroups.WriteFile(path, "cpu.cfs_burst_us", burst); err != nil {
// this is a special trick for burst feature, the current systemd and low version of kernel will not support it.
// So, an `no such file or directory` error would be raised, and we can ignore it .
if !errors.Is(err, unix.ENOENT) {
// 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 r.CpuQuota != 0 {
if err := cgroups.WriteFile(path, "cpu.cfs_quota_us", strconv.FormatInt(r.CpuQuota, 10)); err != nil { if err := cgroups.WriteFile(path, "cpu.cfs_quota_us", strconv.FormatInt(r.CpuQuota, 10)); err != nil {
return err return err
@@ -93,6 +115,13 @@ func (s *CpuGroup) Set(path string, r *configs.Resources) error {
return err return err
} }
} }
if burst != "" {
if err := cgroups.WriteFile(path, "cpu.cfs_burst_us", burst); err != nil {
if !errors.Is(err, unix.ENOENT) {
return err
}
}
}
} }
if r.CPUIdle != nil { if r.CPUIdle != nil {
+12
View File
@@ -45,6 +45,7 @@ func TestCpuSetBandWidth(t *testing.T) {
const ( const (
quotaBefore = 8000 quotaBefore = 8000
quotaAfter = 5000 quotaAfter = 5000
burstBefore = 2000
periodBefore = 10000 periodBefore = 10000
periodAfter = 7000 periodAfter = 7000
rtRuntimeBefore = 8000 rtRuntimeBefore = 8000
@@ -52,9 +53,11 @@ func TestCpuSetBandWidth(t *testing.T) {
rtPeriodBefore = 10000 rtPeriodBefore = 10000
rtPeriodAfter = 7000 rtPeriodAfter = 7000
) )
burstAfter := uint64(1000)
writeFileContents(t, path, map[string]string{ writeFileContents(t, path, map[string]string{
"cpu.cfs_quota_us": strconv.Itoa(quotaBefore), "cpu.cfs_quota_us": strconv.Itoa(quotaBefore),
"cpu.cfs_burst_us": strconv.Itoa(burstBefore),
"cpu.cfs_period_us": strconv.Itoa(periodBefore), "cpu.cfs_period_us": strconv.Itoa(periodBefore),
"cpu.rt_runtime_us": strconv.Itoa(rtRuntimeBefore), "cpu.rt_runtime_us": strconv.Itoa(rtRuntimeBefore),
"cpu.rt_period_us": strconv.Itoa(rtPeriodBefore), "cpu.rt_period_us": strconv.Itoa(rtPeriodBefore),
@@ -62,6 +65,7 @@ func TestCpuSetBandWidth(t *testing.T) {
r := &configs.Resources{ r := &configs.Resources{
CpuQuota: quotaAfter, CpuQuota: quotaAfter,
CpuBurst: &burstAfter,
CpuPeriod: periodAfter, CpuPeriod: periodAfter,
CpuRtRuntime: rtRuntimeAfter, CpuRtRuntime: rtRuntimeAfter,
CpuRtPeriod: rtPeriodAfter, CpuRtPeriod: rtPeriodAfter,
@@ -79,6 +83,14 @@ func TestCpuSetBandWidth(t *testing.T) {
t.Fatal("Got the wrong value, set cpu.cfs_quota_us failed.") t.Fatal("Got the wrong value, set cpu.cfs_quota_us failed.")
} }
burst, err := fscommon.GetCgroupParamUint(path, "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(path, "cpu.cfs_period_us") period, err := fscommon.GetCgroupParamUint(path, "cpu.cfs_period_us")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
+1 -1
View File
@@ -191,7 +191,7 @@ func (m *Manager) Set(r *configs.Resources) error {
if path == "" { if path == "" {
// We never created a path for this cgroup, so we cannot set // We never created a path for this cgroup, so we cannot set
// limits for it (though we have already tried at this point). // limits for it (though we have already tried at this point).
return fmt.Errorf("cannot set %s limit: container could not join or create cgroup", sys.Name()) return fmt.Errorf("cannot set %s limit: container could not join or create cgroup, and the error is %w", sys.Name(), err)
} }
return err return err
} }
+26 -1
View File
@@ -2,16 +2,19 @@ package fs2
import ( import (
"bufio" "bufio"
"errors"
"os" "os"
"strconv" "strconv"
"golang.org/x/sys/unix"
"github.com/opencontainers/runc/libcontainer/cgroups" "github.com/opencontainers/runc/libcontainer/cgroups"
"github.com/opencontainers/runc/libcontainer/cgroups/fscommon" "github.com/opencontainers/runc/libcontainer/cgroups/fscommon"
"github.com/opencontainers/runc/libcontainer/configs" "github.com/opencontainers/runc/libcontainer/configs"
) )
func isCpuSet(r *configs.Resources) bool { func isCpuSet(r *configs.Resources) bool {
return r.CpuWeight != 0 || r.CpuQuota != 0 || r.CpuPeriod != 0 || r.CPUIdle != nil return r.CpuWeight != 0 || r.CpuQuota != 0 || r.CpuPeriod != 0 || r.CPUIdle != nil || r.CpuBurst != nil
} }
func setCpu(dirPath string, r *configs.Resources) error { func setCpu(dirPath string, r *configs.Resources) error {
@@ -32,6 +35,23 @@ func setCpu(dirPath string, r *configs.Resources) error {
} }
} }
var burst string
if r.CpuBurst != nil {
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 { if r.CpuQuota != 0 || r.CpuPeriod != 0 {
str := "max" str := "max"
if r.CpuQuota > 0 { if r.CpuQuota > 0 {
@@ -47,6 +67,11 @@ func setCpu(dirPath string, r *configs.Resources) error {
if err := cgroups.WriteFile(dirPath, "cpu.max", str); err != nil { if err := cgroups.WriteFile(dirPath, "cpu.max", str); err != nil {
return err return err
} }
if burst != "" {
if err := cgroups.WriteFile(dirPath, "cpu.max.burst", burst); err != nil {
return err
}
}
} }
return nil return nil
+3
View File
@@ -69,6 +69,9 @@ type Resources struct {
// CPU hardcap limit (in usecs). Allowed cpu time in a given period. // CPU hardcap limit (in usecs). Allowed cpu time in a given period.
CpuQuota int64 `json:"cpu_quota"` 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"` //nolint:revive
// CPU period to be used for hardcapping (in usecs). 0 to use system default. // CPU period to be used for hardcapping (in usecs). 0 to use system default.
CpuPeriod uint64 `json:"cpu_period"` CpuPeriod uint64 `json:"cpu_period"`
+3
View File
@@ -755,6 +755,9 @@ func CreateCgroupConfig(opts *CreateOpts, defaultDevs []*devices.Device) (*confi
if r.CPU.Quota != nil { if r.CPU.Quota != nil {
c.Resources.CpuQuota = *r.CPU.Quota c.Resources.CpuQuota = *r.CPU.Quota
} }
if r.CPU.Burst != nil {
c.Resources.CpuBurst = r.CPU.Burst
}
if r.CPU.Period != nil { if r.CPU.Period != nil {
c.Resources.CpuPeriod = *r.CPU.Period c.Resources.CpuPeriod = *r.CPU.Period
} }
+4
View File
@@ -28,6 +28,7 @@ In case **-r** is used, the JSON format is like this:
"cpu": { "cpu": {
"shares": 0, "shares": 0,
"quota": 0, "quota": 0,
"burst": 0,
"period": 0, "period": 0,
"realtimeRuntime": 0, "realtimeRuntime": 0,
"realtimePeriod": 0, "realtimePeriod": 0,
@@ -53,6 +54,9 @@ stdin. If this option is used, all other options are ignored.
**--cpu-quota** _num_ **--cpu-quota** _num_
: Set CPU usage limit within a given period (in microseconds). : 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_ **--cpu-rt-period** _num_
: Set CPU realtime period to be used for hardcapping (in microseconds). : Set CPU realtime period to be used for hardcapping (in microseconds).
+15 -1
View File
@@ -260,11 +260,16 @@ function get_cgroup_value() {
# Helper to check a if value in a cgroup file matches the expected one. # Helper to check a if value in a cgroup file matches the expected one.
function check_cgroup_value() { function check_cgroup_value() {
local current local current
local cgroup
cgroup="$(get_cgroup_path "$1")"
if [ ! -f "$cgroup/$1" ]; then
skip "$cgroup/$1 does not exist"
fi
current="$(get_cgroup_value "$1")" current="$(get_cgroup_value "$1")"
local expected=$2 local expected=$2
echo "current $current !? $expected" echo "current $current !? $expected"
[ "$current" = "$expected" ] [ "$current" = "$expected" ] || [ "$current" = "$((expected / 1000))" ]
} }
# Helper to check a value in systemd. # Helper to check a value in systemd.
@@ -310,6 +315,15 @@ function check_cpu_quota() {
check_systemd_value "CPUQuotaPeriodUSec" $sd_period $sd_infinity check_systemd_value "CPUQuotaPeriodUSec" $sd_period $sd_infinity
} }
function check_cpu_burst() {
local burst=$1
if [ -v CGROUP_V2 ]; then
check_cgroup_value "cpu.max.burst" "$burst"
else
check_cgroup_value "cpu.cfs_burst_us" "$burst"
fi
}
# Works for cgroup v1 and v2, accepts v1 shares as an argument. # Works for cgroup v1 and v2, accepts v1 shares as an argument.
function check_cpu_shares() { function check_cpu_shares() {
local shares=$1 local shares=$1
+6
View File
@@ -288,6 +288,12 @@ EOF
runc update test_update --cpu-share 200 runc update test_update --cpu-share 200
[ "$status" -eq 0 ] [ "$status" -eq 0 ]
check_cpu_shares 200 check_cpu_shares 200
runc update test_update --cpu-period 900000 --cpu-burst 500000
[ "$status" -eq 0 ]
check_cpu_burst 500000
runc update test_update --cpu-period 900000 --cpu-burst 0
[ "$status" -eq 0 ]
check_cpu_burst 0
# Revert to the test initial value via json on stding # Revert to the test initial value via json on stding
runc update -r - test_update <<EOF runc update -r - test_update <<EOF
+8
View File
@@ -44,6 +44,7 @@ The accepted format is as follow (unchanged values can be omitted):
"cpu": { "cpu": {
"shares": 0, "shares": 0,
"quota": 0, "quota": 0,
"burst": 0,
"period": 0, "period": 0,
"realtimeRuntime": 0, "realtimeRuntime": 0,
"realtimePeriod": 0, "realtimePeriod": 0,
@@ -73,6 +74,10 @@ other options are ignored.
Name: "cpu-quota", Name: "cpu-quota",
Usage: "CPU CFS hardcap limit (in usecs). Allowed cpu time in a given period", 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{ cli.StringFlag{
Name: "cpu-share", Name: "cpu-share",
Usage: "CPU shares (relative weight vs. other containers)", Usage: "CPU shares (relative weight vs. other containers)",
@@ -153,6 +158,7 @@ other options are ignored.
CPU: &specs.LinuxCPU{ CPU: &specs.LinuxCPU{
Shares: u64Ptr(0), Shares: u64Ptr(0),
Quota: i64Ptr(0), Quota: i64Ptr(0),
Burst: u64Ptr(0),
Period: u64Ptr(0), Period: u64Ptr(0),
RealtimeRuntime: i64Ptr(0), RealtimeRuntime: i64Ptr(0),
RealtimePeriod: u64Ptr(0), RealtimePeriod: u64Ptr(0),
@@ -210,6 +216,7 @@ other options are ignored.
opt string opt string
dest *uint64 dest *uint64
}{ }{
{"cpu-burst", r.CPU.Burst},
{"cpu-period", r.CPU.Period}, {"cpu-period", r.CPU.Period},
{"cpu-rt-period", r.CPU.RealtimePeriod}, {"cpu-rt-period", r.CPU.RealtimePeriod},
{"cpu-share", r.CPU.Shares}, {"cpu-share", r.CPU.Shares},
@@ -299,6 +306,7 @@ other options are ignored.
} }
} }
config.Cgroups.Resources.CpuBurst = r.CPU.Burst
config.Cgroups.Resources.CpuShares = *r.CPU.Shares config.Cgroups.Resources.CpuShares = *r.CPU.Shares
// CpuWeight is used for cgroupv2 and should be converted // CpuWeight is used for cgroupv2 and should be converted
config.Cgroups.Resources.CpuWeight = cgroups.ConvertCPUSharesToCgroupV2Value(*r.CPU.Shares) config.Cgroups.Resources.CpuWeight = cgroups.ConvertCPUSharesToCgroupV2Value(*r.CPU.Shares)