mirror of
https://github.com/opencontainers/runc.git
synced 2026-07-11 06:03:57 +08:00
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:
@@ -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
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"`
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user